Css Tricks

Syndicate content CSS-Tricks
Tips, Tricks, and Techniques on using Cascading Style Sheets.
Updated: 18 hours 16 min ago

Making Calendars With Accessibility and Internationalization in Mind

Mon, 03/13/2023 - 3:23am

Doing a quick search here on CSS-Tricks shows just how many different ways there are to approach calendars. Some show how CSS Grid can create the layout efficiently. Some attempt to bring actual data into the mix. Some rely on a framework to help with state management.

There are many considerations when building a calendar component — far more than what is covered in the articles I linked up. If you think about it, calendars are fraught with nuance, from handling timezones and date formats to localization and even making sure dates flow from one month to the next… and that’s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.

Many developers fear the Date() object and stick with older libraries like moment.js. But while there are many “gotchas” when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!

I don’t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We’ll look into accessibility, using semantic markup and screenreader-friendly <time> -tags — as well as internationalization and formatting, using the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat-APIs.

In other words, we’re making a calendar… only without the extra dependencies you might typically see used in a tutorial like this, and with some of the nuances you might not typically see. And, in the process, I hope you’ll gain a new appreciation for newer things that JavaScript can do while getting an idea of the sorts of things that cross my mind when I’m putting something like this together.

First off, naming

What should we call our calendar component? In my native language, it would be called “kalender element”, so let’s use that and shorten that to “Kal-El” — also known as Superman’s name on the planet Krypton.

Let’s create a function to get things going:

function kalEl(settings = {}) { ... }

This method will render a single month. Later we’ll call this method from [...Array(12).keys()] to render an entire year.

Initial data and internationalization

One of the common things a typical online calendar does is highlight the current date. So let’s create a reference for that:

const today = new Date();

Next, we’ll create a “configuration object” that we’ll merge with the optional settings object of the primary method:

const config = Object.assign( { locale: (document.documentElement.getAttribute('lang') || 'en-US'), today: { day: today.getDate(), month: today.getMonth(), year: today.getFullYear() } }, settings );

We check, if the root element (<html>) contains a lang-attribute with locale info; otherwise, we’ll fallback to using en-US. This is the first step toward internationalizing the calendar.

We also need to determine which month to initially display when the calendar is rendered. That’s why we extended the config object with the primary date. This way, if no date is provided in the settings object, we’ll use the today reference instead:

const date = config.date ? new Date(config.date) : today;

We need a little more info to properly format the calendar based on locale. For example, we might not know whether the first day of the week is Sunday or Monday, depending on the locale. If we have the info, great! But if not, we’ll update it using the Intl.Locale API. The API has a weekInfo object that returns a firstDay property that gives us exactly what we’re looking for without any hassle. We can also get which days of the week are assigned to the weekend:

if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { firstDay: 7, weekend: [6, 7] };

Again, we create fallbacks. The “first day” of the week for en-US is Sunday, so it defaults to a value of 7. This is a little confusing, as the getDay method in JavaScript returns the days as [0-6], where 0 is Sunday… don’t ask me why. The weekends are Saturday and Sunday, hence [6, 7].

Before we had the Intl.Locale API and its weekInfo method, it was pretty hard to create an international calendar without many **objects and arrays with information about each locale or region. Nowadays, it’s easy-peasy. If we pass in en-GB, the method returns:

// en-GB { firstDay: 1, weekend: [6, 7], minimalDays: 4 }

In a country like Brunei (ms-BN), the weekend is Friday and Sunday:

// ms-BN { firstDay: 7, weekend: [5, 7], minimalDays: 1 }

You might wonder what that minimalDays property is. That’s the fewest days required in the first week of a month to be counted as a full week. In some regions, it might be just one day. For others, it might be a full seven days.

Next, we’ll create a render method within our kalEl-method:

const render = (date, locale) => { ... }

We still need some more data to work with before we render anything:

const month = date.getMonth(); const year = date.getFullYear(); const numOfDays = new Date(year, month + 1, 0).getDate(); const renderToday = (year === config.today.year) && (month === config.today.month);

The last one is a Boolean that checks whether today exists in the month we’re about to render.

Semantic markup

We’re going to get deeper in rendering in just a moment. But first, I want to make sure that the details we set up have semantic HTML tags associated with them. Setting that up right out of the box gives us accessibility benefits from the start.

Calendar wrapper

First, we have the non-semantic wrapper: <kal-el>. That’s fine because there isn’t a semantic <calendar> tag or anything like that. If we weren’t making a custom element, <article> might be the most appropriate element since the calendar could stand on its own page.

Month names

The <time> element is going to be a big one for us because it helps translate dates into a format that screenreaders and search engines can parse more accurately and consistently. For example, here’s how we can convey “January 2023” in our markup:

<time datetime="2023-01">January <i>2023</i></time> Day names

The row above the calendar’s dates containing the names of the days of the week can be tricky. It’s ideal if we can write out the full names for each day — e.g. Sunday, Monday, Tuesday, etc. — but that can take up a lot of space. So, let’s abbreviate the names for now inside of an <ol> where each day is a <li>:

<ol> <li><abbr title="Sunday">Sun</abbr></li> <li><abbr title="Monday">Mon</abbr></li> <!-- etc. --> </ol>

We could get tricky with CSS to get the best of both worlds. For example, if we modified the markup a bit like this:

<ol> <li> <abbr title="S">Sunday</abbr> </li> </ol>

…we get the full names by default. We can then “hide” the full name when space runs out and display the title attribute instead:

@media all and (max-width: 800px) { li abbr::after { content: attr(title); } }

But, we’re not going that way because the Intl.DateTimeFormat API can help here as well. We’ll get to that in the next section when we cover rendering.

Day numbers

Each date in the calendar grid gets a number. Each number is a list item (<li>) in an ordered list (<ol>), and the inline <time> tag wraps the actual number.

<li> <time datetime="2023-01-01">1</time> </li>

And while I’m not planning to do any styling just yet, I know I will want some way to style the date numbers. That’s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I’m going to include data-* attributes specifically for that: data-weekend and data-today.

Week numbers

There are 52 weeks in a year, sometimes 53. While it’s not super common, it can be nice to display the number for a given week in the calendar for additional context. I like having it now, even if I don’t wind up not using it. But we’ll totally use it in this tutorial.

We’ll use a data-weeknumber attribute as a styling hook and include it in the markup for each date that is the week’s first date.

<li data-day="7" data-weeknumber="1" data-weekend=""> <time datetime="2023-01-08">8</time> </li> Rendering

Let’s get the calendar on a page! We already know that <kal-el> is the name of our custom element. First thing we need to configure it is to set the firstDay property on it, so the calendar knows whether Sunday or some other day is the first day of the week.

<kal-el data-firstday="${ config.info.firstDay }">

We’ll be using template literals to render the markup. To format the dates for an international audience, we’ll use the Intl.DateTimeFormat API, again using the locale we specified earlier.

The month and year

When we call the month, we can set whether we want to use the long name (e.g. February) or the short name (e.g. Feb.). Let’s use the long name since it’s the title above the calendar:

<time datetime="${year}-${(pad(month))}"> ${new Intl.DateTimeFormat( locale, { month:'long'}).format(date)} <i>${year}</i> </time> Weekday names

For weekdays displayed above the grid of dates, we need both the long (e.g. “Sunday”) and short (abbreviated, ie. “Sun”) names. This way, we can use the “short” name when the calendar is short on space:

Intl.DateTimeFormat([locale], { weekday: 'long' }) Intl.DateTimeFormat([locale], { weekday: 'short' })

Let’s make a small helper method that makes it a little easier to call each one:

const weekdays = (firstDay, locale) => { const date = new Date(0); const arr = [...Array(7).keys()].map(i => { date.setDate(5 + i) return { long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date), short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date) } }) for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop()); return arr; }

Here’s how we invoke that in the template:

<ol> ${weekdays(config.info.firstDay,locale).map(name => ` <li> <abbr title="${name.long}">${name.short}</abbr> </li>`).join('') } </ol> Day numbers

And finally, the days, wrapped in an <ol> element:

${[...Array(numOfDays).keys()].map(i => { const cur = new Date(year, month, i + 1); let day = cur.getDay(); if (day === 0) day = 7; const today = renderToday && (config.today.day === i + 1) ? ' data-today':''; return ` <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}> <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0"> ${new Intl.NumberFormat(locale).format(i + 1)} </time> </li>` }).join('')}

Let’s break that down:

  1. We create a “dummy” array, based on the “number of days” variable, which we’ll use to iterate.
  2. We create a day variable for the current day in the iteration.
  3. We fix the discrepancy between the Intl.Locale API and getDay().
  4. If the day is equal to today, we add a data-* attribute.
  5. Finally, we return the <li> element as a string with merged data.
  6. tabindex="0" makes the element focusable, when using keyboard navigation, after any positive tabindex values (Note: you should never add positive tabindex-values)

To “pad” the numbers in the datetime attribute, we use a little helper method:

const pad = (val) => (val + 1).toString().padStart(2, '0'); Week number

Again, the “week number” is where a week falls in a 52-week calendar. We use a little helper method for that as well:

function getWeek(cur) { const date = new Date(cur.getTime()); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); const week = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7); }

I didn’t write this getWeek-method. It’s a cleaned up version of this script.

And that’s it! Thanks to the Intl.Locale, Intl.DateTimeFormat and Intl.NumberFormat APIs, we can now simply change the lang-attribute of the <html> element to change the context of the calendar based on the current region:

de-DE fa-IR zh-Hans-CN-u-nu-hanidec Styling the calendar

You might recall how all the days are just one <ol> with list items. To style these into a readable calendar, we dive into the wonderful world of CSS Grid. In fact, we can repurpose the same grid from a starter calendar template right here on CSS-Tricks, but updated a smidge with the :is() relational pseudo to optimize the code.

Notice that I’m defining configurable CSS variables along the way (and prefixing them with ---kalel- to avoid conflicts).

kal-el :is(ol, ul) { display: grid; font-size: var(--kalel-fz, small); grid-row-gap: var(--kalel-row-gap, .33em); grid-template-columns: var(--kalel-gtc, repeat(7, 1fr)); list-style: none; margin: unset; padding: unset; position: relative; }

Let’s draw borders around the date numbers to help separate them visually:

kal-el :is(ol, ul) li { border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%)); border-style: var(--kalel-li-bds, solid); border-width: var(--kalel-li-bdw, 0 0 1px 0); grid-column: var(--kalel-li-gc, initial); text-align: var(--kalel-li-tal, end); }

The seven-column grid works fine when the first day of the month is also the first day of the week for the selected locale). But that’s the exception rather than the rule. Most times, we’ll need to shift the first day of the month to a different weekday.

Remember all the extra data-* attributes we defined when writing our markup? We can hook into those to update which grid column (--kalel-li-gc) the first date number of the month is placed on:

[data-firstday="1"] [data-day="3"]:first-child { --kalel-li-gc: 1 / 4; }

In this case, we’re spanning from the first grid column to the fourth grid column — which will automatically “push” the next item (Day 2) to the fifth grid column, and so forth.

Let’s add a little style to the “current” date, so it stands out. These are just my styles. You can totally do what you’d like here.

[data-today] { --kalel-day-bdrs: 50%; --kalel-day-bg: hsl(0, 86%, 40%); --kalel-day-hover-bgc: hsl(0, 86%, 70%); --kalel-day-c: #fff; }

I like the idea of styling the date numbers for weekends differently than weekdays. I’m going to use a reddish color to style those. Note that we can reach for the :not() pseudo-class to select them while leaving the current date alone:

[data-weekend]:not([data-today]) { --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%)); }

Oh, and let’s not forget the week numbers that go before the first date number of each week. We used a data-weeknumber attribute in the markup for that, but the numbers won’t actually display unless we reveal them with CSS, which we can do on the ::before pseudo-element:

[data-weeknumber]::before { display: var(--kalel-weeknumber-d, inline-block); content: attr(data-weeknumber); position: absolute; inset-inline-start: 0; /* additional styles */ }

We’re technically done at this point! We can render a calendar grid that shows the dates for the current month, complete with considerations for localizing the data by locale, and ensuring that the calendar uses proper semantics. And all we used was vanilla JavaScript and CSS!

But let’s take this one more step

Rendering an entire year

Maybe you need to display a full year of dates! So, rather than render the current month, you might want to display all of the month grids for the current year.

Well, the nice thing about the approach we’re using is that we can call the render method as many times as we want and merely change the integer that identifies the month on each instance. Let’s call it 12 times based on the current year.

as simple as calling the render-method 12 times, and just change the integer for month — i:

[...Array(12).keys()].map(i => render( new Date(date.getFullYear(), i, date.getDate()), config.locale, date.getMonth() ) ).join('')

It’s probably a good idea to create a new parent wrapper for the rendered year. Each calendar grid is a <kal-el> element. Let’s call the new parent wrapper <jor-el>, where Jor-El is the name of Kal-El’s father.

<jor-el id="app" data-year="true"> <kal-el data-firstday="7"> <!-- etc. --> </kal-el> <!-- other months --> </jor-el>

We can use <jor-el> to create a grid for our grids. So meta!

jor-el { background: var(--jorel-bg, none); display: var(--jorel-d, grid); gap: var(--jorel-gap, 2.5rem); grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr))); padding: var(--jorel-p, 0); } Final demo CodePen Embed Fallback Bonus: Confetti Calendar

I read an excellent book called Making and Breaking the Grid the other day and stumbled on this beautiful “New Year’s poster”:

Source: Making and Breaking the Grid (2nd Edition) by Timothy Samara

I figured we could do something similar without changing anything in the HTML or JavaScript. I’ve taken the liberty to include full names for months, and numbers instead of day names, to make it more readable. Enjoy!

CodePen Embed Fallback

Making Calendars With Accessibility and Internationalization in Mind originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

5 Mistakes I Made When Starting My First React Project

Fri, 03/10/2023 - 6:41am

You know what it’s like to pick up a new language or framework. Sometimes there’s great documentation to help you find your way through it. But even the best documentation doesn’t cover absolutely everything. And when you work with something that’s new, you’re bound to find a problem that doesn’t have a written solution.

That’s how it was for me the first time I created a React project — and React is one of those frameworks with remarkable documentation, especially now with the beta docs. But I still struggled my way through. It’s been quite a while since that project, but the lessons I gained from it are still fresh in my mind. And even though there are a lot of React “how-to” tutorials in out there, I thought I’d share what I wish I knew when I first used it.

So, that’s what this article is — a list of the early mistakes I made. I hope they help make learning React a lot smoother for you.

Using create-react-app to start a project

TL;DR Use Vite or Parcel.

Create React App (CRA) is a tool that helps you set up a new React project. It creates a development environment with the best configuration options for most React projects. This means you don’t have to spend time configuring anything yourself.

As a beginner, this seemed like a great way to start my work! No configuration! Just start coding!

CRA uses two popular packages to achieve this, webpack and Babel. webpack is a web bundler that optimizes all of the assets in your project, such as JavaScript, CSS, and images. Babel is a tool that allows you to use newer JavaScript features, even if some browsers don’t support them.

Both are good, but there are newer tools that can do the job better, specifically Vite and Speedy Web Compiler (SWC).

These new and improved alternatives are faster and easier to configure than webpack and Babel. This makes it easier to adjust the configuration which is difficult to do in create-react-app without ejecting.

To use them both when setting up a new React project you have to make sure you have Node version 12 or higher installed, then run the following command.

npm create vite

You’ll be asked to pick a name for your project. Once you do that, select React from the list of frameworks. After that, you can select either Javascript + SWC or Typescript + SWC

Then you’ll have to change directory cd into your project and run the following command;

npm i && npm run dev

This should run a development server for your site with the URL localhost:5173

And it’s as simple as that.

Article on Jan 11, 2022 Adding Vite to Your Existing Web App Adam Rackis Article on Jan 18, 2022 Making a Site Work Offline Using the VitePWA Plugin Adam Rackis Article on Jan 12, 2022 Parcel CSS: A New CSS Parser, Transformer, and Minifier Chris Coyier Article on Apr 25, 2019 Using Parcel as a Bundler for React Applications Kingsley Silas Using defaultProps for default values

TL;DR Use default function parameters instead.

Data can be passed to React components through something called props. These are added to a component just like attributes in an HTML element and can be used in a component’s definition by taking the relevant values from the prop object passed in as an argument.

// App.jsx export default function App() { return <Card title="Hello" description="world" /> } // Card.jsx function Card(props) { return ( <div> <h1>{props.title}</h1> <p>{props.description}</p> </div> ); } export default Card;

If a default value is ever required for a prop, the defaultProp property can be used:

// Card.jsx function Card(props) { // ... } Card.defaultProps = { title: 'Default title', description: 'Desc', }; export default Card;

With modern JavaScript, it is possible to destructure the props object and assign a default value to it all in the function argument.

// Card.jsx function Card({title = "Default title", description= "Desc"}) { return ( <div> <h1>{title}</h1> <p>{description}</p> </div> ) } export default Card;

This is more favorable as the code that can be read by modern browsers without the need for extra transformation.

Unfortunately, defaultProps do require some transformation to be read by the browser since JSX (JavaScript XML) isn’t supported out of the box. This could potentially affect the performance of an application that is using a lot of defaultProps.

Article on Oct 23, 2019 Demonstrating Reusable React Components in a Form Kingsley Silas Article on Jun 7, 2017 I Learned How to be Productive in React in a Week and You Can, Too Sarah Drasner Article on Aug 31, 2018 Props and PropTypes in React Kingsley Silas Don’t use propTypes

TL;DR Use TypeScript.

In React, the propTypes property can be used to check if a component is being passed the correct data type for its props. They allow you to specify the type of data that should be used for each prop such as a string, number, object, etc. They also allow you to specify if a prop is required or not.

This way, if a component is passed the wrong data type or if a required prop is not being provided, then React will throw an error.

// Card.jsx import { PropTypes } from "prop-types"; function Card(props) { // ... } Card.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string, }; export default Card;

TypeScript provides a level of type safety in data that’s being passed to components. So, sure, propTypes were a good idea back when I was starting. However, now that TypeScript has become the go-to solution for type safety, I would highly recommend using it over anything else.

// Card.tsx interface CardProps { title: string, description?: string, } export default function Card(props: CardProps) { // ... }

TypeScript is a programming language that builds on top of JavaScript by adding static type-checking. TypeScript provides a more powerful type system, that can catch more potential bugs and improves the development experience.

Article on Aug 31, 2018 Props and PropTypes in React Kingsley Silas Article on Mar 27, 2018 Putting Things in Context With React Neal Fennimore Article on Nov 16, 2018 An Overview of Render Props in React Kingsley Silas Using class components

TL;DR: Write components as functions

Class components in React are created using JavaScript classes. They have a more object-oriented structure and as well as a few additional features, like the ability to use the this keyword and lifecycle methods.

// Card.jsx class Card extends React.Component { render() { return ( <div> <h1>{this.props.title}</h1> <p>{this.props.description}</p> </div> ) } } export default Card;

I prefer writing components with classes over functions, but JavaScript classes are more difficult for beginners to understand and this can get very confusing. Instead, I’d recommend writing components as functions:

// Card.jsx function Card(props) { return ( <div> <h1>{props.title}</h1> <p>{props.description}</p> </div> ) } export default Card;

Function components are simply JavaScript functions that return JSX. They are much easier to read, and do not have additional features like the this keyword and lifecycle methods which make them more performant than class components.

Function components also have the advantage of using hooks. React Hooks allow you to use state and other React features without writing a class component, making your code more readable, maintainable and reusable.

Article on Jul 6, 2019 Getting to Know the useReducer React Hook Kingsley Silas Article on May 1, 2020 Intro to React Hooks Kingsley Silas Article on Jul 15, 2022 React Hooks: The Deep Cuts Blessing Ene Anyebe Importing React unnecessarily

TL;DR: There’s no need to do it, unless you need hooks.

Since React 17 was released in 2020, it’s now unnecessary to import React at the top of your file whenever you create a component.

import React from 'react'; // Not needed! export default function Card() {}

But we had to do that before React 17 because the JSX transformer (the thing that converts JSX into regular JavaScript) used a method called React.createElement that would only work when importing React. Since then, a new transformer has been release which can transform JSX without the createElement method.

You will still need to import React to use hooks, fragments, and any other functions or components you might need from the library:

import { useState } from 'react'; export default function Card() { const [count, setCount] = useState(0); // ... } Those were my early mistakes!

Maybe “mistake” is too harsh a word since some of the better practices came about later. Still, I see plenty of instances where the “old” way of doing something is still being actively used in projects and other tutorials.

To be honest, I probably made way more than five mistakes when getting started. Anytime you reach for a new tool it is going to be more like a learning journey to use it effectively, rather than flipping a switch. But these are the things I still carry with me years later!

If you’ve been using React for a while, what are some of the things you wish you knew before you started? It would be great to get a collection going to help others avoid the same struggles.

5 Mistakes I Made When Starting My First React Project originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Creating a Clock with the New CSS sin() and cos() Trigonometry Functions

Wed, 03/08/2023 - 4:05am

CSS trigonometry functions are here! Well, they are if you’re using the latest versions of Firefox and Safari, that is. Having this sort of mathematical power in CSS opens up a whole bunch of possibilities. In this tutorial, I thought we’d dip our toes in the water to get a feel for a couple of the newer functions: sin() and cos().

There are other trigonometry functions in the pipeline — including tan() — so why focus just on sin() and cos()? They happen to be perfect for the idea I have in mind, which is to place text along the edge of a circle. That’s been covered here on CSS-Tricks when Chris shared an approach that uses a Sass mixin. That was six years ago, so let’s give it the bleeding edge treatment.

Here’s what I have in mind. Again, it’s only supported in Firefox and Safari at the moment:

CodePen Embed Fallback

So, it’s not exactly like words forming a circular shape, but we are placing text characters along the circle to form a clock face. Here’s some markup we can use to kick things off:

<div class="clock"> <div class="clock-face"> <time datetime="12:00">12</time> <time datetime="1:00">1</time> <time datetime="2:00">2</time> <time datetime="3:00">3</time> <time datetime="4:00">4</time> <time datetime="5:00">5</time> <time datetime="6:00">6</time> <time datetime="7:00">7</time> <time datetime="8:00">8</time> <time datetime="9:00">9</time> <time datetime="10:00">10</time> <time datetime="11:00">11</time> </div> </div>

Next, here are some super basic styles for the .clock-face container. I decided to use the <time> tag with a datetime attribute. 

.clock { --_ow: clamp(5rem, 60vw, 40rem); --_w: 88cqi; aspect-ratio: 1; background-color: tomato; border-radius: 50%; container-type: inline; display: grid; height: var(--_ow); place-content: center; position: relative; width var(--_ow); }

I decorated things a bit in there, but only to get the basic shape and background color to help us see what we’re doing. Notice how we save the width value in a CSS variable. We’ll use that later. Not much to look at so far:

It looks like some sort of modern art experiment, right? Let’s introduce a new variable, --_r, to store the circle’s radius, which is equal to half of the circle’s width. This way, if the width (--_w) changes, the radius value (--_r) will also update — thanks to another CSS math function, calc():

.clock { --_w: 300px; --_r: calc(var(--_w) / 2); /* rest of styles */ }

Now, a bit of math. A circle is 360 degrees. We have 12 labels on our clock, so want to place the numbers every 30 degrees (360 / 12). In math-land, a circle begins at 3 o’clock, so noon is actually minus 90 degrees from that, which is 270 degrees (360 - 90).

Let’s add another variable, --_d, that we can use to set a degree value for each number on the clock face. We’re going to increment the values by 30 degrees to complete our circle:

.clock time:nth-child(1) { --_d: 270deg; } .clock time:nth-child(2) { --_d: 300deg; } .clock time:nth-child(3) { --_d: 330deg; } .clock time:nth-child(4) { --_d: 0deg; } .clock time:nth-child(5) { --_d: 30deg; } .clock time:nth-child(6) { --_d: 60deg; } .clock time:nth-child(7) { --_d: 90deg; } .clock time:nth-child(8) { --_d: 120deg; } .clock time:nth-child(9) { --_d: 150deg; } .clock time:nth-child(10) { --_d: 180deg; } .clock time:nth-child(11) { --_d: 210deg; } .clock time:nth-child(12) { --_d: 240deg; }

OK, now’s the time to get our hands dirty with the sin() and cos() functions! What we want to do is use them to get the X and Y coordinates for each number so we can place them properly around the clock face.

The formula for the X coordinate is radius + (radius * cos(degree)). Let’s plug that into our new --_x variable:

--_x: calc(var(--_r) + (var(--_r) * cos(var(--_d))));

The formula for the Y coordinate is radius + (radius * sin(degree)). We have what we need to calculate that:

--_y: calc(var(--_r) + (var(--_r) * sin(var(--_d))));

There are a few housekeeping things we need to do to set up the numbers, so let’s put some basic styling on them to make sure they are absolutely positioned and placed with our coordinates:

.clock-face time { --_x: calc(var(--_r) + (var(--_r) * cos(var(--_d)))); --_y: calc(var(--_r) + (var(--_r) * sin(var(--_d)))); --_sz: 12cqi; display: grid; height: var(--_sz); left: var(--_x); place-content: center; position: absolute; top: var(--_y); width: var(--_sz); }

Notice --_sz, which we’ll use for the width and height of the numbers in a moment. Let’s see what we have so far.

This definitely looks more like a clock! See how the top-left corner of each number is positioned at the correct place around the circle? We need to “shrink” the radius when calculating the positions for each number. We can deduct the size of a number (--_sz) from the size of the circle (--_w), before we calculate the radius:

--_r: calc((var(--_w) - var(--_sz)) / 2);

Much better! Let’s change the colors, so it looks more elegant:

We could stop right here! We accomplished the goal of placing text around a circle, right? But what’s a clock without arms to show hours, minutes, and seconds?

Let’s use a single CSS animation for that. First, let’s add three more elements to our markup,

<div class="clock"> <!-- after <time>-tags --> <span class="arm seconds"></span> <span class="arm minutes"></span> <span class="arm hours"></span> <span class="arm center"></span> </div>

Then some common markup for all three arms. Again, most of this is just make sure the arms are absolutely positioned and placed accordingly:

.arm { background-color: var(--_abg); border-radius: calc(var(--_aw) * 2); display: block; height: var(--_ah); left: calc((var(--_w) - var(--_aw)) / 2); position: absolute; top: calc((var(--_w) / 2) - var(--_ah)); transform: rotate(0deg); transform-origin: bottom; width: var(--_aw); }

We’ll use the same animation for all three arms:

@keyframes turn { to { transform: rotate(1turn); } }

The only difference is the time the individual arms take to make a full turn. For the hours arm, it takes 12 hours to make a full turn. The animation-duration property only accepts values in milliseconds and seconds. Let’s stick with seconds, which is 43,200 seconds (60 seconds * 60 minutes * 12 hours).

animation: turn 43200s infinite;

It takes 1 hour for the minutes arm to make a full turn. But we want this to be a multi-step animation so the movement between the arms is staggered rather than linear. We’ll need 60 steps, one for each minute:

animation: turn 3600s steps(60, end) infinite;

The seconds arm is almost the same as the minutes arm, but the duration is 60 seconds instead of 60 minutes:

animation: turn 60s steps(60, end) infinite;

Let’s update the properties we created in the common styles:

.seconds { --_abg: hsl(0, 5%, 40%); --_ah: 145px; --_aw: 2px; animation: turn 60s steps(60, end) infinite; } .minutes { --_abg: #333; --_ah: 145px; --_aw: 6px; animation: turn 3600s steps(60, end) infinite; } .hours { --_abg: #333; --_ah: 110px; --_aw: 6px; animation: turn 43200s linear infinite; }

What if we want to start at the current time? We need a little bit of JavaScript:

const time = new Date(); const hour = -3600 * (time.getHours() % 12); const mins = -60 * time.getMinutes(); app.style.setProperty('--_dm', `${mins}s`); app.style.setProperty('--_dh', `${(hour+mins)}s`);

I’ve added id="app" to the clockface and set two new custom properties on it that set a negative animation-delay, as Mate Marschalko did when he shared a CSS-only clock. The getHours() method of JavaScipt’s Date object is using the 24-hour format, so we use the remainder operator to convert it into 12-hour format.

In the CSS, we need to add the animation-delay as well:

.minutes { animation-delay: var(--_dm, 0s); /* other styles */ } .hours { animation-delay: var(--_dh, 0s); /* other styles */ }

Just one more thing. Using CSS @supports and the properties we’ve already created, we can provide a fallback to browsers that do not supprt sin() and cos(). (Thank you, Temani Afif!):

@supports not (left: calc(1px * cos(45deg))) {   time {     left: 50% !important;     top: 50% !important;     transform: translate(-50%,-50%) rotate(var(--_d)) translate(var(--_r)) rotate(calc(-1*var(--_d)))   } }

And, voilà! Our clock is done! Here’s the final demo one more time. Again, it’s only supported in Firefox and Safari at the moment.

CodePen Embed Fallback What else can we do?

Just messing around here, but we can quickly turn our clock into a circular image gallery by replacing the <time> tags with <img> then updating the width (--_w) and radius (--_r) values:

CodePen Embed Fallback

Let’s try one more. I mentioned earlier how the clock looked kind of like a modern art experiment. We can lean into that and re-create a pattern I saw on a poster (that I unfortunately didn’t buy) in an art gallery the other day. As I recall, it was called “Moon” and consisted of a bunch of dots forming a circle.

We’ll use an unordered list this time since the circles don’t follow a particular order. We’re not even going to put all the list items in the markup. Instead, let’s inject them with JavaScript and add a few controls we can use to manipulate the final result.

The controls are range inputs (<input type="range">) which we’ll wrap in a <form> and listen for the input event.

<form id="controls"> <fieldset> <label>Number of rings <input type="range" min="2" max="12" value="10" id="rings" /> </label> <label>Dots per ring <input type="range" min="5" max="12" value="7" id="dots" /> </label> <label>Spread <input type="range" min="10" max="40" value="40" id="spread" /> </label> </fieldset> </form>

We’ll run this method on “input”, which will create a bunch of <li> elements with the degree (--_d) variable we used earlier applied to each one. We can also repurpose our radius variable (--_r) .

I also want the dots to be different colors. So, let’s randomize (well, not completely randomized) the HSL color value for each list item and store it as a new CSS variable, --_bgc:

const update = () => { let s = ""; for (let i = 1; i <= rings.valueAsNumber; i++) { const r = spread.valueAsNumber * i; const theta = coords(dots.valueAsNumber * i); for (let j = 0; j < theta.length; j++) { s += `<li style="--_d:${theta[j]};--_r:${r}px;--_bgc:hsl(${random( 50, 25 )},${random(90, 50)}%,${random(90, 60)}%)"></li>`; } } app.innerHTML = s; }

The random() method picks a value within a defined range of numbers:

const random = (max, min = 0, f = true) => f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;

And that’s it. We use JavaScript to render the markup, but as soon as it’s rendered, we don’t really need it. The sin() and cos() functions help us position all the dots in the right spots.

CodePen Embed Fallback Final thoughts

Placing things around a circle is a pretty basic example to demonstrate the powers of trigonometry functions like sin() and cos(). But it’s really cool that we are getting modern CSS features that provide new solutions for old workarounds I’m sure we’ll see way more interesting, complex, and creative use cases, especially as browser support comes to Chrome and Edge.

Creating a Clock with the New CSS sin() and cos() Trigonometry Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Different Ways to Get CSS Gradient Shadows

Fri, 02/10/2023 - 5:13am

It’s a question I hear asked quite often: Is it possible to create shadows from gradients instead of solid colors? There is no specific CSS property that does this (believe me, I’ve looked) and any blog post you find about it is basically a lot of CSS tricks to approximate a gradient. We’ll actually cover some of those as we go.

But first… another article about gradient shadows? Really?

Yes, this is yet another post on the topic, but it is different. Together, we’re going to push the limits to get a solution that covers something I haven’t seen anywhere else: transparency. Most of the tricks work if the element has a non-transparent background but what if we have a transparent background? We will explore this case here!

Before we start, let me introduce my gradient shadows generator. All you have to do is to adjust the configuration, and get the code. But follow along because I’m going to help you understand all the logic behind the generated code.

Table of Contents Non-transparent solution

Let’s start with the solution that’ll work for 80% of most cases. The most typical case: you are using an element with a background, and you need to add a gradient shadow to it. No transparency issues to consider there.

The solution is to rely on a pseudo-element where the gradient is defined. You place it behind the actual element and apply a blur filter to it.

.box { position: relative; } .box::before { content: ""; position: absolute; inset: -5px; /* control the spread */ transform: translate(10px, 8px); /* control the offsets */ z-index: -1; /* place the element behind */ background: /* your gradient here */; filter: blur(10px); /* control the blur */ }

It looks like a lot of code, and that’s because it is. Here’s how we could have done it with a box-shadow instead if we were using a solid color instead of a gradient.

box-shadow: 10px 8px 10px 5px orange;

That should give you a good idea of what the values in the first snippet are doing. We have X and Y offsets, the blur radius, and the spread distance. Note that we need a negative value for the spread distance that comes from the inset property.

Here’s a demo showing the gradient shadow next to a classic box-shadow:

CodePen Embed Fallback

If you look closely you will notice that both shadows are a little different, especially the blur part. It’s not a surprise because I am pretty sure the filter property’s algorithm works differently than the one for box-shadow. That’s not a big deal since the result is, in the end, quite similar.

This solution is good, but still has a few drawbacks related to the z-index: -1 declaration. Yes, there is “stacking context” happening there!

CodePen Embed Fallback

I applied a transform to the main element, and boom! The shadow is no longer below the element. This is not a bug but the logical result of a stacking context. Don’t worry, I will not start a boring explanation about stacking context (I already did that in a Stack Overflow thread), but I’ll still show you how to work around it.

The first solution that I recommend is to use a 3D transform:

.box { position: relative; transform-style: preserve-3d; } .box::before { content: ""; position: absolute; inset: -5px; transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */ background: /* .. */; filter: blur(10px); }

Instead of using z-index: -1, we will use a negative translation along the Z-axis. We will put everything inside translate3d(). Don’t forget to use transform-style: preserve-3d on the main element; otherwise, the 3D transform won’t take effect.

CodePen Embed Fallback

As far as I know, there is no side effect to this solution… but maybe you see one. If that’s the case, share it in the comment section, and let’s try to find a fix for it!

If for some reason you are unable to use a 3D transform, the other solution is to rely on two pseudo-elements — ::before and ::after. One creates the gradient shadow, and the other reproduces the main background (and other styles you might need). That way, we can easily control the stacking order of both pseudo-elements.

.box { position: relative; z-index: 0; /* We force a stacking context */ } /* Creates the shadow */ .box::before { content: ""; position: absolute; z-index: -2; inset: -5px; transform: translate(10px, 8px); background: /* .. */; filter: blur(10px); } /* Reproduces the main element styles */ .box::after { content: """; position: absolute; z-index: -1; inset: 0; /* Inherit all the decorations defined on the main element */ background: inherit; border: inherit; box-shadow: inherit; } CodePen Embed Fallback

It’s important to note that we are forcing the main element to create a stacking context by declaring z-index: 0, or any other property that do the same, on it. Also, don’t forget that pseudo-elements consider the padding box of the main element as a reference. So, if the main element has a border, you need to take that into account when defining the pseudo-element styles. You will notice that I am using inset: -2px on ::after to account for the border defined on the main element.

As I said, this solution is probably good enough in a majority of cases where you want a gradient shadow, as long as you don’t need to support transparency. But we are here for the challenge and to push the limits, so even if you don’t need what is coming next, stay with me. You will probably learn new CSS tricks that you can use elsewhere.

Transparent solution

Let’s pick up where we left off on the 3D transform and remove the background from the main element. I will start with a shadow that has both offsets and spread distance equal to 0.

CodePen Embed Fallback

The idea is to find a way to cut or hide everything inside the area of the element (inside the green border) while keeping what is outside. We are going to use clip-path for that. But you might wonder how clip-path can make a cut inside an element.

Indeed, there’s no way to do that, but we can simulate it using a particular polygon pattern:

clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0) CodePen Embed Fallback

Tada! We have a gradient shadow that supports transparency. All we did is add a clip-path to the previous code. Here is a figure to illustrate the polygon part.

The blue area is the visible part after applying the clip-path. I am only using the blue color to illustrate the concept, but in reality, we will only see the shadow inside that area. As you can see, we have four points defined with a big value (B). My big value is 100vmax, but it can be any big value you want. The idea is to ensure we have enough space for the shadow. We also have four points that are the corners of the pseudo-element.

The arrows illustrate the path that defines the polygon. We start from (-B, -B) until we reach (0,0). In total, we need 10 points. Not eight points because two points are repeated twice in the path ((-B,-B) and (0,0)).

There’s still one more thing left for us to do, and it’s to account for the spread distance and the offsets. The only reason the demo above works is because it is a particular case where the offsets and spread distance are equal to 0.

Let’s define the spread and see what happens. Remember that we use inset with a negative value to do this:

CodePen Embed Fallback

The pseudo-element is now bigger than the main element, so the clip-path cuts more than we need it to. Remember, we always need to cut the part inside the main element (the area inside the green border of the example). We need to adjust the position of the four points inside of clip-path.

.box { --s: 10px; /* the spread */ position: relative; } .box::before { inset: calc(-1 * var(--s)); clip-path: polygon( -100vmax -100vmax, 100vmax -100vmax, 100vmax 100vmax, -100vmax 100vmax, -100vmax -100vmax, calc(0px + var(--s)) calc(0px + var(--s)), calc(0px + var(--s)) calc(100% - var(--s)), calc(100% - var(--s)) calc(100% - var(--s)), calc(100% - var(--s)) calc(0px + var(--s)), calc(0px + var(--s)) calc(0px + var(--s)) ); }

We’ve defined a CSS variable, --s, for the spread distance and updated the polygon points. I didn’t touch the points where I am using the big value. I only update the points that define the corners of the pseudo-element. I increase all the zero values by --s and decrease the 100% values by --s.

CodePen Embed Fallback

It’s the same logic with the offsets. When we translate the pseudo-element, the shadow is out of alignment, and we need to rectify the polygon again and move the points in the opposite direction.

.box { --s: 10px; /* the spread */ --x: 10px; /* X offset */ --y: 8px; /* Y offset */ position: relative; } .box::before { inset: calc(-1 * var(--s)); transform: translate3d(var(--x), var(--y), -1px); clip-path: polygon( -100vmax -100vmax, 100vmax -100vmax, 100vmax 100vmax, -100vmax 100vmax, -100vmax -100vmax, calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)), calc(0px + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)), calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)), calc(100% - var(--s) - var(--x)) calc(0px + var(--s) - var(--y)), calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)) ); }

There are two more variables for the offsets: --x and --y. We use them inside of transform and we also update the clip-path values. We still don’t touch the polygon points with big values, but we offset all the others — we reduce --x from the X coordinates, and --y from the Y coordinates.

Now all we have to do is to update a few variables to control the gradient shadow. And while we are at it, let’s also make the blur radius a variable as well:

CodePen Embed Fallback

Do we still need the 3D transform trick?

It all depends on the border. Don’t forget that the reference for a pseudo-element is the padding box, so if you apply a border to your main element, you will have an overlap. You either keep the 3D transform trick or update the inset value to account for the border.

Here is the previous demo with an updated inset value in place of the 3D transform:

CodePen Embed Fallback

I‘d say this is a more suitable way to go because the spread distance will be more accurate, as it starts from the border-box instead of the padding-box. But you will need to adjust the inset value according to the main element’s border. Sometimes, the border of the element is unknown and you have to use the previous solution.

With the earlier non-transparent solution, it’s possible you will face a stacking context issue. And with the transparent solution, it’s possible you face a border issue instead. Now you have options and ways to work around those issues. The 3D transform trick is my favorite solution because it fixes all the issues (The online generator will consider it as well)

Adding a border radius

If you try adding border-radius to the element when using the non-transparent solution we started with, it is a fairly trivial task. All you need to do is to inherit the same value from the main element, and you are done.

CodePen Embed Fallback

Even if you don’t have a border radius, it’s a good idea to define border-radius: inherit. That accounts for any potential border-radius you might want to add later or a border radius that comes from somewhere else.

It’s a different story when dealing with the transparent solution. Unfortunately, it means finding another solution because clip-path cannot deal with curvatures. That means we won’t be able to cut the area inside the main element.

We will introduce the mask property to the mix.

This part was very tedious, and I struggled to find a general solution that doesn’t rely on magic numbers. I ended up with a very complex solution that uses only one pseudo-element, but the code was a lump of spaghetti that covers only a few particular cases. I don’t think it is worth exploring that route.

I decided to insert an extra element for the sake of simpler code. Here’s the markup:

<div class="box"> <sh></sh> </div>

I am using a custom element, <sh>, to avoid any potential conflict with external CSS. I could have used a <div>, but since it’s a common element, it can easily be targeted by another CSS rule coming from somewhere else that can break our code.

The first step is to position the <sh> element and purposely create an overflow:

.box { --r: 50px; position: relative; border-radius: var(--r); } .box sh { position: absolute; inset: -150px; border: 150px solid #0000; border-radius: calc(150px + var(--r)); }

The code may look a bit strange, but we’ll get to the logic behind it as we go. Next, we create the gradient shadow using a pseudo-element of <sh>.

.box { --r: 50px; position: relative; border-radius: var(--r); transform-style: preserve-3d; } .box sh { position: absolute; inset: -150px; border: 150px solid #0000; border-radius: calc(150px + var(--r)); transform: translateZ(-1px) } .box sh::before { content: ""; position: absolute; inset: -5px; border-radius: var(--r); background: /* Your gradient */; filter: blur(10px); transform: translate(10px,8px); }

As you can see, the pseudo-element uses the same code as all the previous examples. The only difference is the 3D transform defined on the <sh> element instead of the pseudo-element. For the moment, we have a gradient shadow without the transparency feature:

CodePen Embed Fallback

Note that the area of the <sh> element is defined with the black outline. Why I am doing this? Because that way, I am able to apply a mask on it to hide the part inside the green area and keep the overflowing part where we need to see the shadow.

I know it’s a bit tricky, but unlike clip-path, the mask property doesn’t account for the area outside an element to show and hide things. That’s why I was obligated to introduce the extra element — to simulate the “outside” area.

Also, note that I am using a combination of border and inset to define that area. This allows me to keep the padding-box of that extra element the same as the main element so that the pseudo-element won’t need additional calculations.

Another useful thing we get from using an extra element is that the element is fixed, and only the pseudo-element is moving (using translate). This will allow me to easily define the mask, which is the last step of this trick.

mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; CodePen Embed Fallback

It’s done! We have our gradient shadow, and it supports border-radius! You probably expected a complex mask value with oodles of gradients, but no! We only need two simple gradients and a mask-composite to complete the magic.

Let’s isolate the <sh> element to understand what is happening there:

.box sh { position: absolute; inset: -150px; border: 150px solid red; background: lightblue; border-radius: calc(150px + var(--r)); }

Here’s what we get:

CodePen Embed Fallback

Note how the inner radius matches the main element’s border-radius. I have defined a big border (150px) and a border-radius equal to the big border plus the main element’s radius. On the outside, I have a radius equal to 150px + R. On the inside, I have 150px + R - 150px = R.

We must hide the inner (blue) part and make sure the border (red) part is still visible. To do that, I’ve defined two mask layers —One that covers only the content-box area and another that covers the border-box area (the default value). Then I excluded one from another to reveal the border.

mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; CodePen Embed Fallback

I used the same technique to create a border that supports gradients and border-radius. Ana Tudor has also a good article about masking composite that I invite you to read.

Are there any drawbacks to this method?

Yes, this definitely not perfect. The first issue you may face is related to using a border on the main element. This may create a small misalignment in the radii if you don’t account for it. We have this issue in our example, but perhaps you can hardly notice it.

The fix is relatively easy: Add the border’s width for the <sh> element’s inset.

.box { --r: 50px; border-radius: var(--r); border: 2px solid; } .box sh { position: absolute; inset: -152px; /* 150px + 2px */ border: 150px solid #0000; border-radius: calc(150px + var(--r)); }

Another drawback is the big value we’re using for the border (150px in the example). This value should be big enough to contain the shadow but not too big to avoid overflow and scrollbar issues. Luckily, the online generator will calculate the optimal value considering all the parameters.

The last drawback I am aware of is when you’re working with a complex border-radius. For example, if you want a different radius applied to each corner, you must define a variable for each side. It’s not really a drawback, I suppose, but it can make your code a bit tougher to maintain.

.box { --r-top: 10px; --r-right: 40px; --r-bottom: 30px; --r-left: 20px; border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left); } .box sh { border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left)); } .box sh:before { border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left); } CodePen Embed Fallback

The online generator only considers a uniform radius for the sake of simplicity, but you now know how to modify the code if you want to consider a complex radius configuration.

Wrapping up

We’ve reached the end! The magic behind gradient shadows is no longer a mystery. I tried to cover all the possibilities and any possible issues you might face. If I missed something or you discover any issue, please feel free to report it in the comment section, and I’ll check it out.

Again, a lot of this is likely overkill considering that the de facto solution will cover most of your use cases. Nevertheless, it’s good to know the “why” and “how” behind the trick, and how to overcome its limitations. Plus, we got good exercise playing with CSS clipping and masking.

And, of course, you have the online generator you can reach for anytime you want to avoid the hassle.

Different Ways to Get CSS Gradient Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Healthcare, Selling Lemons, and the Price of Developer Experience

Thu, 02/09/2023 - 9:45am

Every now and then, a one blog post is published and it spurs a reaction or response in others that are, in turn, published as blogs posts, and a theme starts to emerge. That’s what happened this past week and the theme developed around the cost of JavaScript frameworks — a cost that, in this case, reveals just how darn important it is to use JavaScript responsibly.

Eric Bailey: Modern Health, frameworks, performance, and harm

This is where the story begins. Eric goes to a health service provider website to book an appointment and gets… a blank screen.

In addition to a terrifying amount of telemetry, Modern Health’s customer-facing experience is delivered using React and Webpack.

If you are familiar with how the web is built, what happened is pretty obvious: A website that over-relies on JavaScript to power its experience had its logic collide with one or more other errant pieces of logic that it summons. This created a deadlock.

If you do not make digital experiences for a living, what happened is not obvious at all. All you see is a tiny fake loading spinner that never stops.

D’oh. This might be mere nuisance — or even laughable — in some situations, but not when someone’s health is on the line:

A person seeking help in a time of crisis does not care about TypeScript, tree shaking, hot module replacement, A/B tests, burndown charts, NPS, OKRs, KPIs, or other startup jargon. Developer experience does not count for shit if the person using the thing they built can’t actually get what they need.

This is the big smack of reality. What happens when our tooling and reporting — the very things that are supposed to make our work more effective — get in the way of the user experience? These are tools that provide insights that can help us anticipate a user’s needs, especially in a time of need.

I realize that pointing the finger at JavaScript frameworks is already divisive. But this goes beyond whether you use React or framework d’jour. It’s about business priorities and developer experience conflicting with user experiences.

Alex Russell: The Market for Lemons

Partisans for slow, complex frameworks have successfully marketed lemons as the hot new thing, despite the pervasive failures in their wake, crowding out higher-quality options in the process.

These technologies were initially pitched on the back of “better user experiences”, but have utterly failed to deliver on that promise outside of the high-management-maturity organisations in which they were born. Transplanted into the wider web, these new stacks have proven to be expensive duds.

There’s the rub. Alex ain’t mincing words, but notice that the onus is on the way frameworks haved been marketed to developers than developers themselves. The sales pitch?

Once the lemon sellers embed the data-light idea that improved “Developer Experience” (“DX”) leads to better user outcomes, improving “DX” became and end unto itself, and many who knew better felt forced to play along. The long lead times in falsifying trickle-down UX was a feature, not a bug; they don’t need you to succeed, only to keep buying.

As marketing goes, the “DX” bait-and-switch is brilliant, but the tech isn’t delivering for anyone but developers.

Tough to stomach, right? No one wants to be duped, and it’s tough to admit a sunken cost when there is one. It gets downright personal if you’ve invested time in a specific piece of tech and effort integrating it into your stack. Development workflows are hard and settling into one is sorta like settling into a house you plan on living in a little while. But you’d want to know if your house was built on what Alex calls a “sandy foundation”.

I’d just like to pause here a moment to say I have no skin in this debate. As a web generalist, I tend to adopt new tools early for familiarity then drop them fast, relegating them to my toolshed until I find a good use for them. In other words, my knowledge is wide but not very deep in one area or thing. HTML, CSS, and JavaScript is my go-to cocktail, but I do care a great deal about user experience and know when to reach for a tool to solve a particular thing.

And let’s acknowledge that not everyone has a say in the matter. Many of us work on managed teams that are prescribed the tools we use. Alex says as much, which I think is important to call out because it’s clear this isn’t meant to be personal. It’s a statement on our priorities and making sure they along to user expectations.

Let’s alow Chris to steer us back to the story…

Chris Coyier: End-To-End Tests with Content Blockers?

So, maybe your app is built on React and it doesn’t matter why it’s that way. There’s still work to do to ensure the app is reliable and accessible.

Just blocking a file shouldn’t totally wreck a website, but it often does! In JavaScript, that may be because the developers have written first-party JavaScript (which I’ll generally allow) that depends on third-party JavaScript (which I’ll generally block).


If I block resources from tracking-website.com, now my first-party JavaScript is going to throw an error. JavaScript isn’t chill. If an error is thrown, it doesn’t execute more JavaScript further down in the file. If further down in that file is transitionToOnboarding();— that ain’t gonna work.

Maybe it’s worth revisiting your workflow and tweaking it to account to identify more points of failure.

So here’s an idea: Run your end-to-end tests in browsers that have popular content blockers with default configs installed. 

Doing so may uncover problems like this that stop your customers, and indeed people in need, from being stopped in their tracks.

Good idea! Hey, anything that helps paint a more realistic picture of how the app is used. That sort of clarity could happen a lot earlier in the process, perhaps before settling on development decisions. Know your users. Why are they using the app? How do they browse the web? Where are they phsically located? What problems could get in their way? Chris has a great talk on that, too.

Healthcare, Selling Lemons, and the Price of Developer Experience originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The truth about CSS selector performance

Tue, 02/07/2023 - 5:59am

Geez, leave it to Patrick Brosset to talk CSS performance in the most approachable and practical way possible. Not that CSS is always what’s gunking up the speed, or even the lowest hanging fruit when it comes to improving performance.

But if you’re looking for gains on the CSS side of things, Patrick has a nice way of sniffing out your most expensive selectors using Edge DevTools:

  • Crack open DevTools.
  • Head to the Performance Tab.
  • Make sure you have the “Enable advanced rendering instrumentation” option enabled. This tripped me up in the process.
  • Record a page load.
  • Open up the “Bottom-Up” tab in the report.
  • Check out your the size of your recalculated styles.

From here, click on one of the Recalculated Style events in the Main waterfall view and you’ll get a new “Selector Stats” tab. Look at all that gooey goodness!

Now you see all of the selectors that were processed and they can be sorted by how long they took, how many times they matched, the number of matching attempts, and something called “fast reject count” which I learned is the number of elements that were easy and quick to eliminate from matching.

A lot of insights here if CSS is really the bottleneck that needs investigating. But read Patrick’s full post over on the Microsoft Edge Blog because he goes much deeper into the why’s and how’s, and walks through an entire case study.

To Shared LinkPermalink on CSS-Tricks

The truth about CSS selector performance originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

The Double Emphasis Thing

Mon, 02/06/2023 - 5:06am

I used to have this boss who loved, loved, loved, loved to emphasize words. This was way back before we used a WYSIWYG editors and I’d have to handcode that crap.

<p> I used to have this boss who <em>loved</em>, <strong>loved</strong>, <strong><em>loved</em></strong>, <strong><em><u>loved</u></em></strong> to emphasize words. </p>

(Let’s not go into the colors he used for even MOAR emphasis.)

Writing all that markup never felt great. The effort it took, sure, whatever. But is it even a good idea to add overload content with double — or more! — emphases?

Different tags convey different emphasis

For starters, the <strong> and <em> tags are designed for different uses. We got them back in HTML5, where:

So, <strong> gives the content more weight in the sense it suggests that the content in it is important or urgent. Think of a warning:

Warning: The following content has been flagged for being awesome.

It might be tempting to reach for <em> to do the same thing. Italicized text can be attention-grabbing after all. But it’s really meant as a hint to use more emphasis when readingt the content in it. For example, here are two versions of the same sentence with the emphasis in different locations:

<p>I ate the <em>entire</em> plate of burritos.</p> <p>I ate the entire <em>plate</em> of burritos.</p>

Both examples stress emphasis, but on different words. And they would sound different if you were to read them out loud. That makes <em> a great way to express tone in your writing. It changes the meaning of the sentence in a way that <strong> does not.

Visual emphasis vs. semantic emphasis

Those are two things you gotta weigh when emphasizing content. Like, there are plenty of instances where you may need to italicize content without affecting the meaning of the sentence. But those can be handled with other tags that render italics:

  • <i>: This is the classic one! Before HTML5, this was used to stress emphasis with italics all over the place. Now, it’s purely used to italicize content visually without changing the semantic meaning.
  • <cite>: Indicating the source of a fact or figure. (“Source: CSS-Tricks“)
  • <address>: Used to mark up contact information, not only physical addresses, but things like email addresses and phone numbers too. (howdy@example.com)

It’s going to he the same thing with <strong>. Rather than using it for styling text you want to look heavier, it’s a better idea to use the classic <b> tag for boldfacing to avoid giving extra signficance to content that doesn’t need it. And remember, some elements like headings are already rendered in bold, thanks to the browser’s default styles. There’s no need to add even more strong emphasis.

Using italics in emphasized content (and vice versa)

There are legitimate cases where you may need to italicize part of a line that’s already emphasized. Or maybe add emphasis to a bit of text that’s already italicized.

A blockquote might be a good example. I’ve seen plenty of times where they are italicized for style, even though default browser styles don’t do it:

blockquote { font-style: italic; }

What if we need to mention a movie title in that blockquote? That should be italicized. There’s no stress emphasis needed, so an <i> tag will do. But it’s still weird to italicize something when it’s already rendered that way:

<blockquote> This movie’s opening weekend performance offers some insight in to its box office momentum as it fights to justify its enormous budget. In its first weekend, <i>Avatar: The Way of Water</i> made $134 million in North America alone and $435 million globally. </blockquote>

In a situation where we’re italicizing something within italicized content like this, we’re supposed to remove the italics from the nested element… <i> in this case.

blockquote i { font-style: normal; }

Container style queries will be super useful to nab all these instances if we get them:

blockquote { container-name: quote; font-style: italic; } @container quote (font-style: italic) { em, i, cite, address { font-style: normal; } }

This little snippet evaluates the blockquote to see if it’s font-style is set to italic. If it is, then it’ll make sure the <em>, <i>, <cite>, and <address> elements are rendered as normal text, while retaining the semantic meaning if there is one.

But back to emphasis within emphasis

I wouldn’t nest <strong> inside <em> like this:

<p>I ate the <em><strong>entire</strong></em> plate of burritos.</p>

…or nest <em> inside <strong> instead:

<p>I ate the <em><strong>entire</strong></em> plate of burritos.</p>

The rendering is fine! And it doesn’t matter what order they’re in… at least in modern browsers. Jennifer Kyrnin mentions that some browsers only render the tag nearest to the text, but I didn’t bump into that anywhere in my limited tests. But something to watch for!

The reason I wouldn’t nest one form of emphasis in another is because it simply isn’t needed. There is no grammar rule that calls for it. Like exclamation points, one form of emphasis is enough, and you ought to use the one that matches what you’re after whether it’s visual, weight, or announced emphasis.

And even though some screen readers are capable of announcing emphasized content, they won’t read the markup with any additional importance or emphasis. So, no additional accessibility perks either, as far as I can tell.

But I really want all the emphasis!

If you’re in the position where your boss is like mine and wants ALL the emphasis, I’d reach for the right HTML tag for the type of emphasis, then apply the rest of the styles with a mix of tags that don’t affect semantics with CSS to help account for anything browser styles won’t handle.

<style> /* If `em` contains `b` or `u` tags */ em:has(b, u) { color: #f8a100; } </style> <p> I used to have this boss who <em>loved</em>, <strong>loved</strong>, <strong><em>loved</em></strong>, <strong><em><u>loved</u></em></strong> to emphasize words. </p>

I might even do it with the <strong> tag too as a defensive measure:

/* If `em` contains `b` or `u` tags */ em:has(b, u), /* If `strong` contains `em` or `u` tags */ strong:has(i, u) { color: #f8a100; }

As long as we’re playing defense, we can identify errors where emphases are nested within emphases by highlighting them in red or something:

/* Highlight semantic emphases within semantic emphases */ em:has(strong), strong:has(em) { background: hsl(0deg 50% 50% / .25); border: 1px dashed hsl(0deg 50% 50% / .25); }

Then I’d probably use that snippet from the last section that removes the default italic styling from an element when it is nested in another italiczed element.

Anything else?


The Double Emphasis Thing originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

A Fancy Hover Effect For Your Avatar

Fri, 02/03/2023 - 5:11am

Do you know that kind of effect where someone’s head is poking through a circle or hole? The famous Porky Pig animation where he waves goodbye while popping out of a series of red rings is the perfect example, and Kilian Valkhof actually re-created that here on CSS-Tricks a while back.

I have a similar idea but tackled a different way and with a sprinkle of animation. I think it’s pretty practical and makes for a neat hover effect you can use on something like your own avatar.

CodePen Embed Fallback

See that? We’re going to make a scaling animation where the avatar seems to pop right out of the circle it’s in. Cool, right? Don’t look at the code and let’s build this animation together step-by-step.

The HTML: Just one element

If you haven’t checked the code of the demo and you are wondering how many divs this’ll take, then stop right there, because our markup is nothing but a single image element:

<img src="" alt="">

Yes, a single element! The challenging part of this exercise is using the smallest amount of code possible. If you have been following me for a while, you should be used to this. I try hard to find CSS solutions that can be achieved with the smallest, most maintainable code possible.

I wrote a series of articles here on CSS-Tricks where I explore different hover effects using the same HTML markup containing a single element. I go into detail on gradients, masking, clipping, outlines, and even layout techniques. I highly recommend checking those out because I will re-use many of the tricks in this post.

An image file that’s square with a transparent background will work best for what we’re doing. Here’s the one I’m using if you want start with that.

Designed by Cang

I’m hoping to see lots of examples of this as possible using real images — so please share your final result in the comments when you’re done so we can build a collection!

Before jumping into CSS, let’s first dissect the effect. The image gets bigger on hover, so we’ll for sure use transform: scale() in there. There’s a circle behind the avatar, and a radial gradient should do the trick. Finally, we need a way to create a border at the bottom of the circle that creates the appearance of the avatar behind the circle.

Let’s get to work!

The scale effect

Let’s start by adding the transform:

img { width: 280px; aspect-ratio: 1; cursor: pointer; transition: .5s; } img:hover { transform: scale(1.35); } CodePen Embed Fallback

Nothing complicated yet, right? Let’s move on.

The circle

We said that the background would be a radial gradient. That’s perfect because we can create hard stops between the colors of a radial gradient, which make it look like we’re drawing a circle with solid lines.

img { --b: 5px; /* border width */ width: 280px; aspect-ratio: 1; background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), #C02942 calc(100% - var(--b)) 99%, #0000 ); cursor: pointer; transition: .5s; } img:hover { transform: scale(1.35); }

Note the CSS variable, --b, I’m using there. It represents the thickness of the “border” which is really just being used to define the hard color stops for the red part of the radial gradient.

CodePen Embed Fallback

The next step is to play with the gradient size on hover. The circle needs to keep its size as the image grows. Since we are applying a scale() transformation, we actually need to decrease the size of the circle because it otherwise scales up with the avatar. So, while the image scales up, we need the gradient to scale down.

Let’s start by defining a CSS variable, --f, that defines the “scale factor”, and use it to set the size of the circle. I’m using 1 as the default value, as in that’s the initial scale for the image and the circle that we transform from.

Here is a demo to illustrate the trick. Hover to see what is happening behind the scenes:

CodePen Embed Fallback

I added a third color to the radial-gradient to better identify the area of the gradient on hover:

radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), #C02942 calc(100% - var(--b)) 99%, lightblue );

Now we have to position our background at the center of the circle and make sure it takes up the full height. I like to declare everything directly on the background shorthand property, so we can add our background positioning and make sure it doesn’t repeat by tacking on those values right after the radial-gradient():

background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;

The background is placed at the center (50%), has a width equal to calc(100%/var(--f)), and has a height equal to 100%.

Nothing scales when --f is equal to 1 — again, our initial scale. Meanwhile, the gradient takes up the full width of the container. When we increase --f, the element’s size grows — thanks to the scale() transform — and the gradient’s size decreases.

Here’s what we get when we apply all of this to our demo:

CodePen Embed Fallback

We’re getting closer! We have the overflow effect at the top, but we still need to hide the bottom part of the image, so it looks like it is popping out of the circle rather than sitting in front of it. That’s the tricky part of this whole thing and is what we’re going to do next.

The bottom border

I first tried tackling this with the border-bottom property, but I was unable to find a way to match the size of the border to the size to the circle. Here’s the best I could get and you can immediately see it’s wrong:

CodePen Embed Fallback

The actual solution is to use the outline property. Yes, outline, not border. In a previous article, I show how outline is powerful and allows us to create cool hover effects. Combined with outline-offset, we have exactly what we need for our effect.

The idea is to set an outline on the image and adjust its offset to create the bottom border. The offset will depend on the scaling factor the same way the gradient size did.

CodePen Embed Fallback

Now we have our bottom “border” (actually an outline) combined with the “border” created by the gradient to create a full circle. We still need to hide portions of the outline (from the top and the sides), which we’ll get to in a moment.

Here’s our code so far, including a couple more CSS variables you can use to configure the image size (--s) and the “border” color (--c):

img { --s: 280px; /* image size */ --b: 5px; /* border thickness */ --c: #C02942; /* border color */ --f: 1; /* initial scale */ width: var(--s); aspect-ratio: 1; cursor: pointer; border-radius: 0 0 999px 999px; outline: var(--b) solid var(--c); outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b)); background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), var(--c) calc(100% - var(--b)) 99%, #0000 ) 50% / calc(100% / var(--f)) 100% no-repeat; transform: scale(var(--f)); transition: .5s; } img:hover { --f: 1.35; /* hover scale */ }

Since we need a circular bottom border, we added a border-radius on the bottom side, allowing the outline to match the curvature of the gradient.

The calculation used on outline-offset is a lot more straightforward than it looks. By default, outline is drawn outside of the element’s box. And in our case, we need it to overlap the element. More precisely, we need it to follow the circle created by the gradient.

When we scale the element, we see the space between the circle and the edge. Let’s not forget that the idea is to keep the circle at the same size after the scale transformation runs, which leaves us with the space we will use to define the outline’s offset as illustrated in the above figure.

Let’s not forget that the second element is scaled, so our result is also scaled… which means we need to divide the result by f to get the real offset value:

Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2

We add a negative sign since we need the outline to go from the outside to the inside:

Offset = (1/f - 1) * S/2

Here’s a quick demo that shows how the outline follows the gradient:

CodePen Embed Fallback

You may already see it, but we still need the bottom outline to overlap the circle rather than letting it bleed through it. We can do that by removing the border’s size from the offset:

outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2) - var(--b)); CodePen Embed Fallback

Now we need to find how to remove the top part from the outline. In other words, we only want the bottom part of the image’s outline.

First, let’s add space at the top with padding to help avoid the overlap at the top:

img { --s: 280px; /* image size */ --b: 5px; /* border thickness */ --c: #C02942; /* border color */ --f: 1; /* initial scale */ width: var(--s); aspect-ratio: 1; padding-block-start: calc(var(--s)/5); /* etc. */ } img:hover { --f: 1.35; /* hover scale */ } CodePen Embed Fallback

There is no particular logic to that top padding. The idea is to ensure the outline doesn’t touch the avatar’s head. I used the element’s size to define that space to always have the same proportion.

Note that I have added the content-box value to the background:

background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), var(--c) calc(100% - var(--b)) 99%, #0000 ) 50%/calc(100%/var(--f)) 100% no-repeat content-box;

We need this because we added padding and we only want the background set to the content box, so we must explicitly tell the background to stop there.

Adding CSS mask to the mix

We reached the last part! All we need to do is to hide some pieces, and we are done. For this, we will rely on the mask property and, of course, gradients.

Here is a figure to illustrate what we need to hide or what we need to show to be more accurate

The left image is what we currently have, and the right is what we want. The green part illustrates the mask we must apply to the original image to get the final result.

We can identify two parts of our mask:

  • A circular part at the bottom that has the same dimension and curvature as the radial gradient we used to create the circle behind the avatar
  • A rectangle at the top that covers the area inside the outline. Notice how the outline is outside the green area at the top — that’s the most important part, as it allows the outline to be cut so that only the bottom part is visible.

Here’s our final CSS:

img { --s: 280px; /* image size */ --b: 5px; /* border thickness */ --c: #C02942; /* border color */ --f: 1; /* initial scale */ --_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box; --_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b)); width: var(--s); aspect-ratio: 1; padding-top: calc(var(--s)/5); cursor: pointer; border-radius: 0 0 999px 999px; outline: var(--b) solid var(--c); outline-offset: var(--_o); background: radial-gradient( circle closest-side, #ECD078 calc(99% - var(--b)), var(--c) calc(100% - var(--b)) 99%, #0000) var(--_g); mask: linear-gradient(#000 0 0) no-repeat 50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%, radial-gradient( circle closest-side, #000 99%, #0000) var(--_g); transform: scale(var(--f)); transition: .5s; } img:hover { --f: 1.35; /* hover scale */ }

Let’s break down that mask property. For starters, notice that a similar radial-gradient() from the background property is in there. I created a new variable, --_g, for the common parts to make things less cluttered.

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box; mask: radial-gradient( circle closest-side, #000 99%, #0000) var(--_g);

Next, there’s a linear-gradient() in there as well:

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box; mask: linear-gradient(#000 0 0) no-repeat 50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%, radial-gradient( circle closest-side, #000 99%, #0000) var(--_g);

This creates the rectangle part of the mask. Its width is equal to the radial gradient’s width minus twice the border thickness:

calc(100% / var(--f) - 2 * var(--b))

The rectangle’s height is equal to half, 50%, of the element’s size.

We also need the linear gradient placed at the horizontal center (50%) and offset from the top by the same value as the outline’s offset. I created another CSS variable, --_o, for the offset we previously defined:

--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

One of the confusing things here is that we need a negative offset for the outline (to move it from outside to inside) but a positive offset for the gradient (to move from top to bottom). So, if you’re wondering why we multiply the offset, --_o, by -1, well, now you know!

Here is a demo to illustrate the mask’s gradient configuration:

CodePen Embed Fallback

Hover the above and see how everything move together. The middle box illustrates the mask layer composed of two gradients. Imagine it as the visible part of the left image, and you get the final result on the right!

Wrapping up

Oof, we’re done! And not only did we wind up with a slick hover animation, but we did it all with a single HTML <img> element. Just that and less than 20 lines of CSS trickery!

Sure, we relied on some little tricks and math formulas to reach such a complex effect. But we knew exactly what to do since we identified the pieces we needed up-front.

Could we have simplified the CSS if we allowed ourselves more HTML? Absolutely. But we’re here to learn new CSS tricks! This was a good exercise to explore CSS gradients, masking, the outline property’s behavior, transformations, and a whole bunch more. If you felt lost at any point, then definitely check out my series that uses the same general concepts. It sometimes helps to see more examples and use cases to drive a point home.

I will leave you with one last demo that uses photos of popular CSS developers. Don’t forget to show me a demo with your own image so I can add it to the collection!

CodePen Embed Fallback

A Fancy Hover Effect For Your Avatar originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Caching Data in SvelteKit

Wed, 02/01/2023 - 5:04am

My previous post was a broad overview of SvelteKit where we saw what a great tool it is for web development. This post will fork off what we did there and dive into every developer’s favorite topic: caching. So, be sure to give my last post a read if you haven’t already. The code for this post is available on GitHub, as well as a live demo.

This post is all about data handling. We’ll add some rudimentary search functionality that will modify the page’s query string (using built-in SvelteKit features), and re-trigger the page’s loader. But, rather than just re-query our (imaginary) database, we’ll add some caching so re-searching prior searches (or using the back button) will show previously retrieved data, quickly, from cache. We’ll look at how to control the length of time the cached data stays valid and, more importantly, how to manually invalidate all cached values. As icing on the cake, we’ll look at how we can manually update the data on the current screen, client-side, after a mutation, while still purging the cache.

This will be a longer, more difficult post than most of what I usually write since we’re covering harder topics. This post will essentially show you how to implement common features of popular data utilities like react-query; but instead of pulling in an external library, we’ll only be using the web platform and SvelteKit features.

Unfortunately, the web platform’s features are a bit lower level, so we’ll be doing a bit more work than you might be used to. The upside is we won’t need any external libraries, which will help keep bundle sizes nice and small. Please don’t use the approaches I’m going to show you unless you have a good reason to. Caching is easy to get wrong, and as you’ll see, there’s a bit of complexity that’ll result in your application code. Hopefully your data store is fast, and your UI is fine allowing SvelteKit to just always request the data it needs for any given page. If it is, leave it alone. Enjoy the simplicity. But this post will show you some tricks for when that stops being the case.

Speaking of react-query, it was just released for Svelte! So if you find yourself leaning on manual caching techniques a lot, be sure to check that project out, and see if it might help.

Setting up

Before we start, let’s make a few small changes to the code we had before. This will give us an excuse to see some other SvelteKit features and, more importantly, set us up for success.

First, let’s move our data loading from our loader in +page.server.js to an API route. We’ll create a +server.js file in routes/api/todos, and then add a GET function. This means we’ll now be able to fetch (using the default GET verb) to the /api/todos path. We’ll add the same data loading code as before.

import { json } from "@sveltejs/kit"; import { getTodos } from "$lib/data/todoData"; export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; const todos = await getTodos(search); return json(todos); }

Next, let’s take the page loader we had, and simply rename the file from +page.server.js to +page.js (or .ts if you’ve scaffolded your project to use TypeScript). This changes our loader to be a “universal” loader rather than a server loader. The SvelteKit docs explain the difference, but a universal loader runs on both the server and also the client. One advantage for us is that the fetch call into our new endpoint will run right from our browser (after the initial load), using the browser’s native fetch function. We’ll add standard HTTP caching in a bit, but for now, all we’ll do is call the endpoint.

export async function load({ fetch, url, setHeaders }) { const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}`); const todos = await resp.json(); return { todos, }; }

Now let’s add a simple form to our /list page:

<div class="search-form"> <form action="/list"> <label>Search</label> <input autofocus name="search" /> </form> </div>

Yep, forms can target directly to our normal page loaders. Now we can add a search term in the search box, hit Enter, and a “search” term will be appended to the URL’s query string, which will re-run our loader and search our to-do items.

Let’s also increase the delay in our todoData.js file in /lib/data. This will make it easy to see when data are and are not cached as we work through this post.

export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 500));

Remember, the full code for this post is all on GitHub, if you need to reference it.

Basic caching

Let’s get started by adding some caching to our /api/todos endpoint. We’ll go back to our +server.js file and add our first cache-control header.

setHeaders({ "cache-control": "max-age=60", });

…which will leave the whole function looking like this:

export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; setHeaders({ "cache-control": "max-age=60", }); const todos = await getTodos(search); return json(todos); }

We’ll look at manual invalidation shortly, but all this function says is to cache these API calls for 60 seconds. Set this to whatever you want, and depending on your use case, stale-while-revalidate might also be worth looking into.

And just like that, our queries are caching.

Note make sure you un-check the checkbox that disables caching in dev tools.

Remember, if your initial navigation on the app is the list page, those search results will be cached internally to SvelteKit, so don’t expect to see anything in DevTools when returning to that search.

What is cached, and where

Our very first, server-rendered load of our app (assuming we start at the /list page) will be fetched on the server. SvelteKit will serialize and send this data down to our client. What’s more, it will observe the Cache-Control header on the response, and will know to use this cached data for that endpoint call within the cache window (which we set to 60 seconds in put example).

After that initial load, when you start searching on the page, you should see network requests from your browser to the /api/todos list. As you search for things you’ve already searched for (within the last 60 seconds), the responses should load immediately since they’re cached.

What’s especially cool with this approach is that, since this is caching via the browser’s native caching, these calls could (depending on how you manage the cache busting we’ll be looking at) continue to cache even if you reload the page (unlike the initial server-side load, which always calls the endpoint fresh, even if it did it within the last 60 seconds).

Obviously data can change anytime, so we need a way to purge this cache manually, which we’ll look at next.

Cache invalidation

Right now, data will be cached for 60 seconds. No matter what, after a minute, fresh data will be pulled from our datastore. You might want a shorter or longer time period, but what happens if you mutate some data and want to clear your cache immediately so your next query will be up to date? We’ll solve this by adding a query-busting value to the URL we send to our new /todos endpoint.

Let’s store this cache busting value in a cookie. That value can be set on the server but still read on the client. Let’s look at some sample code.

We can create a +layout.server.js file at the very root of our routes folder. This will run on application startup, and is a perfect place to set an initial cookie value.

export function load({ cookies, isDataRequest }) { const initialRequest = !isDataRequest; const cacheValue = initialRequest ? +new Date() : cookies.get("todos-cache"); if (initialRequest) { cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false }); } return { todosCacheBust: cacheValue, }; }

You may have noticed the isDataRequest value. Remember, layouts will re-run anytime client code calls invalidate(), or anytime we run a server action (assuming we don’t turn off default behavior). isDataRequest indicates those re-runs, and so we only set the cookie if that’s false; otherwise, we send along what’s already there.

The httpOnly: false flag is also significant. This allows our client code to read these cookie values in document.cookie. This would normally be a security concern, but in our case these are meaningless numbers that allow us to cache or cache bust.

Reading cache values

Our universal loader is what calls our /todos endpoint. This runs on the server or the client, and we need to read that cache value we just set up no matter where we are. It’s relatively easy if we’re on the server: we can call await parent() to get the data from parent layouts. But on the client, we’ll need to use some gross code to parse document.cookie:

export function getCookieLookup() { if (typeof document !== "object") { return {}; } return document.cookie.split("; ").reduce((lookup, v) => { const parts = v.split("="); lookup[parts[0]] = parts[1]; return lookup; }, {}); } const getCurrentCookieValue = name => { const cookies = getCookieLookup(); return cookies[name] ?? ""; };

Fortunately, we only need it once.

Sending out the cache value

But now we need to send this value to our /todos endpoint.

import { getCurrentCookieValue } from "$lib/util/cookieUtils"; export async function load({ fetch, parent, url, setHeaders }) { const parentData = await parent(); const cacheBust = getCurrentCookieValue("todos-cache") || parentData.todosCacheBust; const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}&cache=${cacheBust}`); const todos = await resp.json(); return { todos, }; }

getCurrentCookieValue('todos-cache') has a check in it to see if we’re on the client (by checking the type of document), and returns nothing if we are, at which point we know we’re on the server. Then it uses the value from our layout.

Busting the cache

But how do we actually update that cache busting value when we need to? Since it’s stored in a cookie, we can call it like this from any server action:

cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false }); The implementation

It’s all downhill from here; we’ve done the hard work. We’ve covered the various web platform primitives we need, as well as where they go. Now let’s have some fun and write application code to tie it all together.

For reasons that’ll become clear in a bit, let’s start by adding an editing functionality to our /list page. We’ll add this second table row for each todo:

import { enhance } from "$app/forms"; <tr> <td colspan="4"> <form use:enhance method="post" action="?/editTodo"> <input name="id" value="{t.id}" type="hidden" /> <input name="title" value="{t.title}" /> <button>Save</button> </form> </td> </tr>

And, of course, we’ll need to add a form action for our /list page. Actions can only go in .server pages, so we’ll add a +page.server.js in our /list folder. (Yes, a +page.server.js file can co-exist next to a +page.js file.)

import { getTodo, updateTodo, wait } from "$lib/data/todoData"; export const actions = { async editTodo({ request, cookies }) { const formData = await request.formData(); const id = formData.get("id"); const newTitle = formData.get("title"); await wait(250); updateTodo(id, newTitle); cookies.set("todos-cache", +new Date(), { path: "/", httpOnly: false }); }, };

We’re grabbing the form data, forcing a delay, updating our todo, and then, most importantly, clearing our cache bust cookie.

Let’s give this a shot. Reload your page, then edit one of the to-do items. You should see the table value update after a moment. If you look in the Network tab in DevToold, you’ll see a fetch to the /todos endpoint, which returns your new data. Simple, and works by default.

Immediate updates

What if we want to avoid that fetch that happens after we update our to-do item, and instead, update the modified item right on the screen?

This isn’t just a matter of performance. If you search for “post” and then remove the word “post” from any of the to-do items in the list, they’ll vanish from the list after the edit since they’re no longer in that page’s search results. You could make the UX better with some tasteful animation for the exiting to-do, but let’s say we wanted to not re-run that page’s load function but still clear the cache and update the modified to-do so the user can see the edit. SvelteKit makes that possible — let’s see how!

First, let’s make one little change to our loader. Instead of returning our to-do items, let’s return a writeable store containing our to-dos.

return { todos: writable(todos), };

Before, we were accessing our to-dos on the data prop, which we do not own and cannot update. But Svelte lets us return our data in their own store (assuming we’re using a universal loader, which we are). We just need to make one more tweak to our /list page.

Instead of this:

{#each todos as t}

…we need to do this since todos is itself now a store.:

{#each $todos as t}

Now our data loads as before. But since todos is a writeable store, we can update it.

First, let’s provide a function to our use:enhance attribute:

<form use:enhance={executeSave} on:submit={runInvalidate} method="post" action="?/editTodo" >

This will run before a submit. Let’s write that next:

function executeSave({ data }) { const id = data.get("id"); const title = data.get("title"); return async () => { todos.update(list => list.map(todo => { if (todo.id == id) { return Object.assign({}, todo, { title }); } else { return todo; } }) ); }; }

This function provides a data object with our form data. We return an async function that will run after our edit is done. The docs explain all of this, but by doing this, we shut off SvelteKit’s default form handling that would have re-run our loader. This is exactly what we want! (We could easily get that default behavior back, as the docs explain.)

We now call update on our todos array since it’s a store. And that’s that. After editing a to-do item, our changes show up immediately and our cache is cleared (as before, since we set a new cookie value in our editTodo form action). So, if we search and then navigate back to this page, we’ll get fresh data from our loader, which will correctly exclude any updated to-do items that were updated.

The code for the immediate updates is available at GitHub.

Digging deeper

We can set cookies in any server load function (or server action), not just the root layout. So, if some data are only used underneath a single layout, or even a single page, you could set that cookie value there. Moreoever, if you’re not doing the trick I just showed manually updating on-screen data, and instead want your loader to re-run after a mutation, then you could always set a new cookie value right in that load function without any check against isDataRequest. It’ll set initially, and then anytime you run a server action that page layout will automatically invalidate and re-call your loader, re-setting the cache bust string before your universal loader is called.

Writing a reload function

Let’s wrap-up by building one last feature: a reload button. Let’s give users a button that will clear cache and then reload the current query.

We’ll add a dirt simple form action:

async reloadTodos({ cookies }) { cookies.set('todos-cache', +new Date(), { path: '/', httpOnly: false }); },

In a real project you probably wouldn’t copy/paste the same code to set the same cookie in the same way in multiple places, but for this post we’ll optimize for simplicity and readability.

Now let’s create a form to post to it:

<form method="POST" action="?/reloadTodos" use:enhance> <button>Reload todos</button> </form>

That works!

We could call this done and move on, but let’s improve this solution a bit. Specifically, let’s provide feedback on the page to tell the user the reload is happening. Also, by default, SvelteKit actions invalidate everything. Every layout, page, etc. in the current page’s hierarchy would reload. There might be some data that’s loaded once in the root layout that we don’t need to invalidate or re-load.

So, let’s focus things a bit, and only reload our to-dos when we call this function.

First, let’s pass a function to enhance:

<form method="POST" action="?/reloadTodos" use:enhance={reloadTodos}> import { enhance } from "$app/forms"; import { invalidate } from "$app/navigation"; let reloading = false; const reloadTodos = () => { reloading = true; return async () => { invalidate("reload:todos").then(() => { reloading = false; }); }; };

We’re setting a new reloading variable to true at the start of this action. And then, in order to override the default behavior of invalidating everything, we return an async function. This function will run when our server action is finished (which just sets a new cookie).

Without this async function returned, SvelteKit would invalidate everything. Since we’re providing this function, it will invalidate nothing, so it’s up to us to tell it what to reload. We do this with the invalidate function. We call it with a value of reload:todos. This function returns a promise, which resolves when the invalidation is complete, at which point we set reloading back to false.

Lastly, we need to sync our loader up with this new reload:todos invalidation value. We do that in our loader with the depends function:

export async function load({ fetch, url, setHeaders, depends }) { depends('reload:todos'); // rest is the same

And that’s that. depends and invalidate are incredibly useful functions. What’s cool is that invalidate doesn’t just take arbitrary values we provide like we did. We can also provide a URL, which SvelteKit will track, and invalidate any loaders that depend on that URL. To that end, if you’re wondering whether we could skip the call to depends and invalidate our /api/todos endpoint altogether, you can, but you have to provide the exact URL, including the search term (and our cache value). So, you could either put together the URL for the current search, or match on the path name, like this:

invalidate(url => url.pathname == "/api/todos");

Personally, I find the solution that uses depends more explicit and simple. But see the docs for more info, of course, and decide for yourself.

If you’d like to see the reload button in action, the code for it is in this branch of the repo.

Parting thoughts

This was a long post, but hopefully not overwhelming. We dove into various ways we can cache data when using SvelteKit. Much of this was just a matter of using web platform primitives to add the correct cache, and cookie values, knowledge of which will serve you in web development in general, beyond just SvelteKit.

Moreover, this is something you absolutely do not need all the time. Arguably, you should only reach for these sort of advanced features when you actually need them. If your datastore is serving up data quickly and efficiently, and you’re not dealing with any kind of scaling problems, there’s no sense in bloating your application code with needless complexity doing the things we talked about here.

As always, write clear, clean, simple code, and optimize when necessary. The purpose of this post was to provide you those optimization tools for when you truly need them. I hope you enjoyed it!

Caching Data in SvelteKit originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

AR, VR, and a Model for 3D in HTML

Fri, 01/27/2023 - 4:15am

Tucked down somewhere in the Safari Technology Preview 161 release notes is a seemingly innocous line about support for a new HTML element and attribute:

Added support for <model src> and honor <source type> attributes (257518@main)

Anytime I see mention of some element I don’t recognize, my mind goes straight to Huh! New to me, but probably old news for everyone else. It’s poor posture, I know, as it could just as easily be:

  • Hmm, looks like some propriatary experiment.
  • Wow, a truly new thing!

Truth is, it’s sorta all three.

It’s an evolving concept

As in, the first somewhat official-sounding thing I found on <model> wasn’t in the W3C spec but in WebKit’s repo for explainers. All that’s in the README is a giant note from 2021 that “The <model> element has moved to the Immersive Web CG.” I was about to hop over but my eye caught the HistoryAndEvolution.md file which has a nice rundown of early context on the <model> concept:

The <model> element was born out of a desire to take the next step and improve the experience of Safari’s integration with iOS’s AR Quick Look feature.

I had to look at Apple’s splash page for AR Quick Look. You know the new feature that some stores have where you can transpose a 3D rendering of a product in your own home using your phone camera? That’s the sort of stuff we’re talking about, and Apple links up a nice case study from the Metropolitan Museum of Art.

As I understand it from this limited context:

  • Drop a <model> element in the document.
  • Add an external source file, e.g. <model src="assets/example.usdz">.
The original proposal is from the Immersive Web Committee Group

That’s the team looking make Virtual Reality (VR) and Augmented Reality (AR) part of the web. Apple linked up their repo, so I made the jump and went straight to the explainer. This isn’t the spec or anything, but the original proposal. A much better definition of the element!

HTML allows the display of many media types through elements such as <img>, <picture>, or <video>, but it does not provide a declarative manner to directly display 3D content. Embedding 3D content within a page is comparatively cumbersome and relies on scripting the <canvas> element. We believe it is time to put 3D models on equal footing with other, already supported, media types.


The HTML <model> element aims to allow a website to embed interactive 3D models as conveniently as any other visual media. Models are expected to be created by 3D authoring tools or generated dynamically, but served as a standalone resource by the server.

The basic example pulls this together. It really does feel like the <video> or <picture> elements:

<model style="width: 400px; height: 300px"> <source src="assets/example.usdz" type="model/vnd.usdz+zip"> <source src="assets/example.glb" type="model/gltf-binary"> </model>

.usdz? .glb? Not the type of files that typically cross my desk. Guess I’ll need to brush up on those and any other file types that <model> might support. Again, all of this is merely the original proposal.

The draft proposal isn’t stubbed out quite yet

But it does provide a nice outline of where things could possibly go:

  • Adding a model to a document
  • Enabling interactivity
  • Supporting multiple formats
  • Providing fallback content
  • Making it accessible

There’s a lot to figure out. Most of what’s there are documented issues that need addressing. It does, however, shed more light on <model> like proposed attributes that make it feel even more like <video> such as autoplay, controls, loop, muted, poster, etc.

It goes back even further

The very earliest mention of 3D modeling I found was Keith Clark’s 2018 post in which he prototypes a custom element called <x-model>. He describes it as “a placeholder that provides access to the DOM and CSSOM” where the loading and rendering is done in three.js.

Keith’s idea is followed by the <model-viewer> component Joe Medley shared in 2020 (and a subsequent update to it). There’s even a homepage for it and it’s fun to drag Neil Armstrong around in space.

It’s possibly just an experiment?

I mean, the draft spec hasn’t been fleshed out. Apple seems willing to play ball thanks to the Safari TP 161 announcement. That makes total sense given how bullish Apple is on AR as a whole. (Apple Glasses, anyone?)

Google seems to have its foot in the door, albeit on the Web Components side of things. It’s easy to see how there may be a conflict of interest between what Apple and Google want from AR on the web.

These are all just my notes from trying to grok everything. There’s gotta be a lot more nuance to it than what little I know about it so far. I’m sure someone smarter can tie neater bow around <model> in the comments. &#x1f609;

And while we’re talking Safari Technology Preview, 162 just released the other day and it enables CSS nesting and the CSS relative color syntax.

AR, VR, and a Model for 3D in HTML originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Animating CSS Grid (How To + Examples)

Wed, 01/25/2023 - 6:05am

I’m pleased to shine a light on the fact that the CSS grid-template-rows and grid-template-columns properties are now animatable in all major web browsers! Well, CSS Grid has technically supported animations for a long time, as it’s baked right into the CSS Grid Layout Module Level 1 spec.

But animating these grid properties only recently gained supported by all three major browsers. Shall we take a look at a few examples to get the creative juices flowing?

Table of contents Example 1: Expanding sidebar

First of all, this is what we’re talking about:

CodePen Embed Fallback

A simple two-column grid. Now, before, you might not have built this using CSS Grid because animations and transitions weren’t supported, but what if you wanted the left column — perhaps a sidebar navigation — to expand on hover? Well, now that’s possible.

I know what you’re thinking: “Animating a CSS property? Easy peasy, I’ve been doing it for years!” Me too. However, I ran into an interesting snag while experimenting with a particular use case.

So, we want to transition the grid itself (specifically grid-template-columns, which is set on the .grid class in the example). But the left column (.left) is the selector that requires the :hover pseudo-class. While JavaScript can solve this conundrum easily — thanks, but no thanks — we can accomplish it with CSS alone.

Let’s walk through the whole thing, starting with the HTML. Pretty standard stuff really… a grid with two columns.

<div class="grid"> <div class="left"></div> <div class="right"></div> </div>

Putting the cosmetic CSS aside, you’ll first need to set display: grid on the parent container (.grid).

.grid { display: grid; }

Next, we can define and size the two columns using the grid-template-columns property. We’ll make the left column super narrow, and later increase its width on hover. The right column takes up the rest of the remaining space, thanks to the auto keyword.

.grid { display: grid; grid-template-columns: 48px auto; }

We know we’re going to animate this thing, so let’s go ahead and throw a transition in there while we’re at it so the change between states is smooth and noticeable.

.grid { display: grid; grid-template-columns: 48px auto; transition: 300ms; /* Change as needed */ }

That’s it for the .grid! All that’s left is to apply the hover state. Specifically, we’re going to override the grid-template-columns property so that the left column takes up a greater amount of space on hover.

This alone isn’t all that interesting, although it’s awesome that animations and transitions are supported now in CSS Grid. What’s more interesting is that we can use the relatively new :has() pseudo-class to style the parent container (.grid) while the child (.left) is hovered.

.grid:has(.left:hover) { /* Hover styles */ }

In plain English this is saying, “Do something to the .grid container if it contains an element named .left inside of it that is in a hover state.” That’s why :has() is often referred to as a “parent” selector. We can finally select a parent based on the children it contains — no JavaScript required!

So, let’s increase the width of the .left column to 30% when it is hovered. The .right column will continue to take up all the leftover space:

.grid { display: grid; transition: 300ms; grid-template-columns: 48px auto; } .grid:has(.left:hover) { grid-template-columns: 30% auto; }

We could use CSS variables as well, which may or may not look cleaner depending on your personal preferences (or you might be using CSS variables in your project anyway):

.grid { display: grid; transition: 300ms; grid-template-columns: var(--left, 48px) auto; } .grid:has(.left:hover) { --left: 30%; }

I love that CSS grids can be animated now, but the fact that we can build this particular example with just nine lines of CSS is even more astounding.

Here’s another example by Olivia Ng — similar concept, but with content (click on the nav icon):

CodePen Embed Fallback Example 2: Expanding Panels CodePen Embed Fallback

This example transitions the grid container (the column widths) but also the individual columns (their background colors). It’s ideal for providing more content on hover.

It’s worth remembering that the repeat() function sometimes produces buggy transitions, which is why I set the width of each column individually (i.e. grid-template-columns: 1fr 1fr 1fr).

Example 3: Adding Rows and Columns CodePen Embed Fallback

This example animatedly “adds” a column to the grid. However — you guessed it — this scenario has a pitfall too. The requirement is that the “new” column mustn’t be hidden (i.e. set to display: none), and CSS Grid must acknowledge its existence while setting its width to 0fr.

So, for a three-column grid — grid-template-columns: 1fr 1fr 0fr (yes, the unit must be declared even though the value is 0!) transitions into grid-template-columns: 1fr 1fr 1fr correctly, but grid-template-columns: 1fr 1fr doesn’t. In hindsight, this actually makes perfect sense considering what we know about how transitions work.

Here’s another example by Michelle Barker — same concept, but with an extra column and lot more pizzazz. Make sure to run this one in full-screen mode because it’s actually responsive (no trickery, just good design!).

CodePen Embed Fallback A few more examples

Because why not?

This “Animated Mondrian” is the original proof of concept for animated CSS grids by Chrome DevRel. The grid-row‘s and grid-column‘s utilize the span keyword to create the layout you see before you, and then the grid-template-row’s and grid-template-column‘s are animated using a CSS animation. It’s nowhere near as complex as it looks!

CodePen Embed Fallback

Same concept, but with more of that Michelle Barker pizzazz. Could make a nice loading spinner?

CodePen Embed Fallback

Wrapping up with a bit of nostalgia (showing my age here), the not-very-griddy animated CSS grid by Andrew Harvard. Again — same concept — it’s just that you can’t see the other grid items. But don’t worry, they’re there.

CodePen Embed Fallback

Animating CSS Grid (How To + Examples) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Getting Started With SvelteKit

Mon, 01/23/2023 - 3:48am

SvelteKit is the latest of what I’d call next-gen application frameworks. It, of course, scaffolds an application for you, with the file-based routing, deployment, and server-side rendering that Next has done forever. But SvelteKit also supports nested layouts, server mutations that sync up the data on your page, and some other niceties we’ll get into.

This post is meant to be a high-level introduction to hopefully build some excitement for anyone who’s never used SvelteKit. It’ll be a relaxed tour. If you like what you see, the full docs are here.

In some ways this is a challenging post to write. SvelteKit is an application framework. It exists to help you build… well, applications. That makes it hard to demo. It’s not feasible to build an entire application in a blog post. So instead, we’ll use our imaginations a bit. We’ll build the skeleton of an application, have some empty UI placeholders, and hard-coded static data. The goal isn’t to build an actual application, but instead to show you how SvelteKit’s moving pieces work so you can build an application of your own.

To that end, we’ll build the tried and true To-Do application as an example. But don’t worry, this will be much, much more about seeing how SvelteKit works than creating yet another To-Do app.

The code for everything in this post is available at GitHub. This project is also deployed on Vercel for a live demo.

Creating your project

Spinning up a new SvelteKit project is simple enough. Run npm create svelte@latest your-app-name in the terminal and answer the question prompts. Be sure to pick “Skeleton Project” but otherwise make whatever selections you want for TypeScript, ESLint, etc.

Once the project is created, run npm i and npm run dev and a dev server should start running. Fire up localhost:5173 in the browser and you’ll get the placeholder page for the skeleton app.

Basic routing

Notice the routes folder under src. That holds code for all of our routes. There’s already a +page.svelte file in there with content for the root / route. No matter where in the file hierarchy you are, the actual page for that path always has the name +page.svelte. With that in mind, let’s create pages for /list, /details, /admin/user-settings and admin/paid-status, and also add some text placeholders for each page.

Your file layout should look something like this:

You should be able to navigate around by changing URL paths in the browser address bar.


We’ll want navigation links in our app, but we certainly don’t want to copy the markup for them on each page we create. So, let’s create a +layout.svelte file in the root of our routes folder, which SvelteKit will treat as a global template for all pages. Let’s and add some content to it:

<nav> <ul> <li> <a href="/">Home</a> </li> <li> <a href="/list">To-Do list</a> </li> <li> <a href="/admin/paid-status">Account status</a> </li> <li> <a href="/admin/user-settings">User settings</a> </li> </ul> </nav> <slot /> <style> nav { background-color: beige; } nav ul { display: flex; } li { list-style: none; margin: 15px; } a { text-decoration: none; color: black; } </style>

Some rudimentary navigation with some basic styles. Of particular importance is the <slot /> tag. This is not the slot you use with web components and shadow DOM, but rather a Svelte feature indicating where to put our content. When a page renders, the page content will slide in where the slot is.

And now we have some navigation! We won’t win any design competitions, but we’re not trying to.

Nested layouts

What if we wanted all our admin pages to inherit the normal layout we just built but also share some things common to all admin pages (but only admin pages)? No problem, we add another +layout.svelte file in our root admin directory, which will be inherited by everything underneath it. Let’s do that and add this content:

<div>This is an admin page</div> <slot /> <style> div { padding: 15px; margin: 10px 0; background-color: red; color: white; } </style>

We add a red banner indicating this is an admin page and then, like before, a <slot /> denoting where we want our page content to go.

Our root layout from before renders. Inside of the root layout is a <slot /> tag. The nested layout’s content goes into the root layout’s <slot />. And finally, the nested layout defines its own <slot />, into which the page content renders.

If you navigate to the admin pages, you should see the new red banner:

Defining our data

OK, let’s render some actual data — or at least, see how we can render some actual data. There’s a hundred ways to create and connect to a database. This post is about SvelteKit though, not managing DynamoDB, so we’ll “load” some static data instead. But, we’ll use all the same machinery to read and update it that you’d use for real data. For a real web app, swap out the functions returning static data with functions connecting and querying to whatever database you happen to use.

Let’s create a dirt-simple module in lib/data/todoData.ts that returns some static data along with artificial delays to simulate real queries. You’ll see this lib folder imported elsewhere via $lib. This is a SvelteKit feature for that particular folder, and you can even add your own aliases.

let todos = [ { id: 1, title: "Write SvelteKit intro blog post", assigned: "Adam", tags: [1] }, { id: 2, title: "Write SvelteKit advanced data loading blog post", assigned: "Adam", tags: [1] }, { id: 3, title: "Prepare RenderATL talk", assigned: "Adam", tags: [2] }, { id: 4, title: "Fix all SvelteKit bugs", assigned: "Rich", tags: [3] }, { id: 5, title: "Edit Adam's blog posts", assigned: "Geoff", tags: [4] }, ]; let tags = [ { id: 1, name: "SvelteKit Content", color: "ded" }, { id: 2, name: "Conferences", color: "purple" }, { id: 3, name: "SvelteKit Development", color: "pink" }, { id: 4, name: "CSS-Tricks Admin", color: "blue" }, ]; export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 100)); export async function getTodos() { await wait(); return todos; } export async function getTags() { await wait(); return tags.reduce((lookup, tag) => { lookup[tag.id] = tag; return lookup; }, {}); } export async function getTodo(id) { return todos.find(t => t.id == id); }

A function to return a flat array of our to-do items, a lookup of our tags, and a function to fetch a single to-do (we’ll use that last one in our Details page).

Loading our data

How do we get that data into our Svelte pages? There’s a number of ways, but for now, let’s create a +page.server.js file in our list folder, and put this content in it:

import { getTodos, getTags } from "$lib/data/todoData"; export function load() { const todos = getTodos(); const tags = getTags(); return { todos, tags, }; }

We’ve defined a load() function that pulls in the data needed for the page. Notice that we are not await-ing calls to our getTodos and getTags async functions. Doing so would create a data loading waterfall as we wait for our to-do items to come in before loading our tags. Instead, we return the raw promises from load, and SvelteKit does the necessary work to await them.

So, how do we access this data from our page component? SvelteKit provides a data prop for our component with data on it. We’ll access our to-do items and tags from it using a reactive assignment.

Our List page component now looks like this.

<script> export let data; $: ({ todo, tags } = data); </script> <table cellspacing="10" cellpadding="10"> <thead> <tr> <th>Task</th> <th>Tags</th> <th>Assigned</th> </tr> </thead> <tbody> {#each todos as t} <tr> <td>{t.title}</td> <td>{t.tags.map((id) => tags[id].name).join(', ')}</td> <td>{t.assigned}</td> </tr> {/each} </tbody> </table> <style> th { text-align: left; } </style>

And this should render our to-do items!

Layout groups

Before we move on to the Details page and mutate data, let’s take a peek at a really neat SvelteKit feature: layout groups. We’ve already seen nested layouts for all admin pages, but what if we wanted to share a layout between arbitrary pages at the same level of our file system? In particular, what if we wanted to share a layout between only our List page and our Details page? We already have a global layout at that level. Instead, we can create a new directory, but with a name that’s in parenthesis, like this:

We now have a layout group that covers our List and Details pages. I named it (todo-management) but you can name it anything you like. To be clear, this name will not affect the URLs of the pages inside of the layout group. The URLs will remain the same; layout groups allow you to add shared layouts to pages without them all comprising the entirety of a directory in routes.

We could add a +layout.svelte file and some silly <div> banner saying, “Hey we’re managing to-dos”. But let’s do something more interesting. Layouts can define load() functions in order to provide data for all routes underneath them. Let’s use this functionality to load our tags — since we’ll be using our tags in our details page — in addition to the list page we already have.

In reality, forcing a layout group just to provide a single piece of data is almost certainly not worth it; it’s better to duplicate that data in the load() function for each page. But for this post, it’ll provide the excuse we need to see a new SvelteKit feature!

First, let’s go into our list page’s +page.server.js file and remove the tags from it.

import { getTodos, getTags } from "$lib/data/todoData"; export function load() { const todos = getTodos(); return { todos, }; }

Our List page should now produce an error since there is no tags object. Let’s fix this by adding a +layout.server.js file in our layout group, then define a load() function that loads our tags.

import { getTags } from "$lib/data/todoData"; export function load() { const tags = getTags(); return { tags, }; }

And, just like that, our List page is rendering again!

We’re loading data from multiple locations

Let’s put a fine point on what’s happening here:

  • We defined a load() function for our layout group, which we put in +layout.server.js.
  • This provides data for all of the pages the layout serves — which in this case means our List and Details pages.
  • Our List page also defines a load() function that goes in its +page.server.js file.
  • SvelteKit does the grunt work of taking the results of these data sources, merging them together, and making both available in data.
Our Details page

We’ll use our Details page to edit a to-do item. First, let’s add a column to the table in our List page that links to the Details page with the to-do item’s ID in the query string.

<td><a href="/details?id={t.id}">Edit</a></td>

Now let’s build out our Details page. First, we’ll add a loader to grab the to-do item we’re editing. Create a +page.server.js in /details, with this content:

import { getTodo, updateTodo, wait } from "$lib/data/todoData"; export function load({ url }) { const id = url.searchParams.get("id"); console.log(id); const todo = getTodo(id); return { todo, }; }

Our loader comes with a url property from which we can pull query string values. This makes it easy to look up the to-do item we’re editing. Let’s render that to-do, along with functionality to edit it.

SvelteKit has wonderful built-in mutation capabilities, so long as you use forms. Remember forms? Here’s our Details page. I’ve elided the styles for brevity.

<script> import { enhance } from "$app/forms"; export let data; $: ({ todo, tags } = data); $: currentTags = todo.tags.map(id => tags[id]); </script> <form use:enhance method="post" action="?/editTodo"> <input name="id" type="hidden" value="{todo.id}" /> <input name="title" value="{todo.title}" /> <div> {#each currentTags as tag} <span style="{`color:" ${tag.color};`}>{tag.name}</span> {/each} </div> <button>Save</button> </form>

We’re grabbing the tags as before from our layout group’s loader and the to-do item from our page’s loader. We’re grabbing the actual tag objects from the to-do’s list of tag IDs and then rendering everything. We create a form with a hidden input for the ID and a real input for the title. We display the tags and then provide a button to submit the form.

If you noticed the use:enhance, that simply tells SvelteKit to use progressive enhancement and Ajax to submit our form. You’ll likely always use that.

How do we save our edits?

Notice the action="?/editTodo" attribute on the form itself? This tells us where we want to submit our edited data. For our case, we want to submit to an editTodo “action.”

Let’s create it by adding the following to the +page.server.js file we already have for Details (which currently has a load() function, to grab our to-do):

import { redirect } from "@sveltejs/kit"; // ... export const actions = { async editTodo({ request }) { const formData = await request.formData(); const id = formData.get("id"); const newTitle = formData.get("title"); await wait(250); updateTodo(id, newTitle); throw redirect(303, "/list"); }, };

Form actions give us a request object, which provides access to our formData, which has a get method for our various form fields. We added that hidden input for the ID value so we could grab it here in order to look up the to-do item we’re editing. We simulate a delay, call a new updateTodo() method, then redirect the user back to the /list page. The updateTodo() method merely updates our static data; in real life you’d run some sort of update in whatever datastore you’re using.

export async function updateTodo(id, newTitle) { const todo = todos.find(t => t.id == id); Object.assign(todo, { title: newTitle }); }

Let’s try it out. We’ll go to the List page first:

Now let’s click the Edit button for one of the to-do items to bring up the editing page in /details.

We’re going to add a new title:

Now, click Save. That should get us back to our /list page, with the new to-do title applied.

How did the new title show up like that? It was automatic. Once we redirected to the /list page, SvelteKit automatically re-ran all of our loaders just like it would have done regardless. This is the key advancement that next-gen application frameworks, like SvelteKit, Remix, and Next 13 provide. Rather than giving you a convenient way to render pages then wishing you the best of luck fetching whatever endpoints you might have to update data, they integrate data mutation alongside data loading, allowing the two to work in tandem.

A few things you might be wondering…

This mutation update doesn’t seem too impressive. The loaders will re-run whenever you navigate. What if we hadn’t added a redirect in our form action, but stayed on the current page? SvelteKit would perform the update in the form action, like before, but would still re-run all of the loaders for the current page, including the loaders in the page layout(s).

Can we have more targeted means of invalidating our data? For example, our tags were not edited, so in real life we wouldn’t want to re-query them. Yes, what I showed you is just the default forms behavior in SvelteKit. You can turn the default behavior off by providing a callback to use:enhance. Then SvelteKit provides manual invalidation functions.

Loading data on every navigation is potentially expensive, and unnecessary. Can I cache this data like I do with tools like react-query? Yes, just differently. SvelteKit lets you set (and then respect) the cache-control headers the web already provides. And I’ll be covering cache invalidation mechanisms in a follow-on post.

Everything we’ve done throughout this article uses static data and modifies values in memory. If you need to revert everything and start over, stop and restart the npm run dev Node process.

Wrapping up

We’ve barely scratched the surface of SvelteKit, but hopefully you’ve seen enough to get excited about it. I can’t remember the last time I’ve found web development this much fun. With things like bundling, routing, SSR, and deployment all handled out of the box, I get to spend more time coding than configuring.

Here are a few more resources you can use as next steps learning SvelteKit:

Getting Started With SvelteKit originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

How to Transition to Manifest V3 for Chrome Extensions

Thu, 01/19/2023 - 3:54am

While I am not a regular Chrome extension programmer, I have certainly coded enough extensions and have a wide enough web development portfolio to know my way around the task. However, just recently, I had a client reject one of my extensions as I received feedback that my extension was “outdated”.

As I was scrambling to figure out what was wrong, I swept my embarrassment under the carpet and immediately began my deep dive back into the world of Chrome Extensions. Unfortunately, information on Manifest V3 was scarce and it was difficult for me to understand quickly what this transition was all about.

Needless to say, with a pending job, I had to painstakingly navigate my way around Google’s Chrome Developer Documentation and figure things out for myself. While I got the job done, I did not want my knowledge and research in this area to go to waste and decided to share what I wish I could have had easy access to in my learning journey.

Why the transition to Manifest 3 is important

Manifest V3 is an API that Google will use in its Chrome browser. It is the successor to the current API, Manifest V2, and governs how Chrome extensions interact with the browser. Manifest V3 introduces significant changes to the rules for extensions, some of which will be the new mainstay from V2 we were used to.

The transition to Manifest V3 can be summarized as such:

  1. The transition has been ongoing since 2018.
  2. Manifest V3 will officially begin rolling out in January 2023.
  3. By June 2023, extensions that run Manifest V2 will no longer be available on the Chrome Web Store.
  4. Extensions that do not comply with the new rules introduced in Manifest V3 will eventually be removed from the Chrome Web Store.

One of the main goals of Manifest V3 is to make users safer and improve the overall browser experience. Previously, many browser extensions relied on code in the cloud, meaning it could be difficult to assess whether an extension was risky. Manifest V3 aims to address this by requiring extensions to contain all the code they will run, allowing Google to scan them and detect potential risks. It also forces extensions to request permission from Google for the changes they can implement on the browser.

Staying up-to-date with Google’s transition to Manifest V3 is important because it introduces new rules for extensions that aim to improve user safety and the overall browser experience, and extensions that do not comply with these rules will eventually be removed from the Chrome Web Store.

In short, all of your hard work in creating extensions that used Manifest V2 could be for naught if you do not make this transition in the coming months.

January 2023June 2023January 2024Support for Manifest V2 extensions will be turned off in Chrome’s Canary, Dev, and Beta channels.The Chrome Web Store will no longer allow Manifest V2 extensions to be published with visibility set to Public.The Chrome Web Store will remove all remaining Manifest V2 extensions.Manifest V3 will be required for the Featured badge in the Chrome Web Store.Existing Manifest V2 extensions that are published and publically visible will become unlisted.Support for Manifest 2 will end for all of Chrome’s channels, including the Stable channel, unless the Enterprise channel is extended. The key differences between Manifest V2 and V3

There are many differences between the two, and while I highly recommend that you read up on Chrome’s “Migrating to Manifest V3” guide, here is a short and sweet summary of key points:

  1. Service workers replace background pages in Manifest V3.
  2. Network request modification is handled with the new declarativeNetRequest API in Manifest V3.
  3. In Manifest V3, extensions can only execute JavaScript that is included within their package and cannot use remotely-hosted code.
  4. Manifest V3 introduces promise support to many methods, though callbacks are still supported as an alternative.
  5. Host permissions in Manifest V3 are a separate element and must be specified in the "host_permissions" field.
  6. The content security policy in Manifest V3 is an object with members representing alternative content security policy (CSP) contexts, rather than a string as it was in Manifest V2.

In a simple Chrome Extension’s Manifest that alters a webpage’s background, that might look like this:

// Manifest V2 { "manifest_version": 2, "name": "Shane's Extension", "version": "1.0", "description": "A simple extension that changes the background of a webpage to Shane's face.", "background": { "scripts": ["background.js"], "persistent": true }, "browser_action": { "default_popup": "popup.html" }, "permissions": [ "activeTab", ], "optional_permissions": ["<all_urls>"] } // Manifest V3 { "manifest_version": 3, "name": "Shane's Extension", "version": "1.0", "description": "A simple extension that changes the background of a webpage to Shane's face.", "background": { "service_worker": "background.js" }, "action": { "default_popup": "popup.html" }, "permissions": [ "activeTab", ], "host_permissions": [ "<all_urls>" ] }

If you find some of the tags above seem foreign to you, keep reading to find out exactly what you need to know.

How to smoothly transition to Manifest V3

I have summarized the transition to Manifest V3 in four key areas. Of course, while there are many bells and whistles in the new Manifest V3 that need to be implemented from the old Manifest V2, implementing changes in these four areas will get your Chrome Extension well on the right track for the eventual transition.

The four key areas are:

  1. Updating your Manifest’s basic structure.
  2. Modify your host permissions.
  3. Update the content security policy.
  4. Modify your network request handling.

With these four areas, your Manifest’s fundamentals will be ready for the transition to Manifest V3. Let’s look at each of these key aspects in detail and see how we can work towards future-proofing your Chrome Extension from this transition.

Updating your Manifest’s basic structure

Updating your manifest’s basic structure is the first step in transitioning to Manifest V3. The most important change you will need to make is changing the value of the "manifest_version" element to 3, which determines that you are using the Manifest V3 feature set.

One of the major differences between Manifest V2 and V3 is the replacement of background pages with a single extension service worker in Manifest V3. You will need to register the service worker under the "background" field, using the "service_worker" key and specify a single JavaScript file. Even though Manifest V3 does not support multiple background scripts, you can optionally declare the service worker as an ES Module by specifying "type": "module", which allows you to import further code.

In Manifest V3, the "browser_action" and "page_action" properties are unified into a single "action" property. You will need to replace these properties with "action" in your manifest. Similarly, the "chrome.browserAction" and "chrome.pageAction" APIs are unified into a single “Action” API in Manifest V3, and you will need to migrate to this API.

// Manifest V2 "background": { "scripts": ["background.js"], "persistent": false }, "browser_action": { "default_popup": "popup.html" }, // Manifest V3 "background": { "service_worker": "background.js" }, "action": { "default_popup": "popup.html" }

Overall, updating your manifest’s basic structure is a crucial step in the process of transitioning to Manifest V3, as it allows you to take advantage of the new features and changes introduced in this version of the API.

Modify your host permissions

The second step in transitioning to Manifest V3 is modifying your host permissions. In Manifest V2, you specify host permissions in the "permissions" field in the manifest file. In Manifest V3, host permissions are a separate element, and you should specify them in the "host_permissions" field in the manifest file.

Here is an example of how to modify your host permissions:

// Manifest V2 "permissions": [ "activeTab", "storage", "http://www.css-tricks.com/", ":///*" ] // Manifest V3 "permissions": [ "activeTab", "scripting", "storage" ], "host_permissions": [ "http://www.css-tricks.com/" ], "optional_host_permissions": [ ":///*" ] Update the content security policy

In order to update the CSP of your Manifest V2 extension to be compliant with Manifest V3, you will need to make some changes to your manifest file. In Manifest V2, the CSP was specified as a string in the "content_security_policy" field of the manifest.

In Manifest V3, the CSP is now an object with different members representing alternative CSP contexts. Instead of a single "content_security_policy" field, you will now have to specify separate fields for "content_security_policy.extension_pages" and "content_security_policy.sandbox", depending on the type of extension pages you are using.

You should also remove any references to external domains in the "script-src", "worker-src", "object-src", and "style-src" directives if they are present. It is important to make these updates to your CSP in order to ensure the security and stability of your extension in Manifest V3.

// Manifest V2 "content_security_policy": "script-src 'self' https://css-tricks.com; object-src 'self'" // Manfiest V3 "content_security_policy.extension_pages": "script-src 'self' https://example.com; object-src 'self'", "content_security_policy.sandbox": "script-src 'self' https://css-tricks.com; object-src 'self'" Modify your network request handling

The final step in transitioning to Manifest V3 is modifying your network request handling. In Manifest V2, you would have used the chrome.webRequest API to modify network requests. However, this API is replaced in Manifest V3 by the declarativeNetRequest API.

To use this new API, you will need to specify the declarativeNetRequest permission in your manifest and update your code to use the new API. One key difference between the two APIs is that the declarativeNetRequest API requires you to specify a list of predetermined addresses to block, rather than being able to block entire categories of HTTP requests as you could with the chrome.webRequest API.

It is important to make these changes in your code to ensure that your extension continues to function properly under Manifest V3. Here is an example of how you would modify your manifest to use the declarativeNetRequest API in Manifest V3:

// Manifest V2 "permissions": [ "webRequest", "webRequestBlocking" ] // Manifest V3 "permissions": [ "declarativeNetRequest" ]

You will also need to update your extension code to use the declarativeNetRequest API instead of the chrome.webRequest API.

Other aspects you need to check

What I have covered is just the tip of the iceberg. Of course, if I wanted to cover everything, I could be here for days and there would be no point in having Google’s Chrome Developers guides. While what I covered will have you future-proofed enough to arm your Chrome extensions in this transition, here are some other things you might want to look at to ensure your extensions are functioning at the top of their game.

  • Migrating background scripts to the service worker execution context: As mentioned earlier, Manifest V3 replaces background pages with a single extension service worker, so it may be necessary to update background scripts to adapt to the service worker execution context.
  • Unifying the **chrome.browserAction** and **chrome.pageAction** APIs: These two equivalent APIs are unified into a single API in Manifest V3, so it may be necessary to migrate to the Action API.
  • Migrating functions that expect a Manifest V2 background context: The adoption of service workers in Manifest V3 is not compatible with methods like chrome.runtime.getBackgroundPage(), chrome.extension.getBackgroundPage(), chrome.extension.getExtensionTabs(), and chrome.extension.getViews(). It may be necessary to migrate to a design that passes messages between other contexts and the background service worker.
  • Moving CORS requests in content scripts to the background service worker: It may be necessary to move CORS requests in content scripts to the background service worker in order to comply with Manifest V3.
  • Migrating away from executing external code or arbitrary strings: Manifest V3 no longer allows the execution of external logic using chrome.scripting.executeScript({code: '...'}), eval(), and new Function(). It may be necessary to move all external code (JavaScript, WebAssembly, CSS) into the extension bundle, update script and style references to load resources from the extension bundle, and use chrome.runtime.getURL() to build resource URLs at runtime.
  • Updating certain scripting and CSS methods in the Tabs API: As mentioned earlier, several methods move from the Tabs API to the Scripting API in Manifest V3. It may be necessary to update any calls to these methods to use the correct Manifest V3 API.

And many more!

Feel free to take some time to get yourself up to date on all the changes. After all, this change is inevitable and if you do not want your Manifest V2 extensions to be lost due to avoiding this transition, then spend some time arming yourself with the necessary knowledge.

On the other hand, if you are new to programming Chrome extensions and looking to get started, a great way to go about it is to dive into the world of Chrome’s Web Developer tools. I did so through a course on Linkedin Learning, which got me up to speed pretty quickly. Once you have that base knowledge, come back to this article and translate what you know to Manifest V3!

So, how will I be using the features in the new Manifest V3 going forward?

Well, to me, the transition to Manifest V3 and the removal of the chrome.webRequest API seems to be shifting extensions away from data-centric use cases (such as ad blockers) to more functional and application-based uses. I have been staying away from application development lately as it can get quite resource-intensive at times. However, this shift might be what brings me back!

The rise of AI tools in recent times, many with available-to-use APIs, has sparked tons of new and fresh SaaS applications. Personally, I think that it’s coming at a perfect time with the shift to more application-based Chrome extensions! While many of the older extensions may be wiped out from this transition, plenty of new ones built around novel SaaS ideas will come to take their place.

Hence, this is an exciting update to hop on and revamp old extensions or build new ones! Personally, I see many possibilities in using APIs that involve AI being used in extensions to enhance a user’s browsing experience. But that’s really just the tip of the iceberg. If you’re looking to really get into things with your own professional extensions or reaching out to companies to build/update extensions for them, I would recommend upgrading your Gmail account for the benefits it gives in collaborating, developing, and publishing extensions to the Chrome Web Store.

However, remember that every developer’s requirements are different, so learn what you need to keep your current extensions afloat, or your new ones going!

How to Transition to Manifest V3 for Chrome Extensions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Solved With :has(): Vertical Spacing in Long-Form Text

Wed, 01/18/2023 - 3:44am

If you’ve ever worked on sites with lots of long-form text — especially CMS sites where people can enter screeds of text in a WYSIWYG editor — you’ve likely had to write CSS to manage the vertical spacing between different typographic elements, like headings, paragraphs, lists and so on.

It’s surprisingly tricky to get this right. And it’s one reason why things like the Tailwind Typography plugin and Stack Overflow’s Prose exist — although these handle much more than just vertical spacing.

Firefox supports :has() behind the layout.css.has-selector.enabled flag in about:config at the time of writing.

What makes typographic vertical spacing complicated?

Surely it should just be as simple as saying that each element — p, h2, ul, etc. — has some amount of top and/or bottom margin… right? Sadly, this isn’t the case. Consider this desired behavior:

  • The first and last elements in a block of long-form text shouldn’t have any extra space above or below (respectively). This is so that other, non-typographic elements are still placed predictably around the long-form content.
  • Sections within the long-form content should have a nice big space between them. A “section” being a heading and all the following content that belongs to that heading. In practice, this means having a nice big space before a heading… but not if that heading is immediately preceded by another heading!
We want to more space above the Heading 3 when it follows a typographic element, like a paragraph, but less space when it immediately follows another heading.

You need to look no further than right here at CSS-Tricks to see where this could come in handy. Here are a couple of screenshots of spacing I pulled from another article.

The vertical spacing between Heading 2 and Heading 3 The vertical space between Heading 3 and a paragraph The traditional solution

The typical solution I’ve seen involves putting any long-form content in a wrapping div (or a semantic tag, if appropriate). My go-to class name has been .rich-text, which I think I use as a hangover from older versions of the Wagtail CMS, which would add this class automatically when rendering WYSIWYG content. Tailwind Typography uses a .prose class (plus some modifier classes).

Then we add CSS to select all typographic elements in that wrapper and add vertical margins. Noting, of course, the special behavior mentioned above to do with stacked headings and the first/last element.

CodePen Embed Fallback

The traditional solution sounds reasonable… what’s the problem? I think there are a few…

Rigid structure

Having to add a wrapper class like .rich-text in all the right places means baking in a specific structure to your HTML code. That’s sometimes necessary, but it feels like it shouldn’t have to be in this particular case. It can also be easy to forget to do this everywhere you need to, especially if you need to use it for a mix of CMS and hard-coded content.

The HTML structure gets even more rigid when you want to be able to trim the top and bottom margin off the first and last elements, respectively, because they need to be immediate children of the wrapper element, e.g., .rich-text > *:first-child. That > is important — after all, we don’t want to accidentally select the first list item in each ul or ol with this selector.

Mixing margin properties

In the pre-:has() world, we haven’t had a way to select an element based on what follows it. Therefore, the traditional approach to spacing typographic elements involves using a mix of both margin-top and margin-bottom:

  1. We start by setting our default spacing to elements with margin-bottom.
  2. Next, we space out our “sections” using margin-top — i.e. very big space above each heading
  3. Then we override those big margin-tops when a heading is followed immediately by another heading using the adjacent sibling selector (e.g. h2 + h3).

Now, I don’t know about you, but I’ve always felt it’s better to use a single margin direction when spacing things out, generally favoring margin-bottom (that’s assuming the CSS gap property isn’t feasible, which it is not in this case). Whether this is a big deal, or even true, I’ll let you decide. But personally, I’d rather be setting margin-bottom for spacing long-form content.

Collapsing margins

Because of collapsing margins, this mix of top and bottom margins isn’t a big problem per se. Only the larger of two stacked margins will take effect, not the sum of both margins. But… well… I don’t really like collapsing margins.

Collapsing margins are yet one more thing to be aware of. It might be confusing for junior devs who aren’t up to speed with that CSS quirk. The spacing will totally change (i.e. stop collapsing) if you were to change the wrapper to a flex layout with flex-direction: column for instance, which is something that wouldn’t happen if you set your vertical margins in a single direction.

I more-or-less know how collapsing margins work, and I know that they’re there by design. I also know they’ve made my life easier on occasion. But they’ve also made it harder other times. I just think they’re kinda weird, and I’d generally rather avoid relying on them.

The :has() solution

And here is my attempt at solving these issues with :has().

CodePen Embed Fallback

To recap the improvements this aims to make:

  • No wrapper class is required.
  • We’re working with a consistent margin direction.
  • Collapsing margins are avoided (which may or may not be an improvement, depending on your stance).
  • There’s no setting styles and then immediately overriding them.
Notes and caveats on the :has() solution
  • Always check browser support. At time of writing, Firefox only supports :has() behind an experimental flag.
  • My solution doesn’t include all possible typographic elements. For instance, there’s no <blockquote> in my demo. The selector list is easy enough to extend though.
  • My solution also doesn’t handle non-typographic elements that may be present in your particular long-form text blocks, e.g. <img>. That’s because for the sites I work on, we tend to lock down the WYSIWYG as much as possible to core text nodes, like headings, paragraphs, and lists. Anything else — e.g. quotes, images, tables, etc. — is a separate CMS component block, and those blocks themselves are spaced apart from each other when rendered on a page. But again, the selector list can be extended.
  • I’ve only included h1 for the sake of completeness. I usually wouldn’t allow a CMS user to add an h1 via WYSIWYG, as the page title would be baked into the page template somewhere rather than entered in the CMS page editor.
  • I’m not catering for a heading followed immediately by the same level heading (h2 + h2). This would mean that the first heading wouldn’t “own” any content, which seems like a misuse of headings (and, correct me if I’m wrong, but it might violate WCAG 1.3.1 Info and Relationships). I’m also not catering for skipped heading levels, which are invalid.
  • I am in no way knocking the existing approaches I mentioned. If and when I build another Tailwind site I’ll use the excellent Typography plugin, no question!
  • I’m not a designer. I came up with these spacing values by eyeballing it. You probably could (and should) use better values.
Specificity and project structure

I was going to write a whole big thing here about how the traditional method and the new :has() way of doing it might fit into the ITCSS methodology… But now that we have :where() (the zero-specificity selector) you can pretty much choose your preferred level of specificity for any selector now.

That said, the fact that we’re no longer dealing with a wrapper — .prose, .rich-text, etc. — to me makes it feel like this should live in the “elements” layer, i.e. before you start dealing with class-level specificity. I’ve used :where() in my examples to keep specificity consistent. All the selectors in both of my examples have a specificity score of 0,0,1 (except for the bare-bones reset).

Wrapping up

So there you have it, a bleeding-edge solution to a very boring problem! This newer approach is still not what I’d call “simple” CSS — as I said at the beginning, it’s a more complex topic than it might seem at first. But aside from having a few slightly complex selectors, I think the new approach makes more sense overall, and the less rigid HTML structure seems very appealing.

If you end up using this, or something like it, I’d love to know how it works out for you. And if you can think of ways to improve it, I’d love to hear those too!

Solved With :has(): Vertical Spacing in Long-Form Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

6 Common SVG Fails (and How to Fix Them)

Tue, 01/17/2023 - 3:51am

Someone recently asked me how I approach debugging inline SVGs. Because it is part of the DOM, we can inspect any inline SVG in any browser DevTools. And because of that, we have the ability to scope things out and uncover any potential issues or opportunities to optimize the SVG.

But sometimes, we can’t even see our SVGs at all. In those cases, there are six specific things that I look for when I’m debugging.

1. The viewBox values

The viewBox is a common point of confusion when working with SVG. It’s technically fine to use inline SVG without it, but we would lose one of its most significant benefits: scaling with the container. At the same time, it can work against us when improperly configured, resulting in unwanted clipping.

The elements are there when they’re clipped — they’re just in a part of the coordinate system that we don’t see. If we were to open the file in some graphics editing program, it might look like this:

Screenshot of SVG opened in Illustrator.

The easiest way to fix this? Add overflow="visible" to the SVG, whether it’s in our stylesheet, inline on the style attribute or directly as an SVG presentation attribute. But if we also apply a background-color to the SVG or if we have other elements around it, things might look a little bit off. In this case, the best option will be to edit the viewBox to show that part of the coordinate system that was hidden:

Demo applying overflow="hidden" and editing the viewBox.

There are a few additional things about the viewBox that are worth covering while we’re on the topic:

How does the viewBox work?

SVG is an infinite canvas, but we can control what we see and how we see it through the viewport and the viewBox.

The viewport is a window frame on the infinite canvas. Its dimensions are defined by width and height attributes, or in CSS with the corresponding width and height properties. We can specify any length unit we want, but if we provide unitless numbers, they default to pixels.

The viewBox is defined by four values. The first two are the starting point at the upper-left corner (x and y values, negative numbers allowed). I’m editing these to reframe the image. The last two are the width and height of the coordinate system inside the viewport — this is where we can edit the scale of the grid (which we’ll get into in the section on Zooming).

Here’s simplified markup showing the SVG viewBox and the width and height attributes both set on the <svg>:

<svg viewBox="0 0 700 700" width="700" height="700"> <!-- etc. --> </svg> Reframing

So, this:

<svg viewBox="0 0 700 700">

…maps to this:

<svg viewBox="start-x-axis start-y-axis width height">

The viewport we see starts where 0 on the x-axis and 0 on the y-axis meet.

By changing this:

<svg viewBox="0 0 700 700">

…to this:

<svg viewBox="300 200 700 700">

…the width and height remain the same (700 units each), but the start of the coordinate system is now at the 300 point on the x-axis and 200 on the y-axis.

In the following video I’m adding a red <circle> to the SVG with its center at the 300 point on the x-axis and 200 on the y-axis. Notice how changing the viewBox coordinates to the same values also changes the circle’s placement to the upper-left corner of the frame while the rendered size of the SVG remains the same (700×700). All I did was “reframe” things with the viewBox.


We can change the last two values inside the viewBox to zoom in or out of the image. The larger the values, the more SVG units are added to fit in the viewport, resulting in a smaller image. If we want to keep a 1:1 ratio, our viewBox width and height must match our viewport width and height values.

Let’s see what happens in Illustrator when we change these parameters. The artboard is the viewport which is represented by a white 700px square. Everything else outside that area is our infinite SVG canvas and gets clipped by default.

Figure 1 below shows a blue dot at 900 along the x-axis and 900 along the y-axis. If I change the last two viewBox values from 700 to 900 like this:

<svg viewBox="300 200 900 900" width="700" height="700">

…then the blue dot is almost fully back in view, as seen in Figure 2 below. Our image is scaled down because we increased the viewBox values, but the SVG’s actual width and height dimensions remained the same, and the blue dot made its way back closer to the unclipped area.

Figure 1 Figure 2

There is a pink square as evidence of how the grid scales to fit the viewport: the unit gets smaller, and more grid lines fit into the same viewport area. You can play with the same values in the following Pen to see that work in action:

CodePen Embed Fallback 2. Missing width and height

Another common thing I look at when debugging inline SVG is whether the markup contains the width or height attributes. This is no big deal in many cases unless the SVG is inside a container with absolute positioning or a flexible container (as Safari computes the SVG width value with 0px instead of auto). Excluding width or height in these cases prevents us from seeing the full image, as we can see by opening this CodePen demo and comparing it in Chrome, Safari, and Firefox (tap images for larger view).

Chrome Safari Firefox

The solution? Add a width or height, whether as a presentation attribute, inline in the style attribute, or in CSS. Avoid using height by itself, particularly when it is set to 100% or auto. Another workaround is to set the right and left values.

You can play around with the following Pen and combine the different options.

CodePen Embed Fallback 3. Inadvertent fill and stroke colors

It may also be that we are applying color to the <svg> tag, whether it’s an inline style or coming from CSS. That’s fine, but there could be other color values throughout the markup or styles that conflict with the color set on the <svg>, causing parts to be invisible.

That’s why I tend to look for the fill and stroke attributes in the SVG’s markup and wipe them out. The following video shows an SVG I styled in CSS with a red fill. There are a couple of instances where parts of the SVG are filled in white directly in the markup that I removed to reveal the missing pieces.

4. Missing IDs

This one might seem super obvious, but you’d be surprised how often I see it come up. Let’s say we made an SVG file in Illustrator and were very diligent about naming our layers so that you get nice matching IDs in the markup when exporting the file. And let’s say we plan to style that SVG in CSS by hooking into those IDs.

That’s a nice way to do things. But there are plenty of times where I’ve seen the same SVG file exported a second time to the same location and the IDs are different, usually when copy/pasting the vectors directly. Maybe a new layer was added, or one of the existing ones was renamed or something. Whatever the case, the CSS rules no longer match the IDs in the SVG markup, causing the SVG to render differently than you’d expect.

Pasting Illustrator’s exported SVG file into SVGOMG.

In large SVG files we might find it difficult to find those IDs. This is a good time to open the DevTools, inspect that part of the graphic that’s not working, and see if those IDs are still matching.

So, I’d say it’s worth opening an exported SVG file in a code editor and comparing it to the original before swapping things out. Apps like Illustrator, Figma, and Sketch are smart, but that doesn’t mean we aren’t responsible for vetting them.

5. Checklist for clipping and masking

If an SVG is unexpectedly clipped and the viewBox checks out alright, I usually look at the CSS for clip-path or mask properties that might interfere with the image. It’s tempting to keep looking at the inline markup, but it’s good to remember that an SVG’s styling might be happening elsewhere.

CSS clipping and masking allow us to “hide” parts of an image or element. In SVG, <clipPath> is a vector operation that cuts parts of an image with no halfway results. The <mask> tag is a pixel operation that allows transparency, semi-transparency effects, and blurred edges.

This is a small checklist for debugging cases where clipping and masking are involved:

  • Make sure the clipping path (or mask) and the graphic overlap one another. The overlapping parts are what gets displayed.
  • If you have a complex path that is not intersecting your graphic, try applying transforms until they match.
  • You can still inspect the inner code with the DevTools even though the <clipPath> or <mask> are not rendered, so use it!
  • Copy the markup inside <clipPath> and <mask> and paste it before closing the </svg> tag. Then add a fill to those shapes and check the SVG’s coordinates and dimensions. If you still do not see the image, try adding overflow="hidden" to the <svg> tag.
  • Check that a unique ID is used for the <clipPath> or <mask>, and that the same ID is applied to the shapes or group of shapes that are clipped or masked. A mismatched ID will break the appearance.
  • Check for typos in the markup between the <clipPath> or <mask> tags.
  • fill, stroke, opacity, or some other styles applied to the elements inside <clipPath> are useless — the only useful part is the fill-region geometry of those elements. That’s why if you use a <polyline> it will behave as a <polygon> and if you use a <line> you won’t see any clipping effect.
  • If you don’t see your image after applying a <mask>, make sure that the fill of the masking content is not entirely black. The luminance of the masking element determines the opacity of the final graphic. You’ll be able to see through the brighter parts, and the darker parts will hide your image’s content.

You can play with masked and clipped elements in this Pen.

6. Namespaces

Did you know that SVG is an XML-based markup language? Well, it is! The namespace for SVG is set on the xmlns attribute:

<svg xmlns="http://www.w3.org/2000/svg"> <!-- etc. --> </svg>

There’s a lot to know about namespacing in XML and MDN has a great primer on it. Suffice to say, the namespace provides context to the browser, informing it that the markup is specific to SVG. The idea is that namespaces help prevent conflicts when more than one type of XML is in the same file, like SVG and XHTML. This is a much less common issue in modern browsers but could help explain SVG rendering issues in older browsers or browsers like Gecko that are strict when defining doctypes and namespaces.

The SVG 2 specification does not require namespacing when using HTML syntax. But it’s crucial if support for legacy browsers is a priority — plus, it doesn’t hurt anything to add it. That way, when the <html> element’s xmlns attribute is defined, it will not conflict in those rare cases.

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <body> <svg xmlns="http://www.w3.org/2000/svg" width="700px" height="700px"> <!-- etc. --> </svg> </body> </html>

This is also true when using inline SVG in CSS, like setting it as a background image. In the following example, a checkmark icon appears on the input after successful validation. This is what the CSS looks like:

textarea:valid { background: white url('data:image/svg+xml,\ <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26">\ <circle cx="13" cy="13" r="13" fill="%23abedd8"/>\ <path fill="none" stroke="white" stroke-width="2" d="M5 15.2l5 5 10-12"/>\ </svg>') no-repeat 98% 5px; }

When we remove the namespace inside the SVG in the background property, the image disappears:

Another common namespace prefix is xlink:href. We use it a lot when referencing other parts of the SVG like: patterns, filters, animations or gradients. The recommendation is to start replacing it with href as the other one is being deprecated since SVG 2, but there might be compatibility issues with older browsers. In that case, we can use both. Just remember to include the namespace xmlns:xlink="http://www.w3.org/1999/xlink" if you are still using xlink:href.

Level up your SVG skills!

I hope these tips help save you a ton of time if you find yourself troubleshooting improperly rendered inline SVGs. These are just the things I look for. Maybe you have different red flags you watch for — if so, tell me in the comments!

The bottom line is that it pays to have at least a basic understanding of the various ways SVG can be used. CodePen Challenges often incorporate SVG and offer good practice. Here are a few more resources to level up:

There are a few people I suggest following for SVG-related goodness:

6 Common SVG Fails (and How to Fix Them) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

:has is an unforgiving selector

Wed, 01/11/2023 - 4:18am

A little thing happened on the way to publishing the CSS :has() selector to the ol’ Almanac. I had originally described :has() as a “forgiving” selector, the idea being that anything in its argument is evaluated, even if one or more of the items is invalid.

/* Example: Do not use! */ article:has(h2, ul, ::-scoobydoo) { }

See ::scoobydoo in there? That’s totally invalid. A forgiving selector list ignores that bogus selector and proceeds to evaluate the rest of the items as if it were written like this:

article:has(h2, ul) { }

:has() was indeed a forgiving selector in a previous draft dated May 7, 2022. But that changed after an issue was reported that the forgiving nature conflicts with jQuery when :has() contains a complex selector (e.g. header h2 + p). The W3C landed on a resolution to make :has() an “unforgiving” selector just a few weeks ago.

So, our previous example? The entire selector list is invalid because the bogus selector is invalid. But the other two forgiving selectors, :is() and :where(), are left unchanged.

There’s a bit of a workaround for this. Remember, :is() and :where()are forgiving, even if :has() is not. That means we can nest either of the those selectors in :has() to get more forgiving behavior:

article:has(:where(h2, ul, ::-scoobydoo)) { }

Which one you use might matter because the specificity of :is() is determined by the most specific item in its list. So, if you need to something less specific you’d do better reaching for :where() since it does not add to the specificity score.

/* Specificity: (0,0,1) */ article:has(:where(h2, ul, ::-scoobydoo)) { } /* Specificity: (0,0,2) */ article:has(:is(h2, ul, ::-scoobydoo)) { } CodePen Embed Fallback

We updated a few of our posts to reflect the latest info. I’m seeing plenty of others in the wild that need to be updated, so just a little PSA for anyone who needs to do the same.

:has is an unforgiving selector originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Faking Min Width on a Table Column

Tue, 01/10/2023 - 5:11am

The good ol’ <table> tag is the most semantic HTML for showing tabular data. But I find it very hard to control how the table is presented, particularly column widths in a dynamic environment where you might not know how much content is going into each table cell. In some cases, one column is super wide while others are scrunched up. Other times, we get equal widths, but at the expense of a column that contains more content and needs more space.

But I found a CSS tricks-y workaround that helps make things a little easier. That’s what I want to show you in this post.

The problem

First we need to understand how layout is handled by the browser. We have the table-layout property in CSS to define how a table should distribute the width for each table column. It takes one of two values:

  • auto (default)
  • fixed

Let us start with a table without defining any widths on its columns. In other words, we will let the browser decide how much width to give each column by applying table-layout: auto on it in CSS. As you will notice, the browser does its best with the algorithm it has to divide the full available width between each column.

CodePen Embed Fallback

If we swap out an auto table layout with table-layout: fixed, then the browser will merely divide the full available space by the total number of columns, then apply that value as the width for each column:

CodePen Embed Fallback

But what if we want to control the widths of our columns? We have the <colgroup> element to help! It consists of individual <col> elements we can use to specify the exact width we need for each column. Let’s see how that works in with table-layout: auto:

CodePen Embed Fallback

I have inlined the styles for the sake of illustration.

The browser is not respecting the inline widths since they exceed the amount of available table space when added up. As a result, the table steals space from the columns so that all of the columns are visible. This is perfectly fine default behavior.

How does <colgroup> work with table-layout: fixed. Let’s find out:

CodePen Embed Fallback

This doesn’t look good at all. We need the column with a bunch of content in it to flex a little while maintaining a fixed width for the rest of the columns. A fixed table-layout value respects the width — but so much so that it eats up the space of the column that needs the most space… which is a no-go for us.

This could easily be solved if only we could set a min-width on the column instead of a width. That way, the column would say, “I can give all of you some of my width until we reach this minimum value.“ Then the table would simply overflow its container and give the user a horizontal scroll to display the rest of the table. But unfortunately, min-width on table columns are not respected by the <col> element.

The solution

The solution is to fake a min-width and we need to be a bit creative to do it.

We can add an empty <col> as the second column for our <colgroup> in the HTML and apply a colspan attribute on the first column so that the first column takes up the space for both columns:

<table> <colgroup> <col class="col-200" /> <col /> <col class="col-input" /> <col class="col-date" /> <col class="col-edit" /> </colgroup> <thead> <tr> <th colspan="2">Project name</th> <th>Amount</th> <th>Date</th> <th>Edit</th> </tr> </thead> <!-- etc. --> </table>

Note that I have added classes in place of the inline styles from the previous example. The same idea still applies: we’re applying widths to each column.

The trick is that relationship between the first <col> and the empty second <col>. If we apply a width to the first <col> (it’s 200px in the snippet above), then the second column will be eaten up when the fixed table layout divides up the available space to distribute to the columns. But the width of the first column (200px) is respected and remains in place.

Voilà! We have a faux min-width set on a table cell. The first cell flexes as the available space changes and the table overflows for horizontal scrolling just as we hoped it would.

CodePen Embed Fallback

(I added a little sticky positioning to the first column there.)


Let’s not totally forget about accessibility here. I ran the table through NVDA on Windows and VoiceOver on macOS and found that all five columns are announced, even if we’re only using four of them. And when the first column is in focus, it announces, “Column one through two”. Not perfectly elegant but also not going to cause someone to get lost. I imagine we could throw an aria-hidden attribute on the unused column, but also know ARIA isn’t a substitute for poor HTML.

I’ll admit, this feels a little, um, hacky. But it does work! Let me know if you have a different approach in the comments… or know of any confusions this “hack” might bring to our users.

Faking Min Width on a Table Column originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Styling Buttons in WordPress Block Themes

Mon, 01/09/2023 - 3:56am

A little while back, Ganesh Dahal penned a post here on CSS-Tricks responding to a tweet that asked about adding CSS box shadows on WordPress blocks and elements. There’s a lot of great stuff in there that leverages new features that shipped in WordPress 6.1 that provide controls for applying shadows to things directly in the Block Editor and Site Editor UI.

Ganesh touched briefly on button elements in that post. I want to pick that up and go deeper into approaches for styling buttons in WordPress block themes. Specifically, we’re going to crack open a fresh theme.json file and break down various approaches to styling buttons in the schema.

Why buttons, you ask? That’s a good question, so let’s start with that.

The different types of buttons

When we’re talking about buttons in the context of the WordPress Block Editor, we have to distinguish between two different types:

  1. Child blocks inside of the Buttons block
  2. Buttons that are nested inside other block (e.g. the Post Comments Form block)

If we add both of these blocks to a template, they have the same look by default.

But the markup is very different:

<div class="wp-block-button"> <a class="wp-block-button__link wp-element-button">Button 1</a> </div> <p class="form-submit wp-block-button"> <input name="submit" type="submit" id="submit" class="wp-block-button__link wp-element-button" value="Post Comment"> </p>

As we can see, the HTML tag names are different. It’s the common classes — .wp-block-button and .wp-element-button — that ensure consistent styling between the two buttons.

If we were writing CSS, we would target these two classes. But as we know, WordPress block themes have a different way of managing styles, and that’s through the theme.json file. Ganesh also covered this in great detail, and you’d do well giving his article a read.

So, how do we define button styles in theme.json without writing actual CSS? Let’s do it together.

Creating the base styles

theme.json is a structured set of schema written in property:value pairs. The top level properties are called “sections”, and we’re going to work with the styles section. This is where all the styling instructions go.

We’ll focus specifically on the elements in the styles. This selector targets HTML elements that are shared between blocks. This is the basic shell we’re working with:

// theme.json { "version": 2, "styles": { "elements": { // etc. } } }

So what we need to do is define a button element.

={ "version": 2, "styles": { "elements": { "button": { // etc. } } } }

That button corresponds to HTML elements that are used to mark up button elements on the front end. These buttons contain HTML tags that could be either of our two button types: a standalone component (i.e. the Button block) or a component nested within another block (e.g. the Post Comment block).

Rather than having to style each individual block, we create shared styles. Let’s go ahead and change the default background and text color for both types of buttons in our theme. There’s a color object in there that, in turn, supports background and text properties where we set the values we want:

{ "version": 2, "styles": { "elements": { "button": { "color": { "background": "#17a2b8", "text": "#ffffff" } } } } }

This changes the color of both button types:

If crack open DevTools and have a look at the CSS that WordPress generates for the buttons, we see that the .wp-element-button class adds the styles we defined in theme.json:

.wp-element-button { background-color: #17a2b8; color: #ffffff; }

Those are our default colors! Next, we want to give users visual feedback when they interact with the button.

Implementing interactive button styles

Since this is a site all about CSS, I’d bet many of you are already familiar with the interactive states of links and buttons. We can :hover the mouse cursor over them, tab them into :focus, click on them to make them :active. Heck, there’s even a :visited state to give users a visual indication that they’ve clicked this before.

Those are CSS pseudo-classes and we use them to target a link’s or button’s interactions.

In CSS, we might style a :hover state like this:

a:hover { /* Styles */ }

In theme.json, we’re going to extend our existing button declaration with these pseudo-classes.

{ "version": 2, "styles": { "elements": { "button": { "color": { "background": "#17a2b8", "text": "#ffffff" }, ":hover": { "color": { "background": "#138496" } }, ":focus": { "color": { "background": "#138496" } }, ":active": { "color": { "background": "#138496" } } } } } }

Notice the “structured” nature of this. We’re basically following an outline:

  • Elements
    • Element
      • Object
        • Property
          • Value

We now have a complete definition of our button’s default and interactive styles. But what if we want to style certain buttons that are nested in other blocks?

Styling buttons nested in individual blocks

Let’s imagine that we want all buttons to have our base styles, with one exception. We want the submit button of the Post Comment Form block to be blue. How would we achieve that?

This block is more complex than the Button block because it has more moving parts: the form, inputs, instructive text, and the button. In order to target the button in this block, we have to follow the same sort of JSON structure we did for the button element, but applied to the Post Comment Form block, which is mapped to the core/post-comments-form element:

{ "version": 2, "styles": { "elements" { "button": { // Default button styles } } "blocks": { "core/post-comments-form": { // etc. } } } }

Notice that we’re no longer working in elements anymore. Instead, we’re working inside blocks which is reserved for configuring actual blocks. Buttons, by contrast, are considered a global element since they can be nested in blocks, even though they are available as a standalone block too.

The JSON structure supports elements within elements. So, if there’s a button element in the Post Comment Form block, we can target it in the core/post-comments-form block:

{ "version": 2, "styles": { "elements" { "button": { // Default button styles } } "blocks": { "core/post-comments-form": { "elements": { "button": { "color": { "background": "#007bff" } } } } } } }

This selector means that not only are we targeting a specific block — we’re targeting a specific element that is contained in that block. Now we have a default set of button styles that are applied to all buttons in the theme, and a set of styles that apply to specific buttons that are contained in the Post Comment Form block.

The CSS generated by WordPress has a more precise selector as a result:

.wp-block-post-comments-form .wp-element-button, .wp-block-post-comments-form .wp-block-button__link { background-color: #007bff; }

And what if we want to define different interactive styles for the Post Comment Form button? It’s the same deal as the way we did it for the default styles, only those are defined inside the core/post-comments-form block:

{ "version": 2, "styles": { "elements" { "button": { // Default button styles } } "blocks": { "core/post-comments-form": { "elements": { "button": { "color": { "background": "#007bff" } ":hover": { "color": { "background": "#138496" } }, // etc. } } } } } } What about buttons that are not in blocks?

WordPress automagically generates and applies the right classes to output these button styles. But what if you use a “hybrid” WordPress theme that supports blocks and full-site editing, but also contains “classic” PHP templates? Or what if you made a custom block, or even have a legacy shortcode, that contains buttons? None of these are handled by the WordPress Style Engine!

No worries. In all of those cases, you would add the .wp-element-button class in the template, block, or shortcode markup. The styles generated by WordPress will then be applied in those instances.

And there may be some situations where you have no control over the markup. For example, some block plugin might be a little too opinionated and liberally apply its own styling. That’s where you can typically go to the “Advanced” option in the block’s settings panel and apply the class there:

Wrapping up

While writing “CSS” in theme.json might feel awkward at first, I’ve found that it becomes second nature. Like CSS, there are a limited number of properties that you can apply either broadly or very narrowly using the right selectors.

And let’s not forget the three main advantages of using theme.json:

  1. The styles are applied to buttons in both the front-end view and the block editor.
  2. Your CSS will be compatible with future WordPress updates.
  3. The generated styles work with block themes and classic themes alike — there’s no need to duplicate anything in a separate stylesheet.

If you have used theme.json styles in your projects, please share your experiences and thoughts. I look forward to reading any comments and feedback!

Styling Buttons in WordPress Block Themes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Thank You (2022 Edition)

Wed, 12/28/2022 - 5:46am

You know, this is the time of year where Chris normally publishes a big ol’ reflection of the past year. The first one was published in 2007, the same year CSS-Tricks began, and it continued all the way through 2021 without missing a beat. Having been a CSS-Tricks reader myself all those years, I’d hate to see that change.

So, here we are! 2022 was sure a heckuva year as far as transition goes. At this time last year, we were looking ahead at some goals for the upcoming year (which we’ll get to) but wound up joining DigitalOcean. That was a massive sea change (pun intended) and yet you stuck with us the whole way. It really means a lot to me that so many of you come here to read the things that I and a slew of other guest writers publish here. It just ain’t a community (or any fun) without y’all.

Thank you!

This is the last post we’re publishing this year as we hang up our Gone Fishin’ sign for the holidays. We’ll be back January 9 with a spate of content that’s fresh from the oven.

OK, this is where we start digging into the site’s analytics. That’s something we’ve always been transparent about and will continue to do. It’s not like our numbers are in some off-limits black box, and the hope is that sharing them somehow helps you and your business.

Overall traffic

Overall, Google Analytics is showing a total of 64m pageviews for the entire year. That’s wayyyyy down from last year’s 88m, which is alarming at first glance. I mean, who wants to see a 27% drop in year-over-year traffic?

But there’s good reason for that because we published wayyyy less content this year. We all know Chris was a prolific writer (and still is, of course), often spitting out multiple posts a day. It’s sorta like we lost our most productive contributor for the bulk of the year. Let’s compare the publishing activity for the last few years:

  • 2020: 1,183 articles
  • 2021: 890 articles
  • 2022: 390 articles

A 27% drop in pageviews is a lot less concerning considering we published 43% fewer articles than last year, and a whopping 67% fewer than 2020’s overall total.

Hmm, I don’t feel like I’m working 67% less…

And all of this comes with the caveat that this is just what we get from Google Analytics. In past years, Chris has compared those numbers with stats from Cloudflare (the CDN layer that sits on top of the site) and Jetpack (the plugin that connects our self-hosted WordPress site to WordPress.com’s SaaS-y features). The results are always consistently inconsistent to the extent that I’m not even bothering to look this time around. (Alright, alright maybe just Jetpack… which shows 59.9m pageviews — oddly more than 2021’s 55m total.)

Articles, by the numbers

This is what I always look forward to each year! Here are the top ten articles in 2022 that were published in 2022:

  1. 6 Creative Ideas for CSS Link Hover Effects — Harshil Patel
  2. Explain the First 10 Lines of Twitter’s Source Code to Me — Anand Chowdhary
  3. What Were the Hottest Front-End Tools in 2021? — Louis Lazaris
  4. Replace JavaScript Dialogs With the New HTML Dialog Element — Mads Stoumann
  5. Say Hello to selectmenu, a Fully Style-able select Element — Patrick Brosset
  6. Reliably Send an HTTP Request as a User Leaves a Page — Alex MacArthur
  7. grid-template-columns — Mojtaba Seyedi
  8. A Complete Guide to CSS Cascade Layers — Miriam Suzanne
  9. CSS Database Queries? Sure We Can! — Chris Coyier
  10. CSS-Tricks is joining DigitalOcean! — Chris Coyier

I’m actually surprised that last one wasn’t higher on the list. And I’m really stoked to see one from the Almanac in there, especially because Mojtaba chipped away at all of the CSS Grid properties over the past year and half and he knocked it way out of the ballpark. I thought I had a good handle on grid until I started reading all of the gold nuggets he packed into each property. There’s so much to learn in there and Mojtaba has a knack for clearly explaining complicated things. I’m hoping to update the CSS Grid guide with all that fresh information (but more on that in a bit).

I love seeing the CSS Cascade Layers guide in there, too! I had so much fun working with Miriam on it. If you didn’t know it, she’s an editor for the spec. It’s a treat (and honor, really) to host her work here and make it available for us all to bookmark and reference.

Here’s 11-20 for kicks:

  1. Animation With Basic JavaScript —Md Shuvo
  2. Flutter For Front-End Web Developers —Obumuneme Nwabude
  3. CSS Grid and Custom Shapes, Part 1 — Temani Afif
  4. Write HTML, the HTML Way (Not the XHTML Way) — Jens Oliver Meiert
  5. A Whistle-Stop Tour of 4 New CSS Color Features — Chris Coyier
  6. Cool Hover Effects That Use Background Properties — Temani Afif
  7. Let’s Create a Tiny Programming Language — Md Shuvo
  8. Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D — Temani Afif
  9. A Perfect Table of Contents With HTML + CSS — Nicholas C. Zakas
  10. CSS-Based Fingerprinting — Chris Coyier

All posts that were published in 2022 make up 4.8m pageviews, or about 7.8% of all pageviews. Our most viewed article is always the ol’ Flexbox guide which garnered 5.8m views this year. I’d love to see our new content outpace that one item, and I believe that would’ve easily happened if we’d kept up the pace of publishing. Back of the napkin math here, but we may have been around 67m pageviews if we had published 540 more articles to match last year’s number of published articles.

If we take a few steps back, then we can see the most-viewed articles from the past year, regardless of when they were published:

  1. A Complete Guide to Flexbox
  2. A Complete Guide to Grid
  3. Perfect Full Page Background Image
  4. The Shapes of CSS
  5. Media Queries for Standard Devices
  6. Using SVG
  7. How to Scale SVG
  8. CSS Triangle
  9. Gradient Borders in CSS
  10. Truncate String with Ellipsis
  11. How to use @font-face in CSS

Yep, nearly identical to last year. And the year before. And the year before. And… well, almost. “Gradient Borders in CSS” is new, bumping the box-shadow property off the list. Everything else from the four spot on merely swapped places.

Speaking of the properties in the Almanac, I wanna see what y’all referenced most this past year:

  1. ::after / ::before
  2. transition
  3. box-shadow
  4. scrollbar
  5. justify-content
  6. flex-wrap
  7. gap
  8. overflow-wrap
  9. animation
  10. white-space

One pseudo at the top and nothing but properties after that. Interesting, given that relational pseudo selector functions like :has(), :is(), and :where() are new kids on the block.


The numbers here are way too messy to draw any insightful conclusions. After moving to DigitalOcean, we had to scrub our list of 91K+ subscribers for compliance purposes and the number plummeted as a result. If you were dropped from the list, you can re-subscribe here.

The good news? We’re still doing the newsletter! We actually fired it back up in August after a five-month hiatus. We were on a weekly cadence, but are at once a month now while yours truly is authoring it. I sure hope to bump it back to a weekly publication. (I miss you, Robin!)

Site updates

It’s been mostly about keeping the ship afloat, if I’m being honest. Other than some minor tweaks and maintenance, the site is pretty much where it was at this time last year.

That will change big time in 2023. If you’ve been keeping up with our monthly Behind the CSScenes updates, then you know that we’re planning to migrate CSS-Tricks from WordPress to the same homespun CMS that DigitalOcean uses for all of its (stellar) community content.

That work kicked off a couple months ago and should be done within the first half of the year. You can bet that we’ll keep you updated along the way. Besides a fresh design and a new back-end, it should be business as usual. If you have any questions about that work and what it means for your favorite front-end publication, please do hit me up in the comments or shoot me an email.

If you haven’t seen it yet, here are a few comps that our designer, Logan Liffick, put together:

2021 goal review

Oy, I’m hesitant to even look. All the effort it’s taken to integrate with DigitalOcean and find a new rhythm dominated everybody’s time, leaving precious little to take a crack at Chris’ goals, which were:

  • More SEO focus. I’ll give us a passing grade here. The truth is that Chris and I were already digging our heels into this prior to the acquisition. We replaced the Yoast SEO plugin with RankMath, taking advantage of its in-editor tools to help us learn how to optimize our posts for search results. And to be clear: it’s less about increasing traffic for more sponsorship revenue than it is recognizing that search is the primary way readers like you find us, and making it easier for you to find what you’re looking for. That’s especially true now that we’re backed by DigitalOcean and rely on sponsorships way less than we used to.
  • &#x1f6ab; Another digital book. Swing and a miss! Well, we never actually swung in the first place, or stepped into the batter’s box for that matter. (Is this how sports analogies work?) Chris published a book of The Greatest CSS Tricks in 2020 and made it a perk of being a paid CSS-Tricks subscriber. The idea was to do another one this year, but we got rid of the paid subscriptions and opened The Greatest CSS Tricks up for everyone to enjoy, free of charge.
  • &#x1f6ab; More social media experimentation. Nope! But that might be for the best, considering where Twitter is at right this second. We might be forced to experiment in this area next year more out of neccessity than interest. Twitter has always been a drip in the proverbial bucket of CSS-Tricks traffic; so much so that investing in it feels like putting our eggs in the wrong basket, er bucket. I dunno. Part of me just wants to sit on my hands and see how things shake out before deciding on anything new or different.
2023 goal-setting

New year, new goals, right? Allow me to put a bunch of words in the team’s mouth and project what I feel are top priorities for us heading into 2023:

  • A smooth site migration. Nothing would make me happier1 than a hiccup-free move to DigitalOcean’s architecture. But c’mon, we all know something always comes up when it’s go time. This site has 7,000+ articles that have been written over 15 years, and there have been 19 versions of the site in that timespan. There are so many custom post types, custom fields, page templates, functionality plugins, integrations, and a database that’s over 6GB to move over and map to an existing system. Good thing we have a team of top-notch developers here to take it on!
  • Publish 1-2 new guides. I’d love to aim higher, actually. We went from nine new guides in 2020 to a paltry one new guide in 2021, and another one this past year: Miriam’s Complete Guide to CSS Cascade Layers. I have a list of 10 more that I’d love to write, but think we’ll set the bar super low given our recent track record. I mentioned earlier that I’d love to incorporate Mojtaba’s work in the Almanac into the existing CSS Grid guide. That’s no small amount of work and I’d count it towards the goal if we can pull it off.
  • Expand the Almanac. This is my moonshot. I’d love to see more types of documentation in there. We have pseudo-selectors and properties, which is great and always has been. But, geez, think of all the other things we could have in there: functions, at-rules, units, selectors, property values, etc. We’re only scratching the surface of what could possibly go in there! If we get even one of those, I’d be in place-self: heaven.
Thank you so, so, so much!

This is my dream job and I wouldn’t have it without readers like you. I can’t believe it’s been eight years since my very first article was published and that I’m still here, working with learning from the brightest minds in our field. I could ramble (more than I already have) on how much the CSS-Tricks community means to me, but what it really comes down to is… thank you, thank you, thank you. From the bottom of my heart, thank you.

And thanks to all the fine folks here at DigitalOcean who have made a great home for CSS-Tricks. Extra special high-fives to Haley Mills, Sydney Rossman Reich, Bradley Kouchi, Karen Digi, David Berg, Matt Crowley, Logan Liffick, and Kirstyn Kellogg for getting me personally up to speed and making me feel so welcome here. It’s a great place to be.

Forward, we go!

  1. Well, as happy as a WordPress fanboy like myself can be. ↩️

Thank You (2022 Edition) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

2022 Roundup of Web Research

Wed, 12/21/2022 - 5:03am

We’ve started making a tradition of rounding up the latest front-end research at the end of each year. We did it in 2020 and again in 2021. Reports are released throughout the year by a bunch of different companies and organizations researching everything from web design trends to developer skills to popular coding languages and so many other things.

Last year, it seemed the overarching trend was around remote work and its impact on developer productivity and workplace environments. We also saw TailwindCSS continue to explode in usage, dismal accessibility on the top 1 million websites, and a massive growth in API usage, among other findings.

So, what’s in store for this year? Read on to find out.

Table of contents HTTP Archive Annual State of the Web

What it is: A study that looks at 8.3 million websites sourced from the Chrome UX Report that analyzes how the sites were made, breaking things up into sections that include page content, user experience, content publishing, and content distribution. The CSS chapter is written by Rachel Andrew (so you know it’s good) and reviewed by folks that include Chris Lilley and CSS-Tricks contributor Jens Oliver Meiert.

What it found: The last two years has shows that CSS continues to contribute to overall page weight. This year was no exception, with CSS weight increasing 7% at the 90th percentile. That’s on par with past years.

What was the most popular class name in 2022? .active. But you also see a bunch of vendor-specific prefixes in the list, like .fa- for Font Awesome, and many for WordPress, such as .wp-, .has-large-font-size, and .has-pale-cyan-blue-background-color (phew!) which I guess is an indication of what the most popular background color is for WordPress sites.

The report also shows pixels as the most commonly used CSS unit for the font-size property. Maybe we’ll see that number trend down next year after Josh Collinsworth published why that’s not a great idea.

There’s so much more to read in here that goes beyond CSS and is worth checking out.

Read Report The WebAIM Million 2022

What it is: An evaluation of the accessibility of the top one million as evaulated by the WAVE stand-alone API. That group of sites consists of domains pulled from from the Majestic Millions listthe Alexa Top 1,000,000 web sites, and the DomCop top 10 million domains.

What it found: Well, hey, look at that! The number of distinct accessibility errors has decreased 1.1% since February 2021. That’s modest improvement, but we’re still looking at a grand total of 50,829,406 errors — or an average of 50.8 errors per site — and those are just the ones that could be detected! I think we’d all do well taking time to read Hidde de Vries’s post on how to fix the common accessibility issues outlined in the report.

Read Report State of CSS 2022 Survey

What it is: This survey pokes at CSS usage each year, surveying developers on the features they use, as well as their understanding of and satisfaction with them. Co-creator Sacha Greif openly wondered if there is too much CSS in a post he wrote right here on CSS-Tricks and a good part of that was based on the rising number of CSS features in recent years. This year’s survey garnered 8,714 responses from developers around the world.

What it found: Some really neat stuff this year. With all of the new CSS stuff that’s shipped in the past couple of years, there are still plenty of them that have yet to gain traction, whether it’s an awareness thing or the lack of browser support. For example, a whopping 46.7% of folks are only aware of 40% or less of the features covered in the survey. Container queries are a good example of a feature with awareness (58% have heard of it) but little use (12.6% have used it). The :has() selector is a glaring success with 54% knowing about it and 34.8% already using it.

There’s a lot more in here, like CSS-in-JS trends (interest is downward), accessibility features (needs more awareness), and which blogs y’all read (thanks for making CSS-Tricks #1!).

Oh, and don’t miss Lea Verou’s conclusion, which predicts that CSS nesting and color manipulation will rule in 2023. I agree. Nesting would be a game-changer that could put a dent in CSS preprocessor usage. And there are so many new color features today and in the works that are sure to impact the way we define and use colors. But I also suspect that @container becomes a much bigger deal as browser support catches on and we find ourselves writing more container queries where we may have reached for media queries in the past.

See Results Interop 2022 Dashboard

What it is: This is more of a live dashboard than a report. It analyzes data from a fixed point in time, displaying results from the web-platform-tests which are a group of test suites for many web platform specifications that run daily. The idea is that we can see which web features are broadly supported, which ones aren’t, and how browsers rank in terms of supporting them.

What it found: It’s less about what this dashboard has found than what it is currently showing. And right now, Safari is leading the pack as far as supporting what the focus areas are for 2022, which include newer features like Cascade Layers, Container Queries, the <dialog> element, Subgrid, and viewport units. Safari scores 89% in the tests, with Firefox right on its heels at 88% and Chrome/Edge not far behind at 84%.

If you look at the scores for the experimental versions of those browsers, Safari Technology Preview jumps way up to 94% while Firefox and Chrome/Edge sit at 88%. It’s really tough to make the whole “Safari is the new Internet Explorer” point these days, at least when it comes to these focus areas. There are other legitimate criticisms of it for way different reasons that are tied to iOS.

Open Dashboard Jamstack Community Survey 2022

What it is: A survey of approximately 7,000 members of the Jamstack community that provides a snapshop of who Jamstack developers are and the sorts of things they’re working on.

What it found: This survey is interesting as heck because it offers a peek into things like job titles and employment on top of Jamstack-specific stuff. For example, four out of five developers are now working remote most of the time and half of those would quit their jobs if they had to return to the office.

Here’s another neat trend: In 2021, 32% of folks referred to themselves as “full-stack developers” in 2021 while 45% called themselves “front-end developers”. That practically swapped in 2022, with 44% of respondents calling themselves “full-stack” and 32% going with “front-end”.

You’ve gotta look at the full set of results to get even more insights on what Jamstack developers are building and how they are building those things. Like, WordPress is still the most widely-used CMS at 59% of respondents (22% of which is headless WordPress), but surprisingly, Notion is quickly gaining traction in the Jamstack CMS space, at 26%. I’ve been interested in Notion as a CMS ever since Chris wrote about it 2020.

See Results 2022 State of Open Source

What it is: A survey of 2,660 developers by the Open Source Initiative and OpenLogic that tracks the usage of open source projects and contributions to them. The survey was open for six weeks and attracted responses from 15 countries.

What it found: The Open Source Initiative published their 10 takeaways from the report. Among those? 79% say they sponsor open source organizations (which might be expected from this audience). Deeper in the report, jQuery (31%) is still ranked as the top technology for app development. React (27%) clocks in at second, and Angular (26%) comes in at third.

Download PDF StackOverflow 2022 Developer Survey

What it is: A survey of more than 70,000 developers to measure how they learn, which tools they’re using, and what they want in the future.

What it found: I love this survey because it always affirms the amount of time I spend looking things up. 87% of folks spend at least 30 minutes searching for answers to problems, 25% of which spend an hour or more. The survey found that a team of 50 developers spends between 333-651 hours of time looking up answers per week.

Otherwise, JavaScript is the most used language for the tenth year in a row (but Rust is the most loved) and VS Code is the overwhelmingly popular IDE at 74%.

Survey Results GitHub’s 2022 State of the Octoverse

What it is: Straight from the horse’s mouth: “An exploration of open source software including its impact on the world and companies, plus key trends shaping software development.” It draws on GitHub activity data rather than surveying a group of respondents.

What it found: Whew, 94 million developers used GitHub in 2022! That’s a whole lot more than the 2.8 million who used it in 2012. 20.5 million newbies joined this year alone. Also, there was a 20% year-over-year growth in the number of repos hosted on GitHub, and more than 3.5 billion contributions to GitHub projects over the year. Interestingly, only 20% of all GitHub repos are public, perhaps due to private repos becoming a free feature in 2019.

Nothing new has changed on the languages front. Last year, JavaScript was the most used language and that’s true this year as well. However, TypeScript seems to have slowed down in growth after skyrocketing in popularity last year. I suspected it would jump up a few spots this year, but it’s still in fourth behind Python and Java (which is far from dead).

Read Report GitHub Copilot’s impact on developer productivity and happiness

What it is: GitHub published a report on GitHub Copilot, its AI-flavored development assistant. Is Copilot making developers’ lives easier? Is it making them more productive? Those are the sorts of things covered in this report, drawing on survey results they published in an academic paper, and external research on development productivity. There’s good qualitiative feedback in there as well.

What it found: Can you guess it? Yep, those who use Copilot feel more productive than those who do not use it. And those who use it complete tasks ~55% faster than those who do not use it for the same tasks. What it sounds like, if I’m reading this right, is that Copilot users enjoy the way it handles all the “fiddly” things for them — like auto-closing brackets.

Dave’s thoughts on Copilot seem to jive with the report’s description of Copilot being like a pair programmer with a calculator attached. Maybe not the best pair programmer in the world, but one in which your mental model shifts from writing code to checking code.

Read report The Software House State of Frontend 2022

What it is: A survey of 3,703 developers to “see the real day-to-day perspective from [front-end] professionals of all levels and backgrounds.” What makes this survey a little different is that it also polls 19 invited experts in the field, including — you guessed it — Chris Coyier weighing in on styling tools.

What it found: You know, there’s really more findings here than a mere summary can do justice. This might be the most comprehensive set of results of the whole bunch. There’s so much to grok, from frameworks, hosting, and SSG to browser technologies, code management, and testing. And that only scratches the surface. If nothing else, it’s worth clicking through to the full report just for the analysis from the invited experts.

Read Report Sparkbox 2022 Design Systems Survey

What it is: A survey all about design systems that’s focused on adoption, contributions, design, technical debt, and how design systems are used. This year’s results reflect the answers of 219 submissions, down from last year’s 376.

What it found: Last year, the survey found that 40% of folks consider their design systems “successful” or “very” successful. Those figures are less obvious in this year’s survey. But more interesting is what’s included in their systems. Sure, typography, colors, components, and layouts are common to most of them. But it’s the lack of things like developer-ready code (65%), accessibility guidelines (57%), and content guidelines (45%) that might be influencing the finding that only 65% of people who identify as design system subscribers say they get what they need from their systems.

See Results UXTools.co 2022 Design Tools Survey

What it is: The sixth edition of a survey that looks at the tooling people use for things like prototyping, UI design, design systems, and user testing. This year received 4,260 submissions.

What it found: First off, we’re dealing with a bunch of designers. 82% have “designer” somewhere in their job title, compared to a mere 6% who call themselves developers. That’s reasonable for a survey that’s all about UX tooling.

So, what tools are they using? Figma by a loooooong mile for UI design. 73% report Figma as their design software of choice, followed by a neck-and-neck race between Adode XD (6%) and Sketch (5%) for a distant second. Figma also leads the pack when it comes to basic UI protoyping and managing design systems.

Do you want to know the top tool for storing, tagging, and organizing research? It’s Notion! Funny how it comes up as both an emerging CMS and a research repository in different surveys.

See Results 2023 HackerRank Developer Skills Report

What it is: A survey of HankerRank community members and their development skills, such as the languages they use and their experience with them.

What it found: I don’t know! I tried several times to download the report, but got nothing more than a spinning wheel. The link to the report takes you to a sneak peek with some basic findings, like the top five used languages — Java, Python, SQL, C++, and JavaScript, in that order — make up the overwhelming majority of all reported languages. There’s also findings on the fastest growing languages, which is where TypeScript (182%), PHP (172%), and Go (125%) are dominant. Swift usage fell hard at -42% which is interesting considering the findings in the next survey we’re going to look at.

Read Report Tower Git Mac Dev Survey 2022

What it is: A survey of 2,506 developers (down from last year’s 4,072) working on the MacOS platform with the goal of understanding the profile of this specific developer niche.

What it found: Last year’s takeaway was the age of this crowd trending younger, suggesting a growth in Mac-related development. And lots of them really wanted to learn Swift. What changed? Not a whole lot! Most developer are still in the 30-44 age range (40.9%) even though that’s significantly down from 54.8% last year. And the largest age group (19.5%) is in the 35-39 range. They still work with JavaScript most (52.7%) and still want to learn Swift the most (28.2%).

See Results Developer Nation 2022 Q1 Pulse Report

What it is: A report is based on a global online developer survey designed, produced, and carried out by SlashData over ten weeks between December 2021 and February 2022 to measure developer trends, technology preferences, and emerging technology patterns.

What it found: I like that this report breaks down its demographics by gender. And while the result is unsurprising — there are way more men (81%) than women (17%) — it’s still a confirmation of the almost tangible dismal gender equality in the development industry as a whole.

Wanna know this survey’s top five programming languages? It’s exactly the same as HackerRank’s top five, with one exception: C# knocked JavaScript off the list. I also find it interesting that the top emerging area of interest for this group is artificial intelligence software, beating out augmented reality, robotics, cryptocurrency, and blockchain. Maybe some of these folks are the ones influencing GitHub’s Copilot research findings?

Read Report Postman 2022 State of the API Report

What it is: A survey of more than 37,000 developers (up from 28,000 last year and 13,500 in 2020!) that measures who is developing with APIs, what sort of work they’re doing with them, and how APIs are evolving.

What it found: Last year, I reported this:

67% of developers say they’ve adopted an API-first philosophy and 94% say they believe their companies will either invest more or the same in APIs in the next year. We’ll see when those results roll in next year!

The same data point this year says that number is down to 89% — and with a larger pool of survey participants. That said, Postman API requests skyrocketed from 855 million last year to 1.13 billion this year. Wow. I’d say last year’s prediction that more companies would investment in API usage this year is spot on.

The most popular APIs? That group includes known entities like Salesforce and Twitter, but welcomes Notion to the list — it’s really been a banner year for Notion according to many of the surveys in this roundup.

Get Report CodeinWP WordPress Hosting Survey 2022

What it is: A survey all about WordPress hosting that polls people who read the CodeinWP blog. They received 3,400 submissions this year. They’ve apparently been doing this survey since 2016 but it’s slipped under my radar until this year.

What it found: GoDaddy is the hosting provider of choice for this group, which was the story in 2019 and 2020 as well. But it only represents 11.8% of survey participants. The market is pretty crowded with Bluehost (8.4%), Hostinger (4.8%), and HostGator (3.4%) trailing behind. LOLzzz for GoDaddy also falling dead last in hosting satisfaction with 6.3/10 satisfaction rate. WP Engine got the top rating score (9.2/10) but that’s based on just 21 survey participants, compared to GoDaddy’s 377. Plus, the survey notes that many specified “WordPress” as their host… which could either mean they use WordPress.com or are simply confused between WordPress.com and a self-hosted WordPress site. &#x1f937;‍♂️

See Results WordPress LMS Websites: A Data Study

What it is: Let’s look at another WordPress-centric survey while we’re at it. This one is run by a group called Sell Courses Online, which is a dead giveaway that it’s focused on learning management systems (LMS) in the WordPress ecosystem.

What it found: I admit I’m super interested in this report because I teach web development in higher education and have played with a bunch of LMSs. WordPress is ripe software for for it, too, with quite a few plugin options. It’s super affordable as well, with most folks (41.3%) spending less than $50/month on their tech stack, and 76.2% spending less than $250. Most of those low-spend sites rely on a freemium-based LMS model.

And what’s included in that stack? 65.3% rely on WooCommerce for selling courses, 57.5% use Elementor as a page builder, 19% use the Astra theme (while 66% specify others), and 13.5% use Paid Memberships Pro for user accounts.

Hey, what about the actual LMS functionality? LearnDash is is the most popular LMS plugin with 34%, followed by LearnPress (31%) and Tutor LMS (19%). I’ve worked with LearnDash and love it, especially the number of add-ons to extend the functionality with more features as needed.

View Research UN E-Government Survey 2022

What it is: It’s funny, but I have a degree in Economics that I clearly haven’t used in my professional career, and there’s a bunch of stuff in here that’s way over my head. What it boils down to, if I’m understanding correctly, is that this report measures the online development of governments across United Nations member states, drawing on a composite of three different indices.

Has the United States progressed in its digital infrastructure and strategies? That’s the sort of thing this report looks at, taking in factors like what online services a country provides, how it approaches cybersecurity, efforts to increase digital proficiencies, and even how technology has been used to address crises like the COVID-19 pandemic.

The first survey was published in 2001. This 2022 survey is the eleventh edition of this biennial publication.

What it found: Honestly, you’d do better reading the press release (PDF) than relying on my uneducated insights. But at a super high level, Denmark, Finland and the Republic of Korea lead the 2022 digital government rankings, “scoring the highest when it comes to the scope and quality of online services, status of telecommunication infrastructure and existing human capacity.”

See Results LinkedIn 2022 Workplace Learning Report

What it is: The name of the report sorta says it all — LinkedIn looks at the state of the professional learning landscape in workplaces. This is the sixth edition, surveying 1,444 learning and development professionals (L&D), and 610 learners in November 2021.

A lot of this year’s report is written around the COVID-19 pandemic’s impact on learning in the workplace, like how learning has been affected by layoffs and remote work arrangements.

What it found: Learning continues despite The Great Reshuffle/Great Resignation or whatever you want to call the relatively new trend of quitting jobs and changing careers. For example, 46% of L&D professionals say there is a wider technological skill gap on their teams, and 49% say execs are concerned that employees do not have the right skills to meet business strategies. That suggests the post-pandemic technological landscape has created higher expectations as far as employees having relevant technical skills, particularly when it comes to what’s needed for successful remote work.

That, in turn, has led to a rise in demand for workplace learning programs and profressionals. L&D professionals are in higher demand and make more money than they did before. And only 8% expect their L&D budget to decrease in the coming year.

What sorts of learning programs have top priority? Diversity, equity, and inclusion (45%), leading through change programs (42%), in-person training (41%, up from 25%!), upskilling and reskilling (41%), and digital fluency (30%). A lot of soft skills in there!

View Report UpWork: The Great Work Teardown

What it is: While we’re on the topic of changing workplace environments, let’s look at this one that investigates the workplace trends that are changing perspectives on when, where, and how people work — and how businesses are adapting to those changing perspectives.

What it found: The stats are super interesting, but I couldn’t find any information on the methodology it used to get them. Like 50% of businesses have reported higher turnover compared to pre-pandemic times, 38% plan to spend more on independent remote freelancers, and 37% are fully remote today with 28% expecting to go fully remote in a year. What’s going to happen to all those empty office buildings?!

On the employee side of things, 61% say they are more productive when they work remote. 45% of business also report an increase in productivity as a result of remote work and a whopping 63% reduction in unscheduled work absences.

There are other interesting stats on how other things are changing, like traditional work hours, where people choose to work, and the perception of workplace culture.

Get Report UpWork 2022 Labor Market Trends and Insights

What it is: Another one from UpWork! This time it’s looking at the overall labor market. And there’s a documented methodology this time, saying that numbers are based on survey results of 1,000 hiring professionals from a third-party as well as findings from a separate study from a separate firm that surveyed 6,000 working professionals.

What it found: Well, UpWork’s “Great Work Teardown” report found that there’s big growth in business relying on remote freelancers. This report confirms that 78% of hiring pros saying they’ve used remote freelancers and 52% saying they are using more of them today than they have in previous years.

Get this: 60% of managers at mid-sized companies report higher turnover since pre-pandemic levels, while only 25% of small companies report the same. And roughly 45% of all hiring managers say they plan to combat turnover by offering more learning programs, confirming LinkedIn’s workplace learning report.

And, hey, if you’re looking for a higher salary or more perks, this might be the time to to strike because around 50% of managers are considering higher salaries and bigger benefit packages to retain staff.

See Trends Reblaze 2022 State of Web Security Survey

What it is: This survey is new to the collection! It asked 300 web security pros what they consider to be their biggest online threats and how they plan on defending against them.

What it found: The most common attacks were DDoS, with half of the survey’s participants saying they’ve dealt with them in the past year. Next up is SQL injections (38%) and ransomware (29%), where ransomware is considered the most severe threat. (The report also cites a U.S. Treasury finding that U.S. firms paid out $590 million in ransomware attacks in the first half of 2021 alone. Geez.)

Also neat: 90% of participants say they are using a public cloud, making cloud-based security more of a thing. (AWS tops the list at 67%. DigitalOcean (4%), the home for CSS-Tricks, is sandwiched between Oracle (7%) and IBM (3%) as an interesting aside.)

API security is tops as far as priority goes. With Postman’s State of the API report showing a year-over-year increase in API requests that goes from 855 million last year to 1.13 billion this year, it’s clear why that is.

See Results

(Linking directly to the PDF to save you the registration effort.)

Trend Micro 2022 Midyear Cybersecurity Report

What it is: Let’s keep talking cybersecurity. This report polls 6,297 IT security decision-makers from 29 countries about their thoughts on the cybersecurity risks they face.

What it found: A good chunk of folks (62%) say they have blindspots in their defense strategies and 43% belive that that their exposure to threats is out of control. That’s in line with the Reblaze survey above that reported 50% of folks saying they have no certainty as far as how many bots account for overall traffic. This report notes that 37% of participants cite cloud assets as the area they have the least insight into.

The report gets into a bunch of specific attacks that I had no idea were even a thing. It’s unnerving how attacks seem to get smarter and smarter each year while the businesses continue to increase their exposure to them. This report provides a lot of excellent detail on those threats, including a section devoted to cybersecurity efforts in the Russia-Ukraine conflict.

Get Report 1Password: The realities of parenting and growing up online

What it is: Let’s heap more on the cybersecurity research pile will this report from the folks behind the 1Password app. This one hits pretty close to home for me because it looks at parenting in the always-online era, which is something pinned to the back of my mind since I have two young daughters who love their screens.

1Password teamed up with Malwarebytes to produce this report, which is based on a survey of 1,000 parents and 1,000 children that were born between 1997-2009 that was prepared by another firm, Method Research. The data is fresh having been collected in August this year.

What it found: A bunch of stats I wish I could unsee. Like 74% of parents think they’re keeping their kids safe, while only 51% agree. And 74% of kids have ways to workaround being monitored by their parents (where 9% claim to have a secret device their parents don’t know about). Cyberbullying is the top concern for both parents (73%) and kids (66%).

Parents also need to be more responsible. 73% of kids wish their parents would ask for permission to post photos of them while only 34% of parents actually ask (and — eek! — 39% don’t believe they need to ask permission). The importance here is that 11% of kids say they’ve been stalked or bullied because of something posted by their parents, and 12% report being harmed in some way, whether its hacked accounts, stolen identities, or tarnished credit cores.

Download PDF The Eclipse Foundation 2022 IoT & Edge Developer Survey

What it is: A survey of 910 global developers, committers, architects, and decision-makers that took place between April and June 2022 to spot trends in the Internet of Things (IoT) space. You know, like that smart fridge in your kitchen and voice-controlled curtains that shade your living room. That and more serious stuff like trends in artificial intelligence and edge computing.

What it found: Last year, I called Microsoft’s IoT findings “mostly nice trivia for cocktail chatter.” This report is a lot more granular and is probably most helpful for those working in the space, as there’s so much information on the stacks that developers use and overarching concerns about them. I mean, I don’t particularly care that “64-bit ARM architectures are gaining ground with gateway and edge node suppliers.” But I bet that’s super important to some of you reading this.

Download PDF

(Linking directly to the PDF to save you the registration effort.)

CampaignMonitor Ultimate Email Marketing Benchmarks for 2022

What it is: A study of benchmarks related to email marketing based on 100 billion emails delivered on CampaignMonitor’s platform in 2021. It looks at things like open and click rates, and breaks them down by industry to help folks get the most out of their email marketing campaigns and know how to gauge success.

What it found: It’s probably better for you to see their table of results by industry rather than having me regurgitate the results of all 19 industries they identified. But on a global level, a 21.5% open rate is quite average across all industries, as is a 2.3% click-through rate. It appears Monday produces the highest open rate (22% on average) while Sunday produces the lowest (20.3%), so not a whole lot of variance there. Same deal with click-through rates, where Tuesday is highest (2.4%) and Saturday and Sunday share the lowest rate (2.1%). Again, not a lot of difference but it could be helpful knowing this stuff if you’re trying to milk every last drop out of a campaign.

See Benchmarks Wrapping up

We looked at 27 different reports on front-end and front-end-adjacent topics! That’s more than the 25 we covered last year and the fitting number of 20 we looked at in 2020.

If there’s one BIG takeaway from all these takeaways, it’s to remember this is all in good fun. Many of the studies lack the scientific methods we’d want to base decisions on, and the sample sizes are far too small to accurately reflect reality. But they sure are interesting, right?!

Some reports are annual, some are one-off, and others seemingly happen whenever the heck they wanna do it. So, if I missed any from previous years, it’s probably because they aren’t annual or just aren’t available as I’m writing this. As always, let me know if there’s a report I missed and I’ll try to work it in.

There are more reports on the way! In fact, you can take the 2022 State of JavaScript survey and the 2022 WordPress Annual Survey as this is being written. Have at ’em!

2022 Roundup of Web Research originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

©2003 - Present Akamai Design & Development.