CSS-Tricks: Recent Episodes

None

Tips, Tricks, and Techniques on using Cascading Style Sheets.

View Details

In a previous article, I showed you how to refactor the Resize Observer API into something way simpler to use:

// From thisconst observer = new ResizeObserver(observerFn)function observerFn (entries) { for (let entry of entries) { // Do something with each entry }}const element = document.querySelector('#some-element')observer.observe(element);

// To this const node = document.querySelector('#some-element')const obs = resizeObserver(node, { callback({ entry }) { // Do something with each entry }}) Today, we’re going to do the same for MutationObserver and IntersectionObserver.

Refactoring Mutation ObserverMutationObserver has almost the same API as that of ResizeObserver. So we can practically copy-paste the entire chunk of code we wrote for resizeObserver to mutationObserver.

export function mutationObserver(node, options = {}) { const observer = new MutationObserver(observerFn) const { callback, ...opts } = options observer.observe(node, opts) function observerFn(entries) { for (const entry of entries) { // Callback pattern if (options.callback) options.callback({ entry, entries, observer }) // Event listener pattern else { node.dispatchEvent( new CustomEvent('mutate', { detail: { entry, entries, observer }, }) ) } } }} You can now use mutationObserver with the callback pattern or event listener pattern.

const node = document.querySelector('.some-element')// Callback pattern const obs = mutationObserver(node, { callback ({ entry, entries }) { // Do what you want with each entry }})// Event listener patternnode.addEventListener('mutate', event => { const { entry } = event.detail // Do what you want with each entry}) Much easier!

Disconnecting the observerUnlike ResizeObserver who has two methods to stop observing elements, MutationObserver only has one, the disconnect method.

export function mutationObserver(node, options = {}) { // ... return { disconnect() { observer.disconnect() } }} But, MutationObserver has a takeRecords method that lets you get unprocessed records before you disconnect. Since we should takeRecords before we disconnect, let’s use it inside disconnect.

To create a complete API, we can return this method as well.

export function mutationObserver(node, options = {}) { // ... return { // ... disconnect() { const records = observer.takeRecords() observer.disconnect() if (records.length > 0) observerFn(records) } }} Now we can disconnect our mutation observer easily with disconnect.

const node = document.querySelector('.some-element')const obs = mutationObserver(/* ... */)obs.disconnect() MutationObserver’s observe optionsIn case you were wondering, MutationObserver’s observe method can take in 7 options. Each one of them determines what to observe, and they all default to false.

  • subtree: Monitors the entire subtree of nodes
  • childList: Monitors for addition or removal children elements. If subtree is true, this monitors all descendant elements.
  • attributes: Monitors for a change of attributes
  • attributeFilter: Array of specific attributes to monitor
  • attributeOldValue: Whether to record the previous attribute value if it was changed
  • characterData: Monitors for change in character data
  • characterDataOldValue: Whether to record the previous character data value

Refactoring Intersection ObserverThe API for IntersectionObserver is similar to other observers. Again, you have to:

  1. Create a new observer: with the new keyword. This observer takes in an observer function to execute.
  2. Do something with the observed changes: This is done via the observer function that is passed into the observer.
  3. Observe a specific element: By using the observe method.
  4. (Optionally) unobserve the element: By using the unobserve or disconnect method (depending on which Observer you’re using).

But IntersectionObserver requires you to pass the options in Step 1 (instead of Step 3). So here’s the code to use the IntersectionObserver API.

// Step 1: Create a new observer and pass in relevant optionsconst options = {/*...*/}const observer = new IntersectionObserver(observerFn, options)// Step 2: Do something with the observed changesfunction observerFn (entries) { for (const entry of entries) { // Do something with entry }}// Step 3: Observe the elementconst element = document.querySelector('#some-element')observer.observe(element)// Step 4 (optional): Disconnect the observer when we're done using itobserver.disconnect(element) Since the code is similar, we can also copy-paste the code we wrote for mutationObserver into intersectionObserver. When doing so, we have to remember to pass the options into IntersectionObserver and not the observe method.

export function mutationObserver(node, options = {}) { const { callback, ...opts } = options const observer = new MutationObserver(observerFn, opts) observer.observe(node) function observerFn(entries) { for (const entry of entries) { // Callback pattern if (options.callback) options.callback({ entry, entries, observer }) // Event listener pattern else { node.dispatchEvent( new CustomEvent('intersect', { detail: { entry, entries, observer }, }) ) } } }} Now we can use intersectionObserver with the same easy-to-use API:

const node = document.querySelector('.some-element')// Callback pattern const obs = intersectionObserver(node, { callback ({ entry, entries }) { // Do what you want with each entry }})// Event listener patternnode.addEventListener('intersect', event => { const { entry } = event.detail // Do what you want with each entry}) Disconnecting the Intersection ObserverIntersectionObserver‘s methods are a union of both resizeObserver and mutationObserver. It has four methods:

  • observe: observe an element
  • unobserve: stops observing one element
  • disconnect: stops observing all elements
  • takeRecords: gets unprocessed records

So, we can combine the methods we’ve written in resizeObserver and mutationObserver for this one:

export function intersectionObserver(node, options = {}) { // ... return { unobserve(node) { observer.unobserve(node) }, disconnect() { // Take records before disconnecting. const records = observer.takeRecords() observer.disconnect() if (records.length > 0) observerFn(records) }, takeRecords() { return observer.takeRecords() }, }} Now we can stop observing with the unobserve or disconnect method.

const node = document.querySelector('.some-element')const obs = intersectionObserver(node, /*...*/)// Disconnect the observerobs.disconnect() IntersectionObserver optionsIn case you were wondering, IntersectionObserver takes in three options:

  • root: The element used to check if observed elements are visible
  • rootMargin: Lets you specify an offset amount from the edges of the root
  • threshold: Determines when to log an observer entry

Here’s an article to help you understand IntersectionObserver options.

Using this in practice via Splendid LabzSplendid Labz has a utils library that contains resizeObserver, mutationObserver and IntersectionObserver.

You can use them if you don’t want to copy-paste the above snippets into every project.

import { resizeObserver, intersectionObserver, mutationObserver } from 'splendidlabz/utils/dom'const mode = document.querySelector(‘some-element’)const resizeObs = resizeObserver(node, /* ... */)const intersectObs = intersectionObserver(node, /* ... */)const mutateObs = mutationObserver(node, /* ... */) Aside from the code we’ve written together above (and in the previous article), each observer method in Splendid Labz is capable of letting you observe and stop observing multiple elements at once (except mutationObserver because it doesn’t have a unobserve method)

const items = document.querySelectorAll('.elements')const obs = resizeObserver(items, { callback ({ entry, entries }) { /* Do what you want here */ }})// Unobserves two items at onceconst subset = [items[0], items[1]]obs.unobserve(subset) So it might be just a tad easier to use the functions I’ve already created for you. 😉

Shameless Plug: Splendid Labz contains a ton of useful utilities — for CSS, JavaScript, Astro, and Svelte — that I have created over the last few years.

I’ve parked them all in into Splendid Labz, so I no longer need to scour the internet for useful functions for most of my web projects. If you take a look, you might just enjoy what I’ve complied!

(I’m still making the docs at the time of writing so it can seem relatively empty. Check back every now and then!)

Learning to refactor stuffIf you love the way I explained how to refactor the observer APIs, you may find how I teach JavaScript interesting.

In my JavaScript course, you’ll learn to build 20 real life components. We’ll start off simple, add features, and refactor along the way.

Refactoring is such an important skill to learn — and in here, I make sure you got cement it into your brain.

That’s it! Hope you had fun reading this piece!


A Better API for the Intersection and Mutation Observers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I have had the opportunity to edit over a lot of the new color entries coming to the CSS-Tricks Almanac. We’ve already published several with more on the way, including a complete guide on color functions:

  • color()
  • hsl()
  • lab()
  • lch()
  • oklab()
  • oklch()
  • rgb()

And I must admit: I didn’t know a lot about color in CSS (I still used rgb(), which apparently isn’t what cool people do anymore), so it has been a fun learning experience. One of the things I noticed while trying to keep up with all this new information was how long the glossary of color goes, especially the “color” concepts. There are “color spaces,” “color models,” “color gamuts,” and basically a “color” something for everything.

They are all somewhat related, and it can get confusing as you dig into using color in CSS, especially the new color functions that have been shipped lately, like contrast-color() and color-mix(). Hence, I wanted to make the glossary I wish I had when I was hearing for the first time about each concept, and that anyone can check whenever they forget what a specific “color” thing is.

As a disclaimer, I am not trying to explain color, or specifically, color reproduction, in this post; that would probably be impossible for a mortal like me. Instead, I want to give you a big enough picture for some technicalities behind color in CSS, such that you feel confident using functions like lab() or oklch() while also understanding what makes them special.

What’s a color?Let’s slow down first. In order to understand everything in color, we first need to understand the color in everything.

While it’s useful to think about an object being a certain color (watch out for the red car, or cut the white cable!), color isn’t a physical property of objects, or even a tangible thing. Yes, we can characterize light as the main cause of color1, but it isn’t until visible light enters our eyes and is interpreted by our brains that we perceive a color. As said by Elle Stone:

Light waves are out there in the world, but color happens in the interaction between light waves and the eye, brain, and mind.

Even if color isn’t a physical thing, we still want to replicate it as reliably as possible, especially in the digital era. If we take a photo of a beautiful bouquet of lilies (like the one on my desk) and then display it on a screen, we expect to see the same colors in both the image and reality. However, “reality” here is a misleading term since, once again, the reality of color depends on the viewer. To solve this, we need to understand how light wavelengths (something measurable and replicable) create different color responses in viewers (something not so measurable).

Luckily, this task was already carried out 95 years ago by the International Commission on Illumination (CIE, by its French name). I wish I could get into the details of the experiment, but we haven’t gotten into our first color thingie yet. What’s important is that from these measurements, the CIE was able to map all the colors visible to the average human (in the experiment) to light wavelengths and describe them with only three values.

Initially, those three primary values corresponded to the red, green, and blue wavelengths used in the experiment, and they made up the CIERGB Color Space, but researchers noticed that some colors required a negative wavelength2 to represent a visible color. To avoid that, a series of transformations were performed on the original CIERGB and the resulting color space was called CIEXYZ.

This new color space also has three values, X and Z represent the chromaticity of a color, while Y represents its luminance. Since it has three axes, it makes a 3D shape, but if we slice it such that its luminance is the same, we get all the visible colors for a given luminance in a figure you have probably seen before.

This is called the xy chromaticity diagram and holds all the colors visible by the average human eye (based on the average viewer in the CIE 1931 experiment). Colors inside the shape are considered real, while those outside are deemed imaginary.

Color SpacesThe purpose of the last explanation was to reach the CIEXYZ Color Space concept, but what exactly is a “color space”? And why is the CIEXYZ Color Space so important?

The CIEXYZ Color Space is a mapping from all the colors visible by the average human eye into a 3D coordinate system, so we only need three values to define a color. Then, a color space can be thought of as a general mapping of color, with no need to include every visible color, and it is usually defined through three values as well.

RGB Color SpacesThe most well-known color spaces are the RGB color spaces (note the plural). As you may guess from the name, here we only need the amount of red, green, and blue to describe a color. And to describe an RGB color space, we only need to define its “reddest”, “greenest”, and “bluest” values3. If we use coordinates going from 0 to 1 to define a color in the RGB color space, then:

  • (1, 0, 0) means the reddest color.
  • (0, 1, 0) means the greenest color.
  • (0, 0, 1) means the bluest color.

However, “reddest”, “bluest”, and “greenest” are only arbitrary descriptions of color. What makes a color the “bluest” is up to each person. For example, which of the following colors do you think is the bluest?

As you can guess, something like “bluest” is an appalling description. Luckily, we just have to look back at the CIEXYZ color space — it’s pretty useful! Here, we can define what we consider the reddest, greenest, and bluest colors just as coordinates inside the xy chromaticity diagram. That’s all it takes to create an RGB color space, and why there are so many!

Credit: Elle StoneIn CSS, the most used color space is the standard RGB (sRGB) color space, which, as you can see in the last image, leaves a lot of colors out. However, in CSS, we can use modern RGB color spaces with a lot more colors through the color() function, such as display-p3, prophoto-rgb, and rec2020.

Credit: Chrome Developer TeamNotice how the ProPhoto RGB color space goes out of the visible color. This is okay. Colors outside are clamped; they aren’t new or invisible colors.

In CSS, besides sRGB, we have two more color spaces: the CIELAB color space and the Oklab color space. Luckily, once we understood what the CIEXYZ color space is, then these two should be simpler to understand. Let’s dig into that next.

CIELAB and Oklab Color SpacesAs we saw before, the sRGB color space lacks many of the colors visible by the average human eye. And as modern screens got better at displaying more colors, CSS needed to adopt newer color spaces to fully take advantage of those newer displays. That wasn’t the only problem with sRGB — it also lacks perceptual uniformity, meaning that changes in the color’s chromaticity also change its perceived lightness. Check, for example, this demo by Adam Argyle:

CodePen Embed FallbackCreated in 1976 by the CIE, CIELAB, derived from CIEXYZ, also encompasses all the colors visible by the human eye. It works with three coordinates: L for perceptual lightness, a for the amount of red-green, and b* for the amount of yellow-blue in the color.

Credit: Linshang TechnologyIt has a way better perceptual uniformity than sRGB, but it still isn’t completely uniform, especially in gradients involving blue. For example, in the following white-to-blue gradient, CIELAB shifts towards purple.

Image Credits to Björn OttossonAs a final improvement, Björn Ottosson came up with the Oklab color space, which also holds all colors visible by the human eye while keeping a better perceptual uniformity. Oklab also uses the three L*a*b* coordinates. Thanks to all these improvements, it is the color space I try to use the most lately.

Color ModelsWhen I was learning about these concepts, my biggest challenge after understanding color spaces was not getting them confused with color models and color gamuts. These two concepts, while complementary and closely related to color spaces, aren’t the same, so they are a common pitfall when learning about color.

A color model refers to the mathematical description of color through tuples of numbers, usually involving three numbers, but these values don’t give us an exact color until we pair them with a color space. For example, you know that in the RGB color model, we define color through three values: red, green, and blue. However, it isn’t until we match it to an RGB color space (e.g., sRGB with display-p3) that we have a color. In this sense, a color space can have several color models, like sRGB, which uses RGB, HSL, and HWB. At the same time, a color model can be used in several color spaces.

I found plenty of articles and tutorials where “color spaces” and “color models” were used interchangeably. And some places were they had a different definition of color spaces and models than the one provided here. For example, Chrome’s High definition CSS color guide defines CSS’s RGB and HSL as different color spaces, while MDN’s Color Space entry does define RGB and HSL as part of the sRGB color space.

Personally, in CSS, I find it easier to understand the idea of RGB, HSL and HWB as different models to access the sRGB color space.

Color GamutsA color gamut is more straightforward to explain. You may have noticed how we have talked about a color space having more colors than another, but it would be more correct to say it has a “wider” gamut, since a color gamut is the range of colors available in a color space. However, a color gamut isn’t only restricted by color space boundaries, but also by physical limitations. For example, an older screen may decrease the color gamut since it isn’t able to display each color available in a given color space. In this case where a color can’t be represented (due to physical limitation or being outside the color space itself), it’s said to be “out of gamut”.

Color FunctionsIn CSS, the only color space available used to be sRGB. Nowadays, we can work with a lot of modern color spaces through their respective color functions. As a quick reference, each of the color spaces in CSS uses the following functions:

  • sRGB: We can work in sRGB using the ol’ hexadecimal notation, named colors, and the rgb(), rgba(), hsl(), hsla() and hwb() functions.
  • CIELAB: Here we have the lab() for Cartesian coordinates and lch() for polar coordinates.
  • Oklab: Similar to CIELAB, we have oklab() for Cartesian coordinates and oklch() for polar coordinates.
  • More through the color() and color-mix(). Outside these three color spaces, we can use many more using the color() and color-mix() functions. Specifically, we can use the RGB color spaces: rgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020 and the XYZ color space: xyz, xyz-d50, or xyz-d65.

TL;DR1. Color spaces are a mapping between available colors and a coordinate system. In CSS, we have three main color spaces: sRGB, CIELAB, and Oklab, but many more are accessible through the color() function. 2. Color models define color with tuples of numbers, but they don’t give us information about the actual color until we pair them with a color space. For example, the RGB model doesn’t mean anything until we assign it an RGB color space. 3. Most of the time, we want to talk about how many colors a color space holds, so we use the term color gamut for the task. However, a color gamut is also tied to the physical limitations of a camera/display. A color may be out-of-gamut, meaning it can’t be represented in a given color space. 4. In CSS, we can access all these color spaces through color functions, of which there are many. 5. The CIEXYZ color space is extremely useful to define other color spaces, describe their gamuts, and convert between them.

References* Completely Painless Programmer’s Guide to XYZ, RGB, ICC, xyY, and TRCs (Elle Stone) * Color Spaces (Bartosz Ciechanowski) * The CIE XYZ and xyY Color Spaces(Douglas A. Kerr) * From personal project to industry standard (Björn Ottosson) * High definition CSS color guide (Adam Argyle) * Color Spaces: Explained from the Ground Up (Video Tech Explained) * Color Space (MDN) * What Makes a Color Space Well Behaved? (Elle Stone)

Footnotes1 Light is the main cause of color, but color can be created by things other than light. For example, rubbing your closed eyes mechanically stimulates your retina, creating color in what’s called phosphene. ⤴️

2 If negative light also makes you scratch your head, and for more info on how the CIEXYZ color space was created, I highly recommend Douglas A. Kerr The CIE XYZ and xyY Color Spaces paper. ⤴️

3 We also need to define the darkest dark color (“black”) and the lightest light color (“white”). However, for well-behaved color spaces, these two can be abstracted from the reddest, blues, and greenest colors. ⤴️


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

View Details

If you asked me a few months ago, “What does it take for a website to stand out?” I may have said fancy animations, creative layouts, cool interactions, and maybe just the general aesthetics, without pointing out something in particular. If you ask me now, after working on color for the better part of the year, I can confidently say it’s all color. Among all the aspects that make a design, a good color system will make it as beautiful as possible.

However, color in CSS can be a bit hard to fully understand since there are many ways to set the same color, and sometimes they even look the same, but underneath are completely different technologies. That’s why, in this guide, we will walk through all the ways you can set up colors in CSS and all the color-related properties out there!


Colors are in everythingThey are in your phone, in what your eye sees, and on any screen you look at; they essentially capture everything. Design-wise, I see the amazing use of colors on sites listed over at awwwards.com, and I’m always in awe.

Not all color is the same. In fact, similar colors can live in different worlds, known as color spaces. Take for example, sRGB, the color space used on the web for the better part of its existence and hence the most known. While it’s the most used, there are many colors that are simply missing in sRGB that new color spaces like CIELAB and Oklab bring, and they cover a wider range of colors sRGB could only dream of, but don’t let me get ahead of myself.


What’s a color space?A color space is the way we arrange and represent colors that exist within a device, like printers and monitors. We have different types of color spaces that exist in media (Rec2020, Adobe RGB, etc), but not all of them are covered in CSS. Luckily, the ones we have are sufficient to produce all the awesome and beautiful colors we need. In this guide, we will be diving into the three main color spaces available in CSS: sRGB, CIELAB, and OkLab.


The sRGB Color SpaceThe sRGB is one of the first color spaces we learn. Inside, there are three color functions, which are essentially notations to define a color: rgb(), hsl(), and hwb().

sRGB has been a standard color space for the web since 1996. However, it’s closer to how old computers represented color, rather than how humans understand it, so it had some problems like not being able to capture the full gamut of modern screens. Still, many modern applications and websites use sRGB, so even though it is the “old way” of doing things, it is still widely accepted and used today.

The rgb() function rgb() uses three values, r, g, and b which specifies the redness, greenness, and blueness of the color you want.

All three values are non-negative, and they go from 0 to 255.

.element { color: rgb(245 123 151);} It also has an optional value (the alpha value) preceded by a forward slash. It determines the level of opacity for the color, which goes from 0 (or 0%) for a completely transparent color, to 1 (or 100%) for a fully opaque one.

.element { color: rgb(245 123 151 / 20%);} There are two ways you can write inside rgb(). Either using the legacy syntax that separates the three values with commas or the modern syntax that separates each with spaces.

You want to combine the two syntax formats, yes? That’s a no-no. It won’t even work.

/* This would not work */.element { color: rgb(225, 245, 200 / 0.5);}/* Neither will this */.element { color: rgb(225 245 200, 0.5);}/* Or this */.element { color: rgb(225, 245 200 / 0.5);} But, following one consistent format will do the trick, so do that instead. Either you’re so used to the old syntax and it’s hard for you to move on, continue to use the legacy syntax, or you’re one who’s willing to try and stick to something new, use the modern syntax.

/* Valid (Modern syntax) */.element { color: rgb(245 245 255 / 0.5);}/* Valid (Legacy syntax) */.element { color: rgb(245, 245, 255, 0.5);} CodePen Embed Fallback The rgba() function rgba() is essentially the same as rgb() with an extra alpha value used for transparency.

In terms of syntax, the rgba() function can be written in two ways:

  • Comma-separated and without percentages
  • Space-separated, with the alpha value written after a forward slash (/)

.element { color: rgba(100, 50, 0, 0.5);}.element { color: rgba(100 50 0 / 0.5);} So, what’s the difference between rgba() and rgb()?

Breaking news! There is no difference. Initially, only rgba() could set the alpha value for opacity, but in recent years, rgb() now supports transparency using the forward slash (/) before the alpha value.

rgb() also supports legacy syntax (commas) and modern syntax (spaces), so there’s practically no reason to use rgba() anymore; it’s even noted as a CSS mistake by folks at W3C.

In a nutshell, rgb() and rgba() are the same, so just use rgb().

/* This works */.element-1 { color: rgba(250 30 45 / 0.8);}/* And this works too, so why not just use this? */.element-2 { color: rgb(250 30 45 / 0.8);} The hexadecimal notation The hexadecimal CSS color code is a 3, 4, 6, or 8 (being the maximum) digit code for colors in sRGB. It’s basically a shorter way of writing rgb(). The hexadecimal color (or hex color) begins with a hash token (#) and then a hexadecimal number, which means it goes from 0 to 9 and then skips to letters a to f (a being 10, b being 11, and so on, up to f for 15).

In the hexadecimal color system, the 6-digit style is done in pairs. Each pair represents red (RR), blue (BB), and green (GG).

Each value in the pair can go from 00 to FF, which it’s equivalent to 255 in rgb().

Notice how I used caps for the letters (F) and not lowercase letters like I did previously? Well, that’s because hexadecimals are not case-sensitive in CSS, so you don’t have to worry about uppercase or lowercase letters when dealing with hexadecimal colors.

  • 3-digit hexadecimal. The 3-digit hexadecimal system is a shorter way of writing the 6-digit hexadecimal system, where each value represents the color’s redness, greenness, and blueness, respectively

.element { color: #abc;} In reality, each value in the 3-digit system is duplicated and then translated to a visible color

.element { color: #abc; /* Equals #AABBCC */} BUT, this severely limits the colors you can set. What if I want to target the color 213 in the red space, or how would I get a blue of value 103? It’s impossible. That’s why you can only get a total number of 4,096 colors here as opposed to the 17 million in the 6-digit notation. Still, if you want a fast way of getting a certain color in hexadecimal without having to worry about the millions of other colors, use the 3-digit notation.

  • 4-digit hexadecimal. This is similar to the 3-digit hexadecimal notation except it includes the optional alpha value for opacity. It’s a shorter way of writing the 8-digit hexadecimal which also means that all values here are repeated once during color translation.

.element { color: #ABCD2;} For the alpha value, 0 represents 00 (a fully transparent color) and F represents FF (a fully opaque color).

.element { color: #abcd; /* Same as #AABBCCDD */} * 6-digit hexadecimal. The 6-digit hexadecimal system just specifies a hexadecimal color’s redness, blueness, and greenness without its alpha value for color opacity.

.element { color: #abcdef;} * 8-digit hexadecimal. This 8-digit hexadecimal system specifies hexadecimal color’s redness, blueness, greenness, and its alpha value for color opacity. Basically, it is complete for color control in sRGB.

.element { color: #faded101;} The hsl() function Both hsl() and rgb() live in the sRGB space, but they access colors differently. And while the consensus is that hsl() is far more intuitive than rgb(), it all boils down to your preference.

hsl() takes three values: h, s, and l, which set its hue, saturation, and lightness, respectively.

  • The hue sets the base color and represents a direction in the color wheel, so it’s written in angles from 0deg to 360deg.
  • The saturation sets how much of the base color is present and goes from 0 (or 0%) to 100 (or 100%).
  • The lightness represents how close to white or black the color gets.

One cool thing: the hue angle goes from (0deg360deg), but we might as well use negative angles or angles above 360deg, and they will circle back to the right hue. Especially useful for infinite color animation. Pretty neat, right?

Plus, you can easily get a complementary color from the opposite angle (i.e., adding 180deg to the current hue) on the color wheel.

/* Current color */.element { color: hsl(120deg 40 60 / 0.8);}/* Complementary color */.element { color: hsl(300deg 40 60 / 0.8);} You want to combine the two syntax formats like in rgb(), yes? That’s also a no-no. It won’t work.

/* This would not work */.element { color: hsl(130deg, 50, 20 / 0.5);}/* Neither will this */.element { color: hsl(130deg 50 20, 0.5);}/* Or this */.element { color: hsl(130deg 50, 20 / 0.5);} Instead, stick to one of the syntaxes, like in rgb():

/* Valid (Modern syntax) */ .element { color: hsl(130deg 50 20 / 0.5);}/* Valid (Modern syntax) */ .element { color: hsl(130deg, 50, 20, 0.5);} CodePen Embed Fallback The hsla() function hsla() is essentially the same with hsl(). It uses three values to represent its color’s hue (h), saturation (s), and lightness (l), and yes (again), an alpha value for transparency (a). We can write hsla() in two different ways:

  • Comma separated
  • Space separated, with the alpha value written after a forward slash (/)

.element { color: hsla(120deg, 100%, 50%, 0.5);}.element { color: hsla(120deg 100% 50% / 0.5);} So, what’s the difference between hsla() and hsl()?

Breaking news (again)! They’re the same. hsl() and hsla() both:

  • Support legacy and modern syntax
  • Have the power to increase or reduce color opacity

So, why does hsla() still exist? Well, apart from being one of the mistakes of CSS, many applications on the web still use hsla() since there wasn’t a way to set opacity with hsl() when it was first conceived.

My advice: just use hsl(). It’s the same as hsla() but less to write.

/* This works */.element-1 { color: hsla(120deg 80 90 / 0.8);}/* And this works too, so why not just use this? */.element-2 { color: hsl(120deg 80 90 / 0.8);} The hwb() function hwb() also uses hue for its first value, but instead takes two values for whiteness and blackness to determine how your colors will come out (and yes, it also does have an optional transparency value, a, just like rgb() and hsl()).

.element { color: hwb(80deg 20 50 / 0.5);} * The first value h is the same as the hue angle in hsl(), which represents the color position in the color wheel from 0 (or 0deg) to 360 (or 360deg). * The second value, w, represents the whiteness in the color. It ranges from 0/0% (no white) to 100/100% (full white if b is 0). * The third value, b, represents the blackness in the color. It ranges from 0/0% (no black) to 100/100% (fully black if w is 0). * The final (optional) value is the alpha value, a, for the color’s opacity, preceded by a forward slash The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

Although this color function is barely used, it’s completely valid to use, so it’s up to personal preference.

CodePen Embed FallbackNamed colorsCSS named colors are hardcoded keywords representing predefined colors in sRGB. You are probably used to the basic: white, blue, black, red, but there are a lot more, totaling 147 in all, that are defined in the Color Modules Level 4 specification.

Named colors are often discouraged because their names do not always match what color you would expect.


The CIELAB Color SpaceThe CIELAB color space is a relatively new color space on the web that represents a wider color gamut, closer to what the human eye can see, so it holds a lot more color than the sRGB space.

The lab() function For this color function, we have three axes in a space-separated list to determine how the color is set.

.element { color: lab(50 20 20 / 0.9);} * The first value l represents the degree of whiteness to blackness of the color. Its range being 0/(or 0%) (black) to 100 (or 100%) (white). * The second value a represents the degree of greenness to redness of the color. Its range being from -125/0% (green) to125 (or 100%) (red). * The third value b represents the degree of blueness to yellowness of the color. Its range is also from -125 (or 0%) (blue) to 125 (or 100%) (red). * The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

This is useful when you’re trying to obtain new colors and provide support for screens that do support them. Actually, most screens and all major browsers now support lab(), so you should be good.

The CSS lab() color function’s a and b values are actually unbounded. Meaning they don’t technically have an upper or lower limit. But, at practice, those are their limits according to the spec.

CodePen Embed Fallback The lch() function The CSS lch() color function is said to be better and more intuitive than lab().

.element { color: lch(10 30 300deg);} They both use the same color space, but instead of having l, a, and b, lch uses lightness, chroma, and hue.

  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0 (or 0%) (black) to 100 (or 100%) (white).
  • The second value c represents the color’s chroma (which is like saturation). Its range being from 0 (or 100%) to 150 or (or 100%).
  • The third value h represents the color hue. The value’s range is also from 0 (or 0deg) to 360 (or 360deg).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The CSS lch() color function’s chroma (c) value is actually unbounded. Meaning it doesn’t technically have an upper or lower limit. But, in practice, the chroma values above are the limits according to the spec.

CodePen Embed Fallback


The OkLab Color SpaceBjörn Ottosson created this color space as an “OK” and even better version of the lab color space. It was created to solve the limitations of CIELAB and CIELAB color space like image processing in lab(), such as making an image grayscale, and perceptual uniformity. The two color functions in CSS that correspond to this color space are oklab() and oklch().

Perceptual uniformity occurs when there’s a smooth change in the direction of a gradient color from one point to another. If you notice stark contrasts like the example below for rgb() when transitioning from one hue to another, that is referred to as a non-uniform perceptual colormap.

CodePen Embed FallbackNotice how the change from one color to another is the same in oklab() without any stark contrasts as opposed to rgb()? Yeah, OKLab color space solves the stark contrasts present and gives you access to many more colors not present in sRGB.

OKlab actually provides a better saturation of colors while still maintaining the hue and lightness present in colors in CIELAB (and even a smoother transition between colors!).

The oklab() function The oklab() color function, just like lab(), generates colors according to their lightness, red/green axis, blue/yellow axis, and an alpha value for color opacity. Also, the values for oklab() are different from that of lab() so please watch out for that.

.element { color: oklab(30% 20% 10% / 0.9);} * The first value l represents the degree of whiteness to blackness of the color. Its range being 0 (or 0%) (black) to 0.1 (or 100%) (white). * The second value a represents the degree of greenness to redness of the color. Its range being from -0.4 (or -100%) (green) to 0.4 (or 100%) (red). * The third value b represents the degree of blueness to yellowness of the color. The value’s range is also from -0.4 (or 0%) (blue) to 0.4 (or -100%) (red). * The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

Again, this solves one of the issues in lab which is perceptual uniformity so if you’re looking to use a better alternative to lab, use oklab().

The CSS oklab() color function’s a and b values are actually unbounded. Meaning they don’t technically have an upper or lower limit. But, theoretically, those are the limits for the values according to the spec.

CodePen Embed Fallback The oklch() function The oklch() color function, just like lch(), generates colors according to their lightness, chroma, hue, and an alpha value for color opacity. The main difference here is that it solves the issues present in lab() and lch().

.element { color: oklch(40% 20% 100deg / 0.7);} * The first value l represents the degree of whiteness to blackness of the color. Its range being 0.0 (or 0%) (black) to 1.0 (or 100%) (white). * The second value c represents the color’s chroma. Its range being from 0 (or 0%) to 0.4 (or 100%) (it theoretically doesn’t exceed 0.5). * The third value h represents the color hue. The value’s range is also from 0 (or 0deg) to 360 (or 360deg). * The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The CSS oklch() color function’s chroma (c) value is actually unbounded. Meaning it doesn’t technically have an upper or lower limit. But, theoretically, the chroma values above are the limits according to the spec.

CodePen Embed Fallback


The color() functionThe color() function allows access to colors in nine different color spaces, as opposed to the previous color functions mentioned, which only allow access to one.

To use this function, you must simply be aware of these 6 parameters:

  • The first value specifies the color space you want to access colors from. They can either be srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, or xyz-d65
  • The next three values (c1, c2, and c3) specifies the coordinates in the color space for the color ranging from 0.01.0.
  • The sixth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

CodePen Embed Fallback


The color-mix() functionThe color-mix() function mixes two colors of any type in a given color space. Basically, you can create an endless number of colors with this method and explore more options than you normally would with any other color function. A pretty powerful CSS function, I would say.

.element { color-mix(in oklab, hsl(40 20 60) 80%, red 20%);} You’re basically mixing two colors of any type in a color space. Do take note, the accepted color spaces here are different from the color spaces accepted in the color() function.

To use this function, you must be aware of these three values:

  • The first value in colorspace specifies the interpolation method used to mix the colors, and these can be any of these 15 color spaces: srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, lab, oklab, xyz, xyz-d50, xyz-d65, hsl, hwb, lch, and oklch.
  • The second and third values specifies an accepted color value and a percentage from 0% to 100%.

CodePen Embed Fallback


The Relative Color SyntaxHere’s how it works. We have:

.element{ color-function(from origin-color c1 c2 c3 / alpha)} * The first value from is a mandatory keyword you must set to extract the color values from origin-color. * The second value, origin-color, represents a color function or value or even another relative color that you want to get color from. * The next three values, c1, c2, and c3 represent the current color function’s color channels and they correspond with the color function’s valid color values. * The sixth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%) which either set from the origin-color or set manually,

Let’s take an example, say, converting a color from rgb() to lab():

.element { color: lab(from rgb(255 210 01 / 0.5) l a b / a);} All the values above will be translated to the corresponding colors in rgb(). Now, let’s take a look at another example where we convert a color from rgb() to oklch():

.element { color: oklch(from rgb(255 210 01 / 0.5) 50% 20% h / a);} Although, the l and c values were changed, the h and a would be taken from the original color, which in this case is a light yellowish color in rgb().

You can even be wacky and use math functions:

All CSS color functions support the relative color syntax. The relative color syntax, simply put, is a way to access other colors in another color function or value, then translating it to the values of the current color function. It goes “from <color>” to another.

.element { color: oklch(from rgb(255 210 01 / 0.5) calc(50% + var(--a)) calc(20% + var(--b)) h / a);} The relative color syntax is, however, different than the color() function in that you have to include the color space name and then fully write out the channels, like this:

.element { color: color(from origin-color colorspace c1 c2 c3 / alpha);} Remember, the color-mix() function is not a part of this. You can have relative color functions inside the color functions you want to mix, yes, but the relative color syntax is not available in color-mix() directly.


Color gradientsCSS is totally capable of transitioning from one color to another. See the “CSS Gradients Guide” for a full run-down, including of the different types of gradients with examples.

Visit the Guide


Properties that support color valuesThere are a lot of properties that support the use of color. Just so you know, this list does not contain deprecated properties.

accent-colorThis CSS property sets the accent color for UI controls like checkboxes and radio buttons, and any other form element

progress { accent-color: lightgreen;}

Accent colors are a way to style unique elements in respect to the chosen color scheme.

background-colorApplies solid colors as background on an element.

.element { background-color: #ff7a18;} border-colorShorthand for setting the color of all four borders.

/* Sets all border colors */.element { border-color: lch(50 50 20);}/* Sets top, right, bottom, left border colors */.element { border-color: black green red blue;} box-shadowAdds shadows to element for creating the illusion of depth. The property accepts a number of arguments, one of which sets the shadow color.

.element { box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);} caret-colorSpecifies the color of the text input cursor (caret).

.element { caret-color: lch(30 40 40);} colorSets the foreground color of text and text decorations.

.element { color: lch(80 10 20);} column-rule-colorSets the color of a line between columns in a multi-column layout. This property can’t act alone, so you need to set the columns and column-rule-style property first before using this.

.element { column: 3; column-rule-style: solid; column-rule-color: lch(20 40 40); /* highlight */} fillSets the color of the SVG shape

.element { fill: lch(40 20 10);} flood-colorSpecifies the flood color to use for <feFlood> and <feDropShadow> elements inside the <filter> element for <svg>. This should not be confused with the flood-color CSS attribute, as this is a CSS property and that’s an HTML attribute (even though they basically do the same thing). If this property is specified, it overrides the CSS flood-color attribute

.element { flood-color: lch(20 40 40);} lighting-colorSpecifies the color of the lighting source to use for <feDiffuseLighting> and <feSpecularLighting> elements inside the <filter> element for <svg>.

.element { lighting-color: lch(40 10 20);} outline-colorSets the color of an element’s outline.

.element { outline-color: lch(20 40 40);} stop-colorSpecifies the color of gradient stops for the <stop> tags for <svg>.

.element { stop-color: lch(20 40 40);} strokeDefines the color of the outline of <svg>.

.element { stroke: lch(20 40 40);} text-decoration-colorSets the color of text decoration lines like underlines.

.element { text-decoration-color: lch(20 40 40);} text-emphasis-colorSpecifies the color of emphasis marks on text.

.element { text-emphasis-color: lch(70 20 40);} text-shadowApplies shadow effects to text, including color.

.element { text-shadow: 1px 1px 1px lch(50 10 30);}


Almanac referencesColor functions Almanac on Feb 22, 2025 rgb() .element { color: rgb(0 0 0 / 0.5); } color Sunkanmi Fafowora Almanac on Feb 22, 2025 hsl() .element { color: hsl(90deg, 50%, 50%); } color Sunkanmi Fafowora Almanac on Jun 12, 2025 hwb() .element { color: hwb(136 40% 15%); } color Gabriel Shoyombo Almanac on Mar 4, 2025 lab() .element { color: lab(50% 50% 50% / 0.5); } color Sunkanmi Fafowora Almanac on Mar 12, 2025 lch() .element { color: lch(10% 0.215 15deg); } color Sunkanmi Fafowora Almanac on Apr 29, 2025 oklab() .element { color: oklab(25.77% 25.77% 54.88%; } color Sunkanmi Fafowora Almanac on May 10, 2025 oklch() .element { color: oklch(70% 0.15 240); } color Gabriel Shoyombo Almanac on May 2, 2025 color() .element { color: color(rec2020 0.5 0.15 0.115 / 0.5); } color Sunkanmi Fafowora Color properties Almanac on Apr 19, 2025 accent-color .element { accent-color: #f8a100; } color Geoff Graham Almanac on Jan 13, 2025 background-color .element { background-color: #ff7a18; } color Chris Coyier Almanac on Jan 27, 2021 caret-color .element { caret-color: red; } color Chris Coyier Almanac on Jul 11, 2022 color .element { color: #f8a100; } color Sara Cope Almanac on Jul 11, 2022 column-rule-color .element { column-rule-color: #f8a100; } color Geoff Graham Almanac on Jan 27, 2025 fill .element { fill: red; } color Geoff Graham Almanac on Jul 11, 2022 outline-color .element { outline-color: #f8a100; } color Mojtaba Seyedi Almanac on Dec 15, 2024 stroke .module { stroke: black;} color Geoff Graham Almanac on Aug 2, 2021 text-decoration-color .element { text-decoration-color: orange; } color Marie Mosley Almanac on Jan 27, 2023 text-emphasis .element { text-emphasis: circle red; } color Joel Olawanle Almanac on Jan 27, 2023 text-shadow p { text-shadow: 1px 1px 1px #000; } color Sara Cope


Related articles & tutorials Article on Aug 12, 2024 Working With Colors Guide color Sarah Drasner Article on Aug 23, 2022 The Expanding Gamut of Color on the Web color Ollie Williams Article on Oct 13, 2015 The Tragicomic History of CSS Color Names color Geoff Graham Article on Feb 11, 2022 A Whistle-Stop Tour of 4 New CSS Color Features color Chris Coyier Article on Feb 7, 2022 Using Different Color Spaces for Non-Boring Gradients color Chris Coyier Article on Oct 29, 2024 Come to the light-dark() Side color Sara Joy Article on Sep 24, 2024 Color Mixing With Animation Composition color Geoff Graham Article on Sep 13, 2016 8-Digit Hex Codes? color Chris Coyier Article on Feb 24, 2021 A DRY Approach to Color Themes in CSS color Christopher Kirk-Nielsen Article on Apr 6, 2017 Accessibility Basics: Testing Your Page For Color Blindness color Chris Coyier Article on Mar 9, 2020 Adventures in CSS Semi-Transparency Land color Ana Tudor Article on Mar 4, 2017 Change Color of All Four Borders Even With border-collapse: collapse; color Daniel Jauch Article on Jan 2, 2020 Color contrast accessibility tools color Robin Rendle Article on Aug 14, 2019 Contextual Utility Classes for Color with Custom Properties color Christopher Kirk-Nielsen Article on Jun 26, 2021 Creating Color Themes With Custom Properties, HSL, and a Little calc() color Dieter Raber Article on May 4, 2021 Creating Colorful, Smart Shadows color Chris Coyier Article on Feb 21, 2018 CSS Basics: Using Fallback Colors color Chris Coyier Article on Oct 21, 2019 Designing accessible color systems color Robin Rendle Article on Jun 22, 2021 Mixing Colors in Pure CSS color Carter Li Article on Jul 26, 2016 Overriding The Default Text Selection Color With CSS color Chris Coyier Article on Oct 21, 2015 Reverse Text Color Based on Background Color Automatically in CSS color Robin Rendle Article on Dec 27, 2019 So Many Color Links color Chris Coyier Article on Aug 18, 2018 Switch font color for different backgrounds with CSS color Facundo Corradini Article on Jan 20, 2020 The Best Color Functions in CSS? color Chris Coyier Article on Dec 3, 2021 What do you name color variables? color Chris Coyier Article on May 8, 2025 Why is Nobody Using the hwb() Color Function? color Sunkanmi Fafowora Table of contents 1. Colors are in everything 2. What’s a color space? 3. The sRGB Color Space 4. The CIELAB Color Space 5. The OkLab Color Space 6. The color() function 7. The color-mix() function 8. The Relative Color Syntax 9. Color gradients 10. Properties that support color values 11. Almanac references 12. Related articles and tutorials


CSS Color Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

How do you keep up with new CSS features?

Let’s say for example that, hypothetically speaking, you run a popular web development survey focused on CSS, and need to figure out what to include in this year’s edition. (In a total coincidence the aforementioned State of CSS survey for this year is actually open right now — go take it to see what’s new in CSS!)

You might think you can just type “new CSS features 2025” in Google and be done with it. But while this does give us a few promising leads, it also unearths a lot of cookie-cutter content that proclaims CSS Grid as the “next big thing”, despite the fact it’s been well-supported for over eight years now.

We need a better approach.

I’ll focus on CSS in this article, but all the resources linked here cover all web platform features, including JavaScript and HTML.

Browser blogsThe browsers themselves are often a good source of what’s new and, thankfully, the big ones maintain blogs where they even cover specific CSS news.

A good general starting point is Google’s web.dev blog, and more specifically Rachel Andrew‘s monthly web platform recaps. Here’s a small sample of those:

  • New to the web platform in January
  • New to the web platform in February
  • New to the web platform in March
  • New to the web platform in April

The WebKit blog is, of course, another great place. Jen Simmons is very active on the blog (as well as Bluesky) and there’s an entire category dedicated solely to CSS. The blog doesn’t publish content as regularly as web.dev, but the breadth of content is incredibly deep and thorough, as you can see in Jen’s write-up on the release of text-wrap: pretty.

And, to round things out, you’ll want to keep an eye on Firefox Nightly News for what’s shipping in Firefox, and the official blog for Microsoft Edge.

CSS-Tricks (and others)I’d be remiss to not mention that CSS-Tricks is also a great source for up-to-date CSS knowledge, including an ever-growing almanac of CSS features. But you probably already know that since you’re reading this.

And let’s not discount other fine publications that cover CSS. Here are just a few:

  • Smashing Magazine
  • Frontend Masters Blog
  • Piccalilli
  • CSS-Tip

Following individual sources can get a little overwhelming, particularly when CSS is moving as fast as it is. That’s where Frontend Dogma comes in with an ever-growing and updated list of curated links from these (and many other sources) in a one-stop shop.

Web Platform Features ExplorerIf you need something a bit more structured to help you figure out what’s new, Web Platform Features Explorer is great way to look up features based on their Baseline status.

Web Platform StatusA similar tool is the Web Platform Status dashboard. This one features more fine-grained filtering tools, letting you narrow down features by Baseline year or even show features mentioned as Top CSS Interop in the latest State of CSS survey!

Another very cool feature is the ability to view a feature’s adoption rate, as measured in terms of percentage of Chrome page views where that feature was used, such as here for the popover HTML attribute:

An important caveat: since sites like Facebook and Google account for a very large percentage of all measured page views, this metric can become skewed once one of these platforms adopts a new feature.

The Web Platform Status’s stats section also features the “chart of shame” (according to Lea Verou), which highlights how certain browsers might be slightly lagging behind their peers in terms of new feature adoption.

Chrome Platform StatusThat same adoption data can also be found on the Chrome Platform Status dashboard, which gives you even more details, such as usage among top sites, as well as sample URLs of sites that are using a feature.

Polypane Experimental Chromium Features DashboardPolypane is a great developer-focused browser that provides a ton of useful tools like contrast checkers, multi-viewport views, and more.

They also provide an experimental Chromium features explorer that breaks new features down by Chromium version, for those of you who want to be at the absolute top of the cutting edge.

Kevin Powell’s YouTube ChannelAs YouTube’s de facto CSS expert, Kevin Powell often puts up great video recaps of new features. You should definitely be following him, but statistically speaking you probably already are! It’s also worth mentioning that Kevin runs a site that publishes weekly HTML and CSS tips.

CSS Working GroupOf course, you can always also go straight to the source and look at what the CSS Working Group itself has been working on! They have a mailing list you can subscribe to keep tabs on things straight from your inbox, as well as an RSS feed.

Browser release notesMost browsers publish a set of release notes any time a new version ships. For the most part, you can get a good pulse on when new CSS features are released by following the three big names in browsers:

  • Chrome release notes
  • Safari release notes
  • Firefox release notes

ChatGPTAnother way to catch up with CSS is to just ask ChatGPT! This sample prompt worked well enough for me:

What are the latest CSS features that have either become supported by browsers in the past year, or will soon become supported?

Other resourcesIf you really want to get in the weeds, Igalia’s BCD Watch displays changes to MDN’s browser-compat-data repo, which itself tracks which features are supported in which browsers.

Also, the latest editions of the HTTP Archive Web Almanac do not seem to include a CSS section specifically, but past editions did feature one, which was a great way to catch up with CSS once a year.

There’s also caniuse has a news section which does not seem to be frequently updated at the moment, but could potentially become a great resource for up-to-date new feature info in the future.

The IntentToShip bot (available on Bluesky, Mastodon, Twitter) posts whenever a browser vendor ships or changes a feature. You can’t get more cutting-edge than that!

And lastly, there’s a ton of folks on social media who are frequently discussing new CSS features and sharing their own thoughts and experiments with them. If you’re on Bluesky, there’s a starter pack of CSS-Tricks authors that’s a good spot to find a deep community of people.

Wrapping upOf course, another great way to make sure no new features are slipping through the cracks is to take the State of CSS survey once a year. I use all the resources mentioned above to try and make sure each survey includes every new important feature. What’s more, you can bookmark features by adding them to your “reading list” as you take the survey to get a nice recap at the end.

So go take this year’s State of CSS survey and then let me know on Bluesky how many new features you learned about!


How to Keep Up With New CSS Features originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Resize Observer, Mutation Observer, and Intersection Observers are all good APIs that are more performant than their older counterparts:

  • ResizeObserver is better than the resize event
  • MutationObserver replaces the now deprecated Mutation Events
  • IntersectionObserver lets you do certain scroll interactions with less performance overhead.

The API for these three observers are quite similar (but they have their differences which we will go into later). To use an observer, you have to follow the steps below:

  1. Create a new observer with the new keyword: This observer takes in an observer function to execute.
  2. Do something with the observed changes: This is done via the observer function that is passed into the observer.
  3. Observe a specific element: By using the observe method.
  4. (Optionally) unobserve the element: By using the unobserve or disconnect method. (depending on which observer you’re using).

In practice, the above steps looks like this with the ResizeObserver.

// Step 1: Create a new observerconst observer = new ResizeObserver(observerFn)// Step 2: Do something with the observed changesfunction observerFn (entries) { for (let entry of entries) { // Do something with entry }}// Step 3: Observe an elementconst element = document.querySelector('#some-element')observer.observe(element);// Step 4 (optional): Disconnect the observerobserver.disconnect(element) This looks clear (and understandable) after the steps have been made clear. But it can look like a mess without the comments:

const observer = new ResizeObserver(observerFn)function observerFn (entries) { for (let entry of entries) { // Do something with entry }}const element = document.querySelector('#some-element')observer.observe(element); The good news is: I think we can improve the observer APIs and make them easier to use.

The Resize ObserverLet’s start with the ResizeObserver since it’s the simplest of them all. We’ll begin by writing a function that encapsulates the resizeObserver that we create.

function resizeObserver () { // ... Do something} The easiest way to begin refactoring the ResizeObserver code is to put everything we’ve created into our resizeObserver first.

function resizeObserver () { const observer = new ResizeObserver(observerFn) function observerFn (entries) { for (let entry of entries) { // Do something with entry } } const node = document.querySelector('#some-element') observer.observe(node);} Next, we can pass the element into the function to make it simpler. When we do this, we can eliminate the document.querySelector line.

function resizeObserver (element) { const observer = new ResizeObserver(observerFn) function observerFn (entries) { for (let entry of entries) { // Do something with entry } } observer.observe(node);} This makes the function more versatile since we can now pass any element into it.

// Usage of the resizeObserver functionconst node = document.querySelector('#some-element')const obs = resizeObserver(node) This is already much easier than writing all of the ResizeObserver code from scratch whenever you wish to use it.

Next, it’s quite obvious that we have to pass in an observer function to the callback. So, we can potentially do this:

// Not greatfunction resizeObserver (node, observerFn) { const observer = new ResizeObserver(observerFn) observer.observe(node);} Since observerFn is always the same — it loops through the entries and acts on every entry — we could keep the observerFn and pass in a callback to perform tasks when the element is resized.

// Better function resizeObserver (node, callback) { const observer = new ResizeObserver(observerFn) function observerFn (entries) { for (let entry of entries) { callback(entry) } } observer.observe(node);} To use this, we can pass callback into the resizeObserver — this makes resizeObserver operate somewhat like an event listener which we are already familiar with.

// Usage of the resizeObserver functionconst node = document.querySelector('#some-element')const obs = resizeObserver(node, entry => { // Do something with each entry}) We can make the callback slightly better by providing both entry and entries. There’s no performance hit for passing an additional variable so there’s no harm providing more flexibility here.

function resizeObserver (element, callback) { const observer = new ResizeObserver(observerFn) function observerFn (entries) { for (let entry of entries) { callback({ entry, entries }) } } observer.observe(element);} Then we can grab entries in the callback if we need to.

// Usage of the resizeObserver function// ...const obs = resizeObserver(node, ({ entry, entries }) => { // ...}) Next, it makes sense to pass the callback as an option parameter instead of a variable. This will make resizeObserver more consistent with the mutationObserver and intersectionObserver functions that we will create in the next article.

function resizeObserver (element, options = {}) { const { callback } = options const observer = new ResizeObserver(observerFn) function observerFn (entries) { for (let entry of entries) { callback({ entry, entries }) } } observer.observe(element);} Then we can use resizeObserver like this.

const obs = resizeObserver(node, { callback ({ entry, entries }) { // Do something ... }}) The observer can take in an option tooResizeObserver‘s observe method can take in an options object that contains one property, box. This determines whether the observer will observe changes to content-box, border-box or device-pixel-content-box.

So, we need to extract these options from the options object and pass them to observe.

function resizeObserver (element, options = {}) { const { callback, ...opts } = options // ... observer.observe(element, opts);} Optional: Event listener patternI prefer using callback because it’s quite straightforward. But if you want to use a standard event listener pattern, we can do that, too. The trick here is to emit an event. We’ll call it resize-obs since resize is already taken.

function resizeObserver (element, options = {}) { // ... function observerFn (entries) { for (let entry of entries) { if (callback) callback({ entry, entries }) else { node.dispatchEvent( new CustomEvent('resize-obs', { detail: { entry, entries }, }), ) } } } // ...} Then we can listen to the resize-obs event, like this:

const obs = resizeObserver(node)node.addEventListener('resize-obs', event => { const { entry, entries } = event.detail}) Again, this is optional.

Unobserving the elementOne final step is to allow the user to stop observing the element(s) when observation is no longer required. To do this, we can return two of the observer methods:

  • unobserve: Stops observing one Element
  • disconnect: Stops observing all Elements

function resizeObserver (node, options = {}) { // ... return { unobserve(node) { observer.unobserve(node) }, disconnect() { observer.disconnet() } }} Both methods do the same thing for what we have built so far since we only allowed resizeObserver to observe one element. So, pick whatever method you prefer to stop observing the element.

const obs = resizeObserver(node, { callback ({ entry, entries }) { // Do something ... }})// Stops observing all elements obs.disconect() With this, we’ve completed the creation of a better API for the ResizeObserver — the resizeObserver function.

Code snippetHere’s the code we’ve wrote for resizeObserver

export function resizeObserver(node, options = {}) { const observer = new ResizeObserver(observerFn) const { callback, ...opts } = options function observerFn(entries) { for (const entry of entries) { // Callback pattern if (callback) callback({ entry, entries, observer }) // Event listener pattern else { node.dispatchEvent( new CustomEvent('resize-obs', { detail: { entry, entries, observer }, }) ) } } } observer.observe(node) return { unobserve(node) { observer.unobserve(node) }, disconnect() { observer.disconnect() } }} Using this in practice via Splendid LabzSplendid Labz has a utils library that contains an enhanced version of the resizeObserver we made above. You can use it if you wanna use a enhanced observer, or if you don’t want to copy-paste the observer code into your projects.

import { resizeObserver } from '@splendidlabz/utils/dom'const node = document.querySelector('.some-element')const obs = resizeObserver(node, { callback ({ entry, entries }) { /* Do what you want here */ }}) Bonus: The Splendid Labz resizeObserver is capable of observing multiple elements at once. It can also unobserve multiple elements at once.

const items = document.querySelectorAll('.elements')const obs = resizeObserver(items, { callback ({ entry, entries }) { /* Do what you want here */ }})// Unobserves two items at onceconst subset = [items[0], items[1]]obs.unobserve(subset) Found this refactoring helpful?Refactoring is ultra useful (and important) because its a process that lets us create code that’s easy to use or maintain.

If you found this refactoring exercise useful, you might just love how I teach JavaScript to budding developers in my Learn JavaScript course.

In this course, you’ll learn to build 20 real-world components. For each component, we start off simple. Then we add features and you’ll learn to refactor along the way.

That’s it!

Hope you enjoyed this piece and see you in the next one.


A Better API for the Resize Observer originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

For years, I believed that drag-and-drop games — especially those involving rotation, spatial logic, and puzzle solving — were the exclusive domain of JavaScript. Until one day, I asked AI:

“Is it possible to build a fully interactive Tangram puzzle game using only CSS?”

The answer: “No — not really. You’ll need JavaScript.” That was all the motivation I needed to prove otherwise.

CodePen Embed FallbackBut first, let’s ask the obvious question: Why would anyone do this?

Well…

  • To know how far CSS can be pushed in creating interactive UIs.
  • To get better at my CSS skills.
  • And it’s fun!

Fair enough?

Now, here’s the unsurprising truth: CSS isn’t exactly made for this. It’s not a logic language, and let’s be honest, it’s not particularly dynamic either. (Sure, we have CSS variables and some handy built-in functions now, hooray!)

In JavaScript, we naturally think in terms of functions, loops, conditions, objects, comparisons. We write logic, abstract things into methods, and eventually ship a bundle that the browser understands. And once it’s shipped? We rarely look at that final JavaScript bundle — we just focus on keeping it lean.

Now ask yourself: isn’t that exactly what Sass does for CSS?

Why should we hand-write endless lines of repetitive CSS when we can use mixins and functions to generate it — cleanly, efficiently, and without caring how many lines it takes, as long as the output is optimized?

So, we put it to the test and it turns out Sass can replace JavaScript, at least when it comes to low-level logic and puzzle behavior. With nothing but maps, mixins, functions, and a whole lot of math, we managed to bring our Tangram puzzle to life, no JavaScript required.

Let the (CSS-only) games begin! 🎉

The gameThe game consists of seven pieces: the classic Tangram set. Naturally, these pieces can be arranged into a perfect square (and many other shapes, too). But we need a bit more than just static pieces.

So here’s what I am building:

  • A puzzle goal, which is the target shape the player has to recreate.
  • A start button that shuffles all the pieces into a staging area.
  • Each piece is clickable and interactive.
  • The puzzle should let the user know when they get a piece wrong and also celebrate when they finish the puzzle.

The HTML structureI started by setting up the HTML structure, which is no small task, considering the number of elements involved.

  • Each shape was given seven radio buttons. I chose radios over checkboxes to take advantage of their built-in exclusivity. Only one can be selected within the same group. This made it much easier to track which shape and state were currently active.
  • The start button? Also a radio input. A checkbox could’ve worked too, but for the sake of consistency, I stuck with radios across the board.
  • The puzzle map itself is just a plain old <div>, simple and effective.
  • For rotation, we added eight radio buttons, each representing a 45-degree increment: 45°, 90°, 135°, all the way to 360°. These simulate rotation controls entirely in CSS.
  • Every potential shadow position got its own radio button too. (Yes, it’s a lot, I know.)
  • And to wrap it all up, I included a classic reset button inside a <form> using <button type="reset">, so players can easily start over at any point.

Given the sheer number of elements required, I used Pug to generate the HTML more efficiently. It was purely a convenience choice. It doesn’t affect the logic or behavior of the puzzle in any way.

Below is a sample of the compiled HTML. It might look overwhelming at first glance (and this is just a portion of it!), but it illustrates the structural complexity involved. This section is collapsed to not nuke your screen, but it can be expanded if you’d like to explore it.

Open HTML Code
```

``` Creating maps for shape dataNow that HTML skeleton is ready, it’s time to inject it with some real power. That’s where our Sass maps come in, and here’s where the puzzle logic starts to shine.

Note: Maps in Sass hold pairs of keys and values, and make it easy to look up a value by its corresponding key. Like objects in JavaScript, dictionaries in Python and, well, maps in C++.

I’m mapping out all the core data needed to control each tangram piece (tan): its color, shape, position, and even interaction logic. These maps contain:

  • the background-color for each tan,
  • the clip-path coordinates that define their shapes,
  • the initial position for each tan,
  • the position of the blocking div (which disables interaction when a tan is selected),
  • the shadow positions (coordinates for the tan’s silhouette displayed on the task board),
  • the grid information, and
  • the winning combinations — the exact target coordinates for each tan, marking the correct solution.

$colors: ( blue-color: #53a0e0, yellow-color: #f7db4f, /* Colors for each tan */ );$nth-child-grid: ( 1: (2, 3, 1, 2, ), 2: ( 3, 4, 1, 2, ), 4: ( 1, 2, 2, 3, ), /* More entries to be added */);$bluePosiblePositions: ( 45: none, 90: ( (6.7, 11.2), ), 135: none, 180: none, /* Positions defined up to 360 degrees */);/* Other tans *//* Data defined for each tan */$tansShapes: ( blueTriangle: ( color: map.get($colors, blue-color), clip-path: ( 0 0, 50 50, 0 100, ), rot-btn-position: ( -20, -25, ), exit-mode-btn-position: ( -20, -33, ), tan-position: ( -6, -37, ), diable-lab-position: ( -12, -38, ), poss-positions: $bluePosiblePositions, correct-position: ((4.7, 13.5), (18.8, 13.3), ), transform-origin: ( 4.17, 12.5,), ),);/* Remaining 7 combinations */$winningCombinations: ( combo1: ( (blueTriangle, 1, 360), (yellowTriangle, 1, 225), (pinkTriangle, 1, 180), (redTriangle, 4, 360), (purpleTriangle, 2, 225), (square, 1, 90), (polygon, 4, 90), ),); You can see this in action on CodePen, where these maps drive the actual look and behavior of each puzzle piece. At this point, there’s no visible change in the preview. We’ve simply prepared and stored the data for later use.

CodePen Embed FallbackUsing mixins to read from mapsThe main idea is to create reusable mixins that will read data from the maps and apply it to the corresponding CSS rules when needed.

But before that, we’ve elevated things to a higher level by making one key decision: We never hard-coded units directly inside the maps. Instead, we built a reusable utility function that dynamically adds the desired unit (e.g., vmin, px, etc.) to any numeric value when it’s being used. This way, when can use our maps however we please.

@function get-coordinates($data, $key, $separator, $unit) { $coordinates: null; // Check if the first argument is a map @if meta.type-of($data) == "map" { // If the map contains the specified key @if map.has-key($data, $key) { // Get the value associated with the key (expected to be a list of coordinates) $coordinates: map.get($data, $key); } // If the first argument is a list } @else if meta.type-of($data) == "list" { // Ensure the key is a valid index (1-based) within the list @if meta.type-of($key) == "number" and $key > 0 and $key <= list.length($data) { // Retrieve the item at the specified index $coordinates: list.nth($data, $key); } // If neither map nor list, throw an error } @else { @error "Invalid input: First argument must be a map or a list."; } // If no valid coordinates were found, return null @if $coordinates == null { @return null; } // Extract x and y values from the list $x: list.nth($coordinates, 1); $y: list.nth($coordinates, -1); // -1 gets the last item (y) // Return the combined x and y values with units and separator @return #{$x}#{$unit}#{$separator}#{$y}#{$unit};} Sure, nothing’s showing up in the preview yet, but the real magic starts now.

CodePen Embed FallbackNow we move on to writing mixins. I’ll explain the approach in detail for the first mixin, and the rest will be described through comments.

The first mixin dynamically applies grid-column and grid-row placement rules to child elements based on values stored in a map. Each entry in the map corresponds to an element index (1 through 8) and contains a list of four values: [start-col, end-col, start-row, end-row].

@mixin tanagram-grid-positioning($nth-child-grid) { // Loop through numbers 1 to 8, corresponding to the tanam pieces @for $i from 1 through 8 { // Check if the map contains a key for the current piece (1-8) @if map.has-key($nth-child-grid, $i) { // Get the grid values for this piece: [start-column, end-column, start-row, end-row] $values: map.get($nth-child-grid, $i); // Target the nth child (piece) and set its grid positions &:nth-child(#{$i}) { // Set grid-column: start and end values based on the first two items in the list grid-column: #{list.nth($values, 1)} / #{list.nth($values, 2)}; // Set grid-row: start and end values based on the last two items in the list grid-row: #{list.nth($values, 3)} / #{list.nth($values, 4)}; } } }} We can expect the following CSS to be generated:

.tanagram-box:nth-child(1) { grid-column: 2 / 3; grid-row: 1 / 2;}.tanagram-box:nth-child(2) { grid-column: 3 / 4; grid-row: 1 / 2;} CodePen Embed FallbackIn this mixin, my goal was actually to create all the shapes (tans). I am using clip-path. There were ideas to use fancy SVG images, but this test project is more about testing the logic rather than focusing on beautiful design. For this reason, the simplest solution was to cut the elements according to dimensions while they are still in the square (the initial position of all the tans).

So, in this case, through a static calculation, the $tansShapes map was updated with the clip-path property:

clip-path: (0 0, 50 50, 0 100); This contains the clip points for all the tans. In essence, this mixin shapes and colors each tan accordingly.

@mixin set-tan-clip-path($tanName, $values) { // Initialize an empty list to hold the final clip-path points $clip-path-points: (); // Extract the 'clip-path' data from the map, which contains coordinate pairs $clip-path-key: map.get($values, clip-path); // Get the number of coordinate pairs to loop through $count: list.length($clip-path-key); // Loop through each coordinate point @for $i from 1 through $count { // Convert each pair of numbers into a formatted coordinate string with units $current-point: get-coordinates($clip-path-key, $i, " ", "%"); // Add the formatted coordinate to the list, separating each point with a comma $clip-path-points: list.append($clip-path-points, #{$current-point}, comma); } // Style for the preview element (lab version), using the configured background color #tan#{$tanName}lab { background: map.get($values, color); clip-path: polygon(#{$clip-path-points}); // Apply the full list of clip-path points } // Apply the same clip-path to the actual tan element .#{$tanName} { clip-path: polygon(#{$clip-path-points}); }} and output in CSS should be:

.blueTriangle { clip-path: polygon(0% 0%, 50% 50%, 0% 100%);}/* other tans */ CodePen Embed FallbackStart logicAlright, now I’d like to clarify what should happen first when the game loads.

First, with a click on the Start button, all the tans “go to their positions.” In reality, we assign them a transform: translate() with specific coordinates and a rotation.

.start:checked ~ .shadow #tanblueTrianglelab { transform-origin: 4.17vmin 12.5vmin; transform: translate(-6vmin,-37vmin) rotate(360deg); cursor: pointer;} CodePen Embed FallbackSo, we still maintain this pattern. We use transform and simply change the positions or angles (in the maps) of both the tans and their shadows on the task board.

When any tan is clicked, the rotation button appears. By clicking on it, the tan should rotate around its center, and this continues with each subsequent click. There are actually eight radio buttons, and with each click, one disappears and the next one appears. When we reach the last one, clicking it makes it disappear and the first one reappears. This way, we get the impression of clicking the same button (they are, of course, styled the same) and being able to click (rotate the tan) infinitely. This is exactly what the following mixin enables.

@mixin set-tan-rotation-states($tanName, $values, $angles, $color) { // This mixin dynamically applies rotation UI styles based on a tan's configuration. // It controls the positioning and appearance of rotation buttons and visual feedback when a rotation state is active. @each $angle in $angles{ & ~ #rot#{$angle}{ transform: translate(get-coordinates($values,rot-btn-position,',',vmin )); background: $color;} & ~ #rotation-#{$angle}:checked{ @each $key in map.keys($tansShapes){ & ~ #tan#{$key}labRes{ visibility: visible; background:rgba(0,0,0,0.4); } & ~ #tan#{$key}lab{ opacity:.3; } & ~ #rotReset{ visibility: visible; } } } }} And the generated CSS should be:

```

blueTriangle-tan:checked ~ #rotation-45:checked ~ #tanblueTrianglelab { transform: translate(-6vmin,-37vmin) rotate(45deg);}#blueTriangle-tan:checked ~ #rotation-45:checked ~ #tanblueTrianglelabRes { visibility: hidden;}

`` OK, the following mixins use theset-clip-pathandset-rotation` mixins. They contain all the information about the tans and their behavior in relation to which tan is clicked and which rotation is selected, as well as their positions (as defined in the second mixin).

@mixin generate-tan-shapes-and-interactions($tansShapes) {// Applies styling logic and UI interactions for each individual tan shape from the $tansShapes map. @each $tanName, $values in $tansShapes{ $color: color.scale(map.get($values, color), $lightness: 10%); $angles: (45, 90, 135, 180, 225, 270, 315, 360); @include set-tan-clip-path($tanName, $values); ##{$tanName}-tan:checked{ & ~ #tan#{$tanName}Res{ visibility:hidden; } & ~ #tan#{$tanName}lab{opacity: 1 !important;background: #{$color};cursor:auto;} @each $key in map.keys($tansShapes){ & ~ #tan#{$tanName}Res:checked ~ #tan#{$key}labRes{visibility: visible;} } & ~ #rot45{display: flex;visibility: visible;} & ~ #rotReset{ transform: translate(get-coordinates($values, exit-mode-btn-position,',', vmin)); } @include set-tan-rotation-states($tanName, $values, $angles, $color); } }}

@mixin set-initial-tan-position($tansShapes) {// This mixin sets the initial position and transformation for both the interactive (`lab`) and shadow (`labRes`) versions// of each tan shape, based on coordinates provided in the $tansShapes map. @each $tanName, $values in $tansShapes{ & ~ .shadow #tan#{$tanName}lab{ transform-origin: get-coordinates($values, transform-origin,' ' ,vmin); transform: translate( get-coordinates($values,tan-position,',', vmin)) rotate(360deg) ; cursor: pointer; } & ~ .shadow #tan#{$tanName}labRes{ visibility:hidden; transform: translate(get-coordinates($values,diable-lab-position,',',vmin)); } }} CodePen Embed FallbackAs mentioned earlier, when a tan is clicked, one of the things that becomes visible is its shadow — a silhouette that appears on the task board.

These shadow positions (coordinates) are currently defined statically. Each shadow has a specific place on the map, and a mixin reads this data and applies it to the shadow using transform: translate().

When the clicked tan is rotated, the number of visible shadows on the task board can change, as well as their angles, which is expected.

Of course, special care was taken with naming conventions. Each shadow element gets a unique ID, made from the name (inherited from its parent tan) and a number that represents its sequence position for the given angle.

Pretty cool, right? That way, we avoid complicated naming patterns entirely!

@mixin render-possible-tan-positions( $name, $angle, $possiblePositions, $visibility, $color, $id, $transformOrigin ) { // This mixin generates styles for possible positions of a tan shape based on its name, rotation angle, and configuration map. // It handles both squares and polygons, normalizing their rotation angles accordingly and applying transform styles if positions exist.} @if $name == 'square' { $angle: normalize-angle($angle); // Normalizujemo ugao ako je u pitanju square } @else if $name == 'polygon'{ $angle: normalize-polygon-angle($angle); } @if map.has-key($possiblePositions, $angle) { $values: map.get($possiblePositions, $angle); @if $values != none { $count: list.length($values); @for $i from 1 through $count { $position: get-coordinates($values, $i, ',', vmin); & ~ #tan#{$name}lab-#{$i}-#{$angle} { @if $visibility == visible { visibility: visible; background-color: $color; opacity: .2; z-index: 2; transform-origin: #{$transformOrigin}; transform: translate(#{$position}) rotate(#{$angle}deg); } @else if $visibility == hidden { visibility: hidden; } &:hover{ opacity: 0.5; cursor: pointer; } } } } }} The generated CSS:

```

blueTriangle-tan:checked ~ #tanblueTrianglelab-1-360 { visibility: visible; background-color: #53a0e0; opacity: 0.2; z-index: 2; transform-origin: 4.17vmin 12.5vmin; transform: translate(4.7vmin,13.5vmin) rotate(360deg);}

``` This next mixin is tied to the previous one and manages when and how the tan shadows appear while their parent tan is being rotated using the button. It listens for the current rotation angle and checks whether there are any shadow positions defined for that specific angle. If there are, it displays them; if not — no shadows!

@mixin render-possible-positions-by-rotation { // This mixin applies rotation to each tan shape. It loops through each tan, calculates its possible positions for each angle, and handles visibility and transformation. // It ensures that rotation is applied correctly, including handling the transitions between various tan positions and visibility states. @each $tanName, $values in $tansShapes{ $possiblePositions: map.get($values, poss-positions); $possibleTansColor: map.get($values, color); $validPosition: get-coordinates($values, correct-position,',' ,vmin); $transformOrigin: get-coordinates($values,transform-origin,' ' ,vmin); $rotResPosition: get-coordinates($values,exit-mode-btn-position ,',' ,vmin ); $angle: 0; @for $i from 1 through 8{ $angle: $i * 45; $nextAngle: if($angle + 45 > 360, 45, $angle + 45); @include render-position-feedback-on-task($tanName,$angle, $possiblePositions,$possibleTansColor, #{$tanName}-tan, $validPosition,$transformOrigin, $rotResPosition); ##{$tanName}-tan{ @include render-possible-tan-positions($tanName,$angle, $possiblePositions,hidden, $possibleTansColor, #{$tanName}-tan,$transformOrigin) } ##{$tanName}-tan:checked{ @include render-possible-tan-positions($tanName,360, $possiblePositions,visible, $possibleTansColor, #{$tanName}-tan,$transformOrigin); & ~ #rotation-#{$angle}:checked { @include render-possible-tan-positions($tanName,360, $possiblePositions,hidden, $possibleTansColor, #{$tanName}-tan,$transformOrigin); & ~ #tan#{$tanName}lab{transform:translate( get-coordinates($values,tan-position,',', vmin)) rotate(#{$angle}deg) ;} & ~ #tan#{$tanName}labRes{ visibility: hidden; } & ~ #rot#{$angle}{ visibility: hidden; } & ~ #rot#{$nextAngle}{ visibility: visible } @include render-possible-tan-positions($tanName,$angle, $possiblePositions,visible, $possibleTansColor, #{$tanName}-tan,$transformOrigin); } } } }} CodePen Embed FallbackWhen a tan’s shadow is clicked, the corresponding tan should move to that shadow’s position. The next mixin then checks whether this new position is the correct one for solving the puzzle. If it is correct, the tan gets a brief blinking effect and becomes unclickable, signaling it’s been placed correctly. If it’s not correct, the tan simply stays at the shadow’s location. There’s no effect and it remains draggable/clickable.

CodePen Embed FallbackOf course, there’s a list of all the correct positions for each tan. Since some tans share the same size — and some can even combine to form larger, existing shapes — we have multiple valid combinations. For this Camel task, all of them were taken into account. A dedicated map with these combinations was created, along with a mixin that reads and applies them.

CodePen Embed FallbackAt the end of the game, when all tans are placed in their correct positions, we trigger a “merging” effect — and the silhouette of the camel turns yellow. At that point, the only remaining action is to click the Restart button.

Well, that was long, but that’s what you get when you pick the fun (albeit hard and lengthy) path. All as an ode to CSS-only magic!


Breaking Boundaries: Building a Tangram Puzzle With (S)CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The HTML popover attribute transforms elements into top-layer elements that can be opened and closed with a button or JavaScript. Most popovers can be light-dismissed, closing when the user clicks or taps outside the popup. Currently, HTML popover lacks built-in auto-close functionality, but it’s easy to add. Auto closing popups are useful for user interfaces like banner notifications — the new-message alerts in phones, for instance.

A ~~picture~~ demo, is worth a thousand words, right? Click on the “Add to my bookmarks” button in the following example. It triggers a notification that dismisses itself after a set amount of time.

CodePen Embed FallbackLet’s start with the popoverThe HTML popover attribute is remarkably trivial to use. Slap it on a div, specify the type of popover you need, and you’re done.

```

Bookmarked!

`` Amanualpopover simply means it cannot be light-dismissed by clicking outside the element. As a result, we have to hide, show, or toggle the popover’s visibility ourselves explicitly with either buttons or JavaScript. Let’s use a semantic HTMLbutton`.

<button popovertarget="pop" popovertargetaction="show"> Add to my bookmarks</button><div popover="manual" id="pop">Bookmarked!</div> The popovertarget and popovertargetaction attributes are the final two ingredients, where popovertarget links the button to the popover element and popovertargetaction ensures that the popover is show-n when the button is clicked.

Hiding the popover with a CSS transitionOK, so the challenge is that we have a popover that is shown when a certain button is clicked, but it cannot be dismissed. The button is only wired up to show the popover, but it does not hide or toggle the popover (since we are not explicitly declaring it). We want the popover to show when the button is clicked, then dismiss itself after a certain amount of time.

The HTML popover can’t be closed with CSS, but it can be hidden from the page. Adding animation to that creates a visual effect. In our example, we will hide the popover by eliminating its CSS height property. You’ll learn in a moment why we’re using height, and that there are other ways you can go about it.

We can indeed select the popover attribute using an attribute selector:

[popover] { height: 0; transition: height cubic-bezier(0.6, -0.28, 0.735, 0.045) .3s .6s; @starting-style { height: 1lh; }} When the popover is triggered by the button, its height value is the one declared in the @starting-style ruleset (1lh). After the transition-delay (which is .6s in the example), the height goes from 1lh to 0 in .3s, effectively hiding the popover.

Once again, this is only hiding the popover, not closing it properly. That’s the next challenge and we’ll need JavaScript for that level of interaction.

Closing the popover with JavaScriptWe can start by setting a variable that selects the popover:

const POPOVER = document.querySelector('[popover]'); Next, we can establish a ResizeObserver that monitors the popover’s size:

const POPOVER = document.querySelector('[popover]');const OBSERVER = new ResizeObserver((entries) => { if(entries[0].contentBoxSize[0].blockSize == 0) OBSERVER.unobserve((POPOVER.hidePopover(), POPOVER)); }); And we can fire that off starting when the button to show the popover is clicked:

const POPOVER = document.querySelector('[popover]');const OBSERVER = new ResizeObserver((entries) => { if(entries[0].contentBoxSize[0].blockSize == 0) OBSERVER.unobserve((POPOVER.hidePopover(), POPOVER)); });document.querySelector('button').onclick = () => OBSERVER.observe(POPOVER); The observer will know when the popover’s CSS height reaches zero at the end of the transition, and, at that point, the popover is closed with hidePopover(). From there, the observer is stopped with unobserve().

In our example, height and ResizeObserver are used to auto-close the notification. You can try any other CSS property and JavaScript observer combination that might work with your preference. Learning about ResizeObserver and MutationObserver can help you find some options.

Setting an HTML fallbackWhen JavaScript is disabled in the browser, if the popover type is set to any of the light-dismissible types, it acts as a fallback. Keep the popover visible by overriding the style rules that hide it. The user can dismiss it by clicking or tapping anywhere outside the element.

If the popover needs to be light-dismissible only when JavaScript is disabled, then include that popover inside a <noscript> element before the manual popover. It’s the same process as before, where you override CSS styles as needed.

```

Bookmarked!

`` When to use this method?Another way to implement all of this would be to usesetTimeout()to create a delay before closing the popover in JavaScript when the button is clicked, then adding a class to thepopover` element to trigger the transition effect. That way, no observer is needed.

With the method covered in this post, the delay can be set and triggered in CSS itself, thanks to @starting-style and transition-delay — no extra class required! If you prefer to implement the delay through CSS itself, then this method works best. The JavaScript will catch up to the change CSS makes at the time CSS defines, not the other way around.


Creating an Auto-Closing Notification With an HTML Popover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

If you’re following along, this is the third post in a series about the new CSS shape() function. We’ve learned how to draw lines and arcs and, in this third part, I will introduce the curve command — the missing command you need to know to have full control over the shape() function. In reality, there are more commands, but you will rarely need them and you can easily learn about them later by checking the documentation.

Better CSS Shapes Using shape()1. Lines and Arcs 2. More on Arcs 3. Curves (you are here!) The curve commandThis command adds a Bézier curve between two points by specifying control points. We can either have one control point and create a Quadratic curve or two control points and create a Cubic curve.

Bézier, Quadratic, Cubic, control points? What?!

For many of you, that definition is simply unclear, or even useless! You can spend a few minutes reading about Bézier curves but is it really worth it? Probably not, unless your job is to create shapes all the day and you have a solid background in geometry.

We already have cubic-bezier() as an easing function for animations but, honestly, who really understands how it works? We either rely on a generator to get the code or we read a “boring” explanation that we forget in two minutes. (I have one right here by the way!)

Don’t worry, this article will not be boring as I will mostly focus on practical examples and more precisely the use case of rounding the corners of irregular shapes. Here is a figure to illustrate a few examples of Bézier curves.

The blue dots are the starting and ending points (let’s call them A and B) and the black dots are the control points. And notice how the curve is tangent to the dashed lines illustrated in red.

In this article, I will consider only one control point. The syntax will follow this pattern:

clip-path: shape( from Xa Ya, curve to Xb Yb with Xc Yc); arc command vs. curve commandWe already saw in Part 1 and Part 2 that the arc command is useful establishing rounded edges and corners, but it will not cover all the cases. That’s why you will need the curve command. The tricky part is to know when to use each one and the answer is “it depends.” There is no generic rule but my advice is to first see if it’s possible (and easy) using arc. If not, then you have to use curve.

For some shapes, we can have the same result using both commands and this is a good starting point for us to understand the curve command and compare it with arc.

Take the following example:

CodePen Embed FallbackThis is the code for the first shape:

.shape { clip-path: shape(from 0 0, arc to 100% 100% of 100% cw, line to 0 100%)} And for the second one, we have this:

.shape { clip-path: shape(from 0 0, curve to 100% 100% with 100% 0, line to 0 100%)} The arc command needs a radius (100% in this case), but the curve command needs a control point (which is 100% 0 in this example).

Now, if you look closely, you will notice that both results aren’t exactly the same. The first shape using the arc command is creating a quarter of a circle, whereas the shape using the curve command is slightly different. If you place both of them above each other, you can clearly see the difference.

CodePen Embed FallbackThis is interesting because it means we can round some corners using either an arc or a curve, but with slightly different results. Which one is better, you ask? I would say it depends on your visual preference and the shape you are creating.

In Part 1, we created rounded tabs using the arc command, but we can also create them with curve.

CodePen Embed FallbackCan you spot the difference? It’s barely visible but it’s there.

Notice how I am using the by directive the same way I am doing with arc, but this time we have the control point, which is also relative. This part can be confusing, so pay close attention to this next bit.

Consider the following:

shape(from Xa Ya, curve by Xb Yb with Xc Yc) It means that both (Xb,Yb) and (Xc,Yc) are relative coordinates calculated from the coordinate of the starting point. The equivalent of the above using a to directive is this:

shape(from Xa Ya, curve to (Xa + Xb) (Ya + Yb) with (Xa + Xc) (Yb + Yc)) We can change the reference of the control point by adding a from directive. We can either use start (the default value), end, or origin.

shape(from Xa Ya, curve by Xb Yb with Xc Yc from end) The above means that the control point will now consider the ending point instead of the starting point. The result is similar to:

shape(from Xa Ya, curve to (Xa + Xb) (Ya + Yb) with (Xa + Xb + Xc) (Ya + Yb + Yc)) If you use origin, the reference will be the origin, hence the coordinate of the control point becomes absolute instead of relative.

The from directive may add some complexity to the code and the calculation, so don’t bother yourself with it. Simply know it exists in case you face it, but keep using the default value.

I think it’s time for your first homework! Similar to the rounded tab exercise, try to create the inverted radius shape we covered in the Part 1 using curve instead of arc. Here are both versions for you to reference, but try to do it without peeking first, if you can.

CodePen Embed FallbackLet’s draw more shapes!Now that we have a good overview of the curve command, let’s consider more complex shapes where arc won’t help us round the corners and the only solution is to draw curves instead. Considering that each shape is unique, so I will focus on the technique rather than the code itself.

Slanted edgeLet’s start with a rectangular shape with a slanted edge.

Getting the shape on the left is quite simple, but the shape on the right is a bit tricky. We can round two corners with a simple border-radius, but for the slanted edge, we will use shape() and two curve commands.

The first step is to write the code of the shape without rounded corners (the left one) which is pretty straightforward since we’re only working with the line command:

.shape { --s: 90px; /* slant size */ clip-path: shape(from 0 0, line to calc(100% - var(--s)) 0, line to 100% 100%, line to 0 100% );} Then we take each corner and try to round it by modifying the code. Here is a figure to illustrate the technique I am going to use for each corner.

We define a distance, R, that controls the radius. From each side of the corner point, I move by that distance to create two new points, which are illustrated above in red. Then, I draw my curve using the new points as starting and ending points. The corner point will be the control point.

The code becomes:

.shape { --s: 90px; /* slant size */ clip-path: shape(from 0 0, Line to Xa Ya, curve to Xb Yb with calc(100% - var(--s)) 0, line to 100% 100%, line to 0 100% );} Notice how the curve is using the coordinates of the corner point in the with directive, and we have two new points, A and B.

Until now, the technique is not that complex. For each corner point, you replace the line command with line + curve commands where the curve command reuses the old point in its with directive.

If we apply the same logic to the other corner, we get the following:

.shape { --s: 90px; /* slant size */ clip-path: shape(from 0 0, line to Xa Ya, curve to Xb Yb with calc(100% - var(--s)) 0, line to Xc Yc, curve to Xd Yd with 100% 100%, line to 0 100% );} Now we need to calculate the coordinates of the new points. And here comes the tricky part because it’s not always simple and it may require some complex calculation. Even if I detail this case, the logic won’t be the same for the other shapes we’re making, so I will skip the math part and give you the final code:

.box { --h: 200px; /* element height */ --s: 90px; /* slant size */ --r: 20px; /* radius */ height: var(--h); border-radius: var(--r) 0 0 var(--r); --_a: atan2(var(--s), var(--h)); clip-path: shape(from 0 0, line to calc(100% - var(--s) - var(--r)) 0, curve by calc(var(--r) * (1 + sin(var(--_a)))) calc(var(--r) * cos(var(--_a))) with var(--r) 0, line to calc(100% - var(--r) * sin(var(--_a))) calc(100% - var(--r) * cos(var(--_a))), curve to calc(100% - var(--r)) 100% with 100% 100%, line to 0 100% );} I know the code looks a bit scary, but the good news is that the code is also really easy to control using CSS variables. So, even if the math is not easy to grasp, you don’t have to deal with it. It should be noted that I need to know the height to be able to calculate the coordinates which means the solution isn’t perfect because the height is a fixed value.

CodePen Embed FallbackArrow-shaped boxHere’s a similar shape, but this time we have three corners to round using the curve command.

CodePen Embed FallbackThe final code is still complex but I followed the same steps. I started with this:

.shape { --s: 90px; clip-path: shape(from 0 0, /* corner #1 */ line to calc(100% - var(--s)) 0, /* corner #2 */ line to 100% 50%, /* corner #3 */ line to calc(100% - var(--s)) 100%, line to 0 100% );} Then, I modified it into this:

.shape { --s: 90px; clip-path: shape(from 0 0, /* corner #1 */ line to Xa Ya curve to Xb Yb with calc(100% - var(--s)) 0, /* corner #2 */ line to Xa Ya curve to Xb Yb with 100% 50%, /* corner #3 */ line to Xa Yb curve to Xb Yb with calc(100% - var(--s)) 100%, line to 0 100% );} Lastly, I use a pen and paper to do all the calculations.

You might think this technique is useless if you are not good with math and geometry, right? Not really, because you can still grab the code and use it easily since it’s optimized using CSS variables. Plus, you aren’t obligated to be super accurate and precise. You can rely on the above technique and use trial and error to approximate the coordinates. It will probably take you less time than doing all the math.

Rounded polygonsI know you are waiting for this, right? Thanks to the new shape() and the curve command, we can now have rounded polygon shapes!

Here is my implementation using Sass where you can control the radius, number of sides and the rotation of the shape:

CodePen Embed FallbackIf we omit the complex geometry part, the loop is quite simple as it relies on the same technique with a line + curve per corner.

$n: 9; /* number of sides*/$r: .2; /* control the radius [0 1] */$a: 15deg; /* control the rotation */.poly { aspect-ratio: 1; $m: (); @for $i from 0 through ($n - 1) { $m: append($m, line to Xai Yai, comma); $m: append($m, curve to Xbi Ybi with Xci Yci, comma); } clip-path: shape(#{$m});} Here is another implementation where I define the variables in CSS instead of Sass:

CodePen Embed FallbackHaving the variables in CSS is pretty handy especially if you want to have some animations. Here is an example of a cool hover effect applied to hexagon shapes:

CodePen Embed FallbackI have also updated my online generator to add the radius parameter. If you are not familiar with Sass, you can easily copy the CSS code from there. You will also find the border-only and cut-out versions!

ConclusionAre we done with the curve command? Probably not, but we have a good overview of its potential and all the complex shapes we can build with it. As for the code, I know that we have reached a level that is not easy for everyone. I could have extended the explanation by explicitly breaking down the math, but then this article would be overly complex and make it seem like using shape() is harder than it is.

This said, most of the shapes I code are available within my online collection that I constantly update and optimize so you can easily grab the code of any shape!

If you want a good follow-up to this article, I wrote an article for Frontend Masters where you can create blob shapes using the curve command.

Better CSS Shapes Using shape()1. Lines and Arcs 2. More on Arcs 3. Curves (you are here!)


Better CSS Shapes Using shape() — Part 3: Curves originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

In many countries, web accessibility is a human right and the law, and there can be heavy fines for non-compliance. Naturally, this means that text and icons and such must have optimal color contrast in accordance with the benchmarks set by the Web Content Accessibility Guidelines (WCAG). Now, there are quite a few color contrast checkers out there (Figma even has one built-in now), but the upcoming contrast-color() function doesn’t check color contrast, it outright resolves to either black or white (whichever one contrasts the most with your chosen color).

Right off the bat, you should know that we’ve sorta looked at this feature before. Back then, however, it was called color-contrast() instead of contrast-color() and had a much more convoluted way of going about things. It was only released in Safari Technology Preview 122 back in 2021, and that’s still the case at the time I’m writing this (now at version 220).

You’d use it like this:

button { --background-color: darkblue; background-color: var(--background-color); color: contrast-color(var(--background-color));} CodePen Embed FallbackHere, contrast-color() has determined that white contrasts with darkblue better than black does, which is why contrast-color() resolves to white. Pretty simple, really, but there are a few shortcomings, which includes a lack of browser support (again, it’s only in Safari Technology Preview at the moment).

We can use contrast-color() conditionally, though:

@supports (color: contrast-color(red)) { /* contrast-color() supported */}@supports not (color: contrast-color(red)) { /* contrast-color() not supported */} The shortcomings of contrast-color()First, let me just say that improvements are already being considered, so here I’ll explain the shortcomings as well as any improvements that I’ve heard about.

Undoubtedly, the number one shortcoming is that contrast-color() only resolves to either black or white. If you don’t want black or white, well… that sucks. However, the draft spec itself alludes to more control over the resolved color in the future.

But there’s one other thing that’s surprisingly easy to overlook. What happens when neither black nor white is actually accessible against the chosen color? That’s right, it’s possible for contrast-color() to just… not provide a contrasting color. Ideally, I think we’d want contrast-color() to resolve to the closest accessible variant of a preferred color. Until then, contrast-color() isn’t really usable.

Another shortcoming of contrast-color() is that it only accepts arguments of the <color> data type, so it’s just not going to work with images or anything like that. I did, however, manage to make it “work” with a gradient (basically, two instances of contrast-color() for two color stops/one linear gradient):

CodePen Embed Fallback <button> <span>A button</span></button>

button { background: linear-gradient(to right, red, blue); span { background: linear-gradient(to right, contrast-color(red), contrast-color(blue)); color: transparent; background-clip: text; }} The reason this looks so horrid is that, as mentioned before, contrast-color() only resolves to black or white, so in the middle of the gradient we essentially have 50% grey on purple. This problem would also get solved by contrast-color() resolving to a wider spectrum of colors.

But what about the font size? As you might know already, the criteria for color contrast depends on the font size, so how does that work? Well, at the moment it doesn’t, but I think it’s safe to assume that it’ll eventually take the font-size into account when determining the resolved color. Which brings us to APCA.

APCA (Accessible Perceptual Contrast Algorithm) is a new algorithm for measuring color contrast reliably. Andrew Somers, creator of APCA, conducted studies (alongside many other independent studies) and learned that 23% of WCAG 2 “Fails” are actually accessible. In addition, an insane 47% of “Passes” are inaccessible.

Not only should APCA do a better job, but the APCA Readability Criterion (ARC) is far more nuanced, taking into account a much wider spectrum of font sizes and weights (hooray for me, as I’m very partial to 600 as a standard font weight). While the criterion is expectedly complex and unnecessarily confusing, the APCA Contrast Calculator does a decent-enough job of explaining how it all works visually, for now.

contrast-color() doesn’t use APCA, but the draft spec does allude to offering more algorithms in the future. This wording is odd as it suggests that we’ll be able to choose between the APCA and WCAG algorithms. Then again, we have to remember that the laws of some countries will require WCAG 2 compliance while others require WCAG 3 compliance (when it becomes a standard).

That’s right, we’re a long way off of APCA becoming a part of WCAG 3, let alone contrast-color(). In fact, it might not even be a part of it initially (or at all), and there are many more hurdles after that, but hopefully this sheds some light on the whole thing. For now, contrast-color() is using WCAG 2 only.

Using contrast-color()Here’s a simple example (the same one from earlier) of a darkblue-colored button with accessibly-colored text chosen by contrast-color(). I’ve put this darkblue color into a CSS variable so that we can define it once but reference it as many times as is necessary (which is just twice for now).

button { --background-color: darkblue; background-color: var(--background-color); /* Resolves to white */ color: contrast-color(var(--background-color));} And the same thing but with lightblue:

button { --background-color: lightblue; background-color: var(--background-color); /* Resolves to black */ color: contrast-color(var(--background-color));} First of all, we can absolutely switch this up and use contrast-color() on the background-color property instead (or in-place of any <color>, in fact, like on a border):

button { --color: darkblue; color: var(--color); /* Resolves to white */ background-color: contrast-color(var(--color));} Any valid <color> will work (named, HEX, RGB, HSL, HWB, etc.):

button { /* HSL this time */ --background-color: hsl(0 0% 0%); background-color: var(--background-color); /* Resolves to white */ color: contrast-color(var(--background-color));} Need to change the base color on the fly (e.g., on hover)? Easy:

button { --background-color: hsl(0 0% 0%); background-color: var(--background-color); /* Starts off white, becomes black on hover */ color: contrast-color(var(--background-color)); &:hover { /* 50% lighter */ --background-color: hsl(0 0% 50%); }} CodePen Embed FallbackSimilarly, we could use contrast-color() with the light-dark() function to ensure accessible color contrast across light and dark modes:

:root { /* Dark mode if checked */ &:has(input[type="checkbox"]:checked) { color-scheme: dark; } /* Light mode if not checked */ &:not(:has(input[type="checkbox"]:checked)) { color-scheme: light; } body { /* Different background for each mode */ background: light-dark(hsl(0 0% 50%), hsl(0 0% 0%)); /* Different contrasted color for each mode */ color: light-dark(contrast-color(hsl(0 0% 50%)), contrast-color(hsl(0 0% 0%)); }} CodePen Embed FallbackThe interesting thing about APCA is that it accounts for the discrepancies between light mode and dark mode contrast, whereas the current WCAG algorithm often evaluates dark mode contrast inaccurately. This one nuance of many is why we need not only a new color contrast algorithm but also the contrast-color() CSS function to handle all of these nuances (font size, font weight, etc.) for us.

This doesn’t mean that contrast-color() has to ensure accessibility at the expense of our “designed” colors, though. Instead, we can use contrast-color() within the prefers-contrast: more media query only:

button { --background-color: hsl(270 100% 50%); background-color: var(--background-color); /* Almost white (WCAG AA: Fail) */ color: hsl(270 100% 90%); @media (prefers-contrast: more) { /* Resolves to white (WCAG AA: Pass) */ color: contrast-color(var(--background-color)); }} Personally, I’m not keen on prefers-contrast: more as a progressive enhancement. Great color contrast benefits everyone, and besides, we can’t be sure that those who need more contrast are actually set up for it. Perhaps they’re using a brand new computer, or they just don’t know how to customize accessibility settings.

Closing thoughtsSo, contrast-color() obviously isn’t useful in its current form as it only resolves to black or white, which might not be accessible. However, if it were improved to resolve to a wider spectrum of colors, that’d be awesome. Even better, if it were to upgrade colors to a certain standard (e.g., WCAG AA) if they don’t already meet it, but let them be if they do. Sort of like a failsafe approach? This means that web browsers would have to take the font size, font weight, element, and so on into account.

To throw another option out there, there’s also the approach that Windows takes for its High Contrast Mode. This mode triggers web browsers to overwrite colors using the forced-colors: active media query, which we can also use to make further customizations. However, this effect is quite extreme (even though we can opt out of it using the forced-colors-adjust CSS property and use our own colors instead) and macOS’s version of the feature doesn’t extend to the web.

I think that forced colors is an incredible idea as long as users can set their contrast preferences when they set up their computer or browser (the browser would be more enforceable), and there are a wider range of contrast options. And then if you, as a designer or developer, don’t like the enforced colors, then you have the option to meet accessibility standards so that they don’t get enforced. In my opinion, this approach is the most user-friendly and the most developer-friendly (assuming that you care about accessibility). For complete flexibility, there could be a CSS property for opting out, or something. Just color contrast by default, but you can keep the colors you’ve chosen as long as they’re accessible.

What do you think? Is contrast-color() the right approach, or should the user agent bear some or all of the responsibility? Or perhaps you’re happy for color contrast to be considered manually?


Exploring the CSS contrast-color() Function… a Second Time originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The State of CSS 2025 Survey dropped a few days ago, and besides waiting for the results, it’s exciting to see a lot of the new things shipped to CSS over the past year reflected in the questions. To be specific, the next survey covers the following features:

  • calc-size()
  • shape()
  • Scroll-driven animations
  • Container scroll-state queries
  • CSS Carousels
  • text-box-edge and text-box-trim
  • field-sizing
  • ::target-text
  • @function
  • display: contents
  • Advanced attr()
  • if()
  • sibling-index() and sibling-count()

Again, a lot!

However, I think the most important questions (regarding CSS) are asked at the end of each section. I am talking about the “What are your top CSS pain points related to ______?” questions. These sections are optional, but help user agents and the CSS Working Group know what they should focus on next.

By nature of comments, those respondents with strong opinions are most likely to fill them in, skewing data towards issues that maybe the majority doesn’t have. So, even if you don’t have a hard-set view on a CSS pain point, I encourage you to fill them — even with your mild annoyances.


The State of CSS 2025 Survey is out! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Like ’em or loath ’em, whether you’re showing an alert, a message, or a newsletter signup, dialogue boxes draw attention to a particular piece of content without sending someone to a different page. In the past, dialogues relied on a mix of divisions, ARIA, and JavaScript. But the HTML dialog element has made them more accessible and style-able in countless ways.

So, how can you take dialogue box design beyond the generic look of frameworks and templates? How can you style them to reflect a brand’s visual identity and help to tell its stories? Here’s how I do it in CSS using ::backdrop, backdrop-filter, and animations.

Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen.I mentioned before that Emmy-award-winning game composer Mike Worth hired me to create a highly graphical design. Mike loves ’90s animation, and he challenged me to find ways to incorporate its retro style without making a pastiche. However, I also needed to achieve that retro feel while maintaining accessibility, performance, responsiveness, and semantics.

A brief overview of dialog and ::backdropLet’s run through a quick refresher.

Note: While I mostly refer to “dialogue boxes” throughout, the HTML element is spelt dialog.

dialog is an HTML element designed for implementing modal and non-modal dialogue boxes in products and website interfaces. It comes with built-in functionality, including closing a box using the keyboard Esc key, focus trapping to keep it inside the box, show and hide methods, and a ::backdrop pseudo-element for styling a box’s overlay.

The HTML markup is just what you might expect:

<dialog> <h2>Keep me informed</h2> <!-- ... --> <button>Close</button></dialog> This type of dialogue box is hidden by default, but adding the open attribute makes it visible when the page loads:

<dialog open> <h2>Keep me informed</h2> <!-- ... --> <button>Close</button></dialog> I can’t imagine too many applications for non-modals which are open by default, so ordinarily I need a button which opens a dialogue box:

<dialog> <!-- ... --></dialog><button>Keep me informed</button> Plus a little bit of JavaScript, which opens the modal:

const dialog = document.querySelector("dialog");const showButton = document.querySelector("dialog + button");showButton.addEventListener("click", () => { dialog.showModal();}); Closing a dialogue box also requires JavaScript:

const closeButton = document.querySelector("dialog button");closeButton.addEventListener("click", () => { dialog.close();}); Unless the box contains a form using method="dialog", which allows it to close automatically on submit without JavaScript:

<dialog> <form method="dialog"> <button>Submit</button> </form></dialog> The dialog element was developed to be accessible out of the box. It traps focus, supports the Esc key, and behaves like a proper modal. But to help screen readers announce dialogue boxes properly, you’ll want to add an aria-labelledby attribute. This tells assistive technology where to find the dialogue box’s title so it can be read aloud when the modal opens.

<dialog aria-labelledby="dialog-title"> <h2 id="dialog-title">Keep me informed</h2> <!-- ... --></dialog> Most tutorials I’ve seen include very little styling for dialog and ::backdrop, which might explain why so many dialogue boxes have little more than border radii and a box-shadow applied.

Out-of-the-box dialogue designsI believe that every element in a design — no matter how small or infrequently seen — is an opportunity to present a brand and tell a story about its products or services. I know there are moments during someone’s journey through a design where paying special attention to design can make their experience more memorable.

Dialogue boxes are just one of those moments, and Mike Worth’s design offers plenty of opportunities to reflect his brand or connect directly to someone’s place in Mike’s story. That might be by styling a newsletter sign-up dialogue to match the scrolls in his news section.

Mike Worth concept design, designed by Andy Clarke, Stuff & Nonsense.Or making the form modal on his error pages look like a comic-book speech balloon.

Mike Worth concept design, designed by Andy Clarke, Stuff & Nonsense.dialog in actionMike’s drop-down navigation menu looks like an ancient stone tablet.

Mike Worth, designed by Andy Clarke, Stuff & Nonsense.I wanted to extend this look to his dialogue boxes with a three-dimensional tablet and a jungle leaf-filled backdrop.

Mike Worth, designed by Andy Clarke, Stuff & Nonsense.This dialog contains a newsletter sign-up form with an email input and a submit button:

<dialog> <h2>Keep me informed</h2> <form> <label for="email" data-visibility="hidden">Email address</label> <input type="email" id="email" required> <button>Submit</button> </form> <button>x</button></dialog> I started by applying dimensions to the dialog and adding the SVG stone tablet background image:

dialog { width: 420px; height: 480px; background-color: transparent; background-image: url("dialog.svg"); background-repeat: no-repeat; background-size: contain;} Then, I added the leafy green background image to the dialogue box’s generated backdrop using the ::backdrop pseudo element selector:

dialog::backdrop { background-image: url("backdrop.svg"); background-size: cover;} Mike Worth, designed by Andy Clarke, Stuff & Nonsense.I needed to make it clear to anyone filling in Mike’s form that their email address is in a valid format. So I combined :has and :valid CSS pseudo-class selectors to change the color of the submit button from grey to green:

dialog:has(input:valid) button { background-color: #7e8943; color: #fff;} I also wanted this interaction to reflect Mike’s fun personality. So, I also changed the dialog background image and applied a rubberband animation to the box when someone inputs a valid email address:

dialog:has(input:valid) { background-image: url("dialog-valid.svg"); animation: rubberBand 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;}@keyframes rubberBand { from { transform: scale3d(1, 1, 1); } 30% { transform: scale3d(1.25, 0.75, 1); } 40% { transform: scale3d(0.75, 1.25, 1); } 50% { transform: scale3d(1.15, 0.85, 1); } 65% { transform: scale3d(0.95, 1.05, 1); } 75% { transform: scale3d(1.05, 0.95, 1); } to { transform: scale3d(1, 1, 1); }} Tip: Daniel Eden’s Animate.css library is a fabulous source of “Just-add-water CSS animations” like the rubberband I used for this dialogue box.

Changing how an element looks when it contains a valid input is a fabulous way to add interactions that are, at the same time, fun and valuable for the user.

Mike Worth, designed by Andy Clarke, Stuff & Nonsense.That combination of :has and :valid selectors can even be extended to the ::backdrop pseudo-class, to change the backdrop’s background image:

dialog:has(input:valid)::backdrop { background-image: url("backdrop-valid.svg");} Try it for yourself:

CodePen Embed FallbackConclusionWe often think of dialogue boxes as functional elements, as necessary interruptions, but nothing more. But when you treat them as opportunities for expression, even the smallest parts of a design can help shape a product or website’s personality.

The HTML dialog element, with its built-in behaviours and styling potential, opens up opportunities for branding and creative storytelling. There’s no reason a dialogue box can’t be as distinctive as the rest of your design.


Andy ClarkeOften referred to as one of the pioneers of web design, Andy Clarke has been instrumental in pushing the boundaries of web design and is known for his creative and visually stunning designs. His work has inspired countless designers to explore the full potential of product and website design.

Andy’s written several industry-leading books, including ‘Transcending CSS,’ ‘Hardboiled Web Design,’ and ‘Art Direction for the Web.’ He’s also worked with businesses of all sizes and industries to achieve their goals through design.

Visit Andy’s studio, Stuff & Nonsense, and check out his Contract Killer, the popular web design contract template trusted by thousands of web designers and developers.


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

View Details

Ready for the second part? We are still exploring the shape() function, and more precisely, the arc command. I hope you took the time to digest the first part because we will jump straight into creating more shapes!

As a reminder, the shape() function is only supported in Chrome 137+ and Safari 18.4+ as I’m writing this in May 2025.

Better CSS Shapes Using shape()1. Lines and Arcs 2. More on Arcs (you are here!) 3. Curves Sector shapeAnother classic shape that can also be used in pie-like charts.

It’s already clear that we have one arc. As for the points, we have two points that don’t move and one that moves depending on how much the sector is filled.

The code will look like this:

.sector { --v: 35; /* [0 100]*/ aspect-ratio: 1; clip-path: shape(from top, arc to X Y of R, line to center);} We define a variable that will control the filling of the sector. It has a value between 0 and 100. To draw the shape, we start from the top, create an arc until the point (X, Y), and then we move to the center.

Are we allowed to use keyword values like top and center?

Yes! Unlike the polygon() function, we have keywords for the particular cases such as top, bottom, left, etc. It’s exactly like background-position that way. I don’t think I need to detail this part as it’s trivial, but it’s good to know because it can make your shape a bit easier to read.

The radius of the arc should be equal to 50%. We are working with a square element and the sector, which is a portion of a circle, need to fill the whole element so the radius is equal to half the width (or height).1

As for the point, it’s placed within that circle, and its position depends on the V value. You don’t want a boring math explanation, right? No need for it, here is the formula of X and Y:

X = 50% + 50% * sin(V * 3.6deg)Y = 50% - 50% * cos(V * 3.6deg) Our code becomes:

.sector { --v: 35; /* [0 100] */ aspect-ratio: 1; clip-path: shape(from top, arc to calc(50% + 50% * sin(var(--v) * 3.6deg)) calc(50% - 50% * cos(var(--v) * 3.6deg)) of 50%, line to center);} CodePen Embed FallbackHmm, the result is not good, but there are no mistakes in the code. Can you figure out what we are missing?

It’s the size and direction of the arc!

Remember what I told you in the last article? You will always have trouble with them, but if we try the different combinations, we can easily fix the issue. In our case, we need to use: small cw.

CodePen Embed FallbackBetter! Let’s try it with more values and see how the shape behaves:

CodePen Embed FallbackOops, some values are good, but others not so much. The direction needs to be clockwise, but maybe we should use large instead of small? Let’s try:

CodePen Embed FallbackStill not working. The issue here is that we are moving one point of the arc based on the V value, and this movement creates a different configuration for the arc command.

Here is an interactive demo to better visualize what is happening:

CodePen Embed FallbackWhen you update the value, notice how large cw always tries to follow the largest arc between the points, while small cw tries to follow the smallest one. When the value is smaller than 50, small cw gives us a good result. But when it’s bigger than 50, the large cw combination is the good one.

I know, it’s a bit tricky and I wanted to study this particular example to emphasize the fact that we can have a lot of headaches working with arcs. But the more issues we face, the better we get at fixing them.

The solution in this case is pretty simple. We keep the use of large cw and add a border-radius to the element. If you check the previous demo, you will notice that even if large cw is not producing a good result, it’s filling the area we want. All we need to do is clip the extra space and a simple border-radius: 50% will do the job!

CodePen Embed FallbackI am keeping the box-shadow in there so we can see the arc, but we can clearly see how border-radius is making a difference on the main shape.

There is still one edge case we need to consider. When the value is equal to 100, both points of the arc will have the same coordinates, which is logical since the sector is full and we have a circle. But when it’s the case, the arc will do nothing by definition and we won’t get a full circle.

To fix this, we can limit the value to, for example, 99.99 to avoid reaching 100. It’s kind of hacky, but it does the job.

.sector { --v: 35; /* [0 100]*/ --_v: min(99.99, var(--v)); aspect-ratio: 1; clip-path: shape(from top, arc to calc(50% + 50% * sin(var(--_v) * 3.6deg)) calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% large cw, line to center); border-radius: 50%;} Now our shape is perfect! And don’t forget that you can apply it to image elements:

CodePen Embed FallbackArc shapeSimilar to the sector shape, we can also create an arc shape. After all, we are working with the arc command, so we have to do it.

We already have half the code since it’s basically a sector shape without the inner part. We simply need to add more commands to cut the inner part.

.arc { --v: 35; --b: 30px; --_v: min(99.99, var(--v)); aspect-ratio: 1; clip-path: shape(from top, arc to calc(50% + 50% * sin(var(--_v) * 3.6deg)) calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% cw large, line to calc(50% + (50% - var(--b)) * sin(var(--_v) * 3.6deg)) calc(50% - (50% - var(--b)) * cos(var(--_v) * 3.6deg)), arc to 50% var(--b) of calc(50% - var(--b)) large ); border-radius: 50%;} From the sector shape, we remove the line to center piece and replace it with another line command that moves to a point placed on the inner circle. If you compare its coordinates with the previous point, you will see an offset equal to --b, which is a variable that defines the arc’s thickness. Then we draw an arc in the opposite direction (ccw) until the point 50% var(--b), which is also a point with an offset equal to --b from the top.

I am not defining the direction of the second arc since, by default, the browser will use ccw.

CodePen Embed FallbackAh, the same issue we hit with the sector shape is striking again! Not all the values are giving a good result due to the same logic we saw earlier, and, as you can see, border-radius is not fixing it. This time, we need to find a way to conditionally change the size of the arc based on the value. It should be large when V is bigger than 50, and small otherwise.

Conditions in CSS? Yes, it’s possible! First, let’s convert the V value like this:

--_f: round(down, var(--_v), 50) The value is within the range [0 99.99] (don’t forget that we don’t want to reach the value 100). We use round() to make sure it’s always equal to a multiple of a specific value, which is 50 in our case. If the value is smaller than 50, the result is 0, otherwise it’s 50.

There are only two possible values, so we can easily add a condition. If --_f is equal to 0 we use small; otherwise, we use large:

.arc { --v: 35; --b: 30px; --_v: min(99.99, var(--v)); --_f: round(down,var(--_v), 50); --_c: if(style(--_f: 0): small; else: large); clip-path: shape(from top, arc to calc(50% + 50% * sin(var(--_v) * 3.6deg)) calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% cw var(--_c), line to calc(50% + (50% - var(--b)) * sin(var(--_v) * 3.6deg)) calc(50% - (50% - var(--b)) * cos(var(--_v) * 3.6deg)), arc to 50% var(--b) of calc(50% - var(--b)) var(--_c) );} I know what you are thinking, but let me tell you that the above code is valid. You probably don’t know it yet, but CSS has recently introduced inline conditionals using an if() syntax. It’s still early to play with it, but we have found a perfect use case for it. Here is a demo that you can test using Chrome Canary:

CodePen Embed FallbackAnother way to express conditions is to rely on style queries that have better support:

.arc { --v: 35; --b: 30px; --_v: min(99.99, var(--v)); --_f: round(down, var(--_v), 50); aspect-ratio: 1; container-name: arc;}.arc:before { content: ""; clip-path: shape(from top, arc to calc(50% + 50% * sin(var(--_v) * 3.6deg)) calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% cw var(--_c, large), line to calc(50% + (50% - var(--b)) * sin(var(--_v) * 3.6deg)) calc(50% - (50% - var(--b)) * cos(var(--_v) * 3.6deg)), arc to 50% var(--b) of calc(50% - var(--b)) var(--_c, large) ); @container style(--_f: 0) { --_c: small }} The logic is the same but, this feature requires a parent-child relation, which is why I am using a pseudo-element. By default, the size will be large, and if the value of --_f is equal to 0, we switch to small.

CodePen Embed FallbackNote that we have to register the variable --_f using @property to be able to either use the if() function or style queries.

Did you notice another subtle change I have made to the shape? I removed border-radius and I applied the conditional logic to the first arc. Both have the same issue, but border-radius can fix only one of them while the conditional logic can fix both, so we can optimize the code a little.

Arc shape with rounded edgesWhat about adding rounded edges to our arc? It’s better, right?

Can you see how it’s done? Take it as a small exercise and update the code from the previous examples to add those rounded edges. I hope you are able to find it by yourself because the changes are pretty straightforward — we update one line command with an arc command and we add another arc command at the end.

clip-path: shape(from top, arc to calc(50% + 50% * sin(var(--_v) * 3.6deg)) calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% cw var(--_c, large), arc to calc(50% + (50% - var(--b)) * sin(var(--_v) * 3.6deg)) calc(50% - (50% - var(--b)) * cos(var(--_v) * 3.6deg)) of 1% cw, arc to 50% var(--b) of calc(50% - var(--b)) var(--_c, large), arc to top of 1% cw); If you do not understand the changes, get out a pen and paper, then draw the shape to better see the four arcs we are drawing. Previously, we had two arcs and two lines, but now we are working with arcs instead of lines.

And did you remember the trick of using a 1% value for the radius? The new arcs are half circles, so we can rely on that trick where you specify a tiny radius and the browser will do the job for you and find the correct value!

CodePen Embed FallbackConclusionWe are done — enough about the arc command! I had to write two articles that focus on this command because it’s the trickiest one, but I hope it’s now clear how to use it and how to handle the direction and size thing, as that is probably the source of most headaches.

By the way, I have only studied the case of circular arcs because, in reality, we can specify two radii and draw elliptical ones, which is even more complex. Unless you want to become a shape() master, you will rarely need elliptical arcs, so don’t bother yourself with them.

Until the next article, I wrote an article for Frontend Masters where you can create more fancy shapes using the arc command that is a good follow-up to this one.

Better CSS Shapes Using shape()1. Lines and Arcs 2. More on Arcs (you are here!) 3. Curves


Footnotes(1) The arc command is defined to draw elliptical arcs by taking two radii, but if we define one radius value, it means that the vertical and horizontal radius will use that same value and we have circular arcs. When it’s a length, it’s trivial, but when we use percentages, the value will resolve against the direction-agnostic size, which is equal to the length of the diagonal of the box, divided by sqrt(2).

In our case, we have a square element so 50% of the direction-agnostic size will be equal to 50% of sqrt(Width² + Height²)/sqrt(2). And since both width and height are equal, we end with 50% of the width (or the height). ⮑


Better CSS Shapes Using shape() — Part 2: More on Arcs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The reading-flow and reading-order proposed CSS properties are designed to specify the source order of HTML elements in the DOM tree, or in simpler terms, how accessibility tools deduce the order of elements. You’d use them to make the focus order of focusable elements match the visual order, as outlined in the Web Content Accessibility Guidelines (WCAG 2.2).

To get a better idea, let’s just dive in!

(Oh, and make sure that you’re using Chrome 137 or higher.)

reading-flow``reading-flow determines the source order of HTML elements in a flex, grid, or block layout. Again, this is basically to help accessibility tools provide the correct focus order to users.

The default value is normal (so, reading-flow: normal). Other valid values include:

  • flex-visual
  • flex-flow
  • grid-rows
  • grid-columns
  • grid-order
  • source-order

Let’s start with the flex-visual value. Imagine a flex row with five links. Assuming that the reading direction is left-to-right (by the way, you can change the reading direction with the direction CSS property), that’d look something like this:

CodePen Embed FallbackNow, if we apply flex-direction: row-reverse, the links are displayed 5-4-3-2-1. The problem though is that the focus order still starts from 1 (tab through them!), which is visually wrong for somebody that reads left-to-right.

CodePen Embed FallbackBut if we also apply reading-flow: flex-visual, the focus order also becomes 5-4-3-2-1, matching the visual order (which is an accessibility requirement!):

```

1 2 3 4 5

```

div { display: flex; flex-direction: row-reverse; reading-flow: flex-visual;} CodePen Embed FallbackTo apply the default flex behavior, reading-flow: flex-flow is what you’re looking for. This is very akin to reading-flow: normal, except that the container remains a reading flow container, which is needed for reading-order (we’ll dive into this in a bit).

For now, let’s take a look at the grid-y values. In the grid below, the grid items are all jumbled up, and so the focus order is all over the place.

CodePen Embed FallbackWe can fix this in two ways. One way is that reading-flow: grid-rows will, as you’d expect, establish a row-by-row focus order:

```

1 2 3 4 5 6 7 8 9 10 11 12

```

div { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 100px; reading-flow: grid-rows; a:nth-child(2) { grid-row: 2 / 4; grid-column: 3; } a:nth-child(5) { grid-row: 1 / 3; grid-column: 1 / 3; }} CodePen Embed FallbackOr, reading-flow: grid-columns will establish a column-by-column focus order:

CodePen Embed Fallbackreading-flow: grid-order will give us the default grid behavior (i.e., the jumbled up version). This is also very akin to reading-flow: normal (except that, again, the container remains a reading flow container, which is needed for reading-order).

There’s also reading-flow: source-order, which is for flex, grid, and block containers. It basically turns containers into reading flow containers, enabling us to use reading-order. To be frank, unless I’m missing something, this appears to make the flex-flow and grid-order values redundant?

reading-order``reading-order sort of does the same thing as reading-flow. The difference is that reading-order is for specific flex or grid items, or even elements in a simple block container. It works the same way as the order property, although I suppose we could also compare it to tabindex.

Note: To use reading-order, the container must have the reading-flow property set to anything other than normal.

I’ll demonstrate both reading-order and order at the same time. In the example below, we have another flex container where each flex item has the order property set to a different random number, making the order of the flex items random. Now, we’ve already established that we can use reading-flow to determine focus order regardless of visual order, but in the example below we’re using reading-order instead (in the exact same way as order):

div { display: flex; reading-flow: source-order; /* Anything but normal */ /* Features at the end because of the higher values */ a:nth-child(1) { /* Visual order */ order: 567; /* Focus order */ reading-order: 567; } a:nth-child(2) { order: 456; reading-order: 456; } a:nth-child(3) { order: 345; reading-order: 345; } a:nth-child(4) { order: 234; reading-order: 234; } /* Features at the beginning because of the lower values */ a:nth-child(5) { order: -123; reading-order: -123; }} CodePen Embed FallbackYes, those are some rather odd numbers. I’ve done this to illustrate how the numbers don’t represent the position (e.g., order: 3 or reading-order: 3 doesn’t make it third in the order). Instead, elements with lower numbers are more towards the beginning of the order and elements with higher numbers are more towards the end. The default value is 0. Elements with the same value will be ordered by source order.

In practical terms? Consider the following example:

div { display: flex; reading-flow: source-order; a:nth-child(1) { order: 1; reading-order: 1; } a:nth-child(5) { order: -1; reading-order: -1; }} CodePen Embed FallbackOf the five flex items, the first one is the one with order: -1 because it has the lowest order value. The last one is the one with order: 1 because it has the highest order value. The ones with no declaration default to having order: 0 and are thus ordered in source order, but otherwise fit in-between the order: -1 and order: 1 flex items. And it’s the same concept for reading-order, which in the example above mirrors order.

However, when reversing the direction of flex items, keep in mind that order and reading-order work a little differently. For example, reading-order: -1 would, as expected, and pull a flex item to the beginning of the focus order. Meanwhile, order: -1 would pull it to the end of the visual order because the visual order is reversed (so we’d need to use order: 1 instead, even if that doesn’t seem right!):

div { display: flex; flex-direction: row-reverse; reading-flow: source-order; a:nth-child(5) { /* Because of row-reverse, this actually makes it first */ order: 1; /* However, this behavior doesn’t apply to reading-order */ reading-order: -1; }} CodePen Embed Fallbackreading-order overrides reading-flow. If we, for example, apply reading-flow: flex-visual, reading-flow: grid-rows, or reading-flow: grid-columns (basically, any declaration that does in fact change the reading flow), reading-order overrides it. We could say that reading-order is applied after reading-flow.

What if I don’t want to use flexbox or grid layout?Well, that obviously rules out all of the flex-y and grid-y reading-flow values; however, you can still set reading-flow: source-order on a block element and then manipulate the focus order with reading-order (as we did above).

How does this relate to the tabindex HTML attribute?They’re not equivalent. Negative tabindex values make targets unfocusable and values other than 0 and -1 aren’t recommended, whereas a reading-order declaration can use any number as it’s only contextual to the reading flow container that contains it.

For the sake of being complete though, I did test reading-order and tabindex together and reading-order appeared to override tabindex.

Going forward, I’d only use tabindex (specifically, tabindex="-1") to prevent certain targets from being focusable (the disabled attribute will be more appropriate for some targets though), and then reading-order for everything else.

Closing thoughtsBeing able to define reading order is useful, or at least it means that the order property can finally be used as intended. Up until now (or rather when all web browsers support reading-flow and reading-order, because they only work in Chrome 137+ at the moment), order hasn’t been useful because we haven’t been able to make the focus order match the visual order.


What We Know (So Far) About CSS Reading Order originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Creating CSS Shapes is a classic and one of my favorite exercise. Indeed, I have one of the biggest collections of CSS Shapes from where you can easily copy the code of any shape. I also wrote an extensive guide on how to create them: The Modern Guide For Making CSS Shapes.

Even if I have detailed most of the modern techniques and tricks, CSS keeps evolving, and new stuff always emerges to simplify our developer life. Recently, clip-path was upgraded to have a new shape() value. A real game changer!

Better CSS Shapes Using shape()1. Lines and Arcs (you are here!) 2. More on Arcs 3. Curves Before we jump in, it’s worth calling out that the shape() function is currently only supported in Chrome 137+ and Safari 18.4+ as I’m writing this in May 2025.

What is shape()?Let me quote the description from the official specification:

While the path() function allows reuse of the SVG path syntax to define more arbitrary shapes than allowed by more specialized shape functions, it requires writing a path as a single string (which is not compatible with, for example, building a path piecemeal with var()), and inherits a number of limitations from SVG, such as implicitly only allowing the px unit.

The shape() function uses a set of commands roughly equivalent to the ones used by path(), but does so with more standard CSS syntax, and allows the full range of CSS functionality, such as additional units and math functions.

In other words, we have the SVG features in the CSS side that we can combine with existing features such as var(), calc(), different units, etc. SVG is already good at drawing complex shapes, so imagine what is possible with something more powerful.

If you keep reading the spec, you will find:

In that sense, shape() is a superset of path(). A path() can be easily converted to a shape(), but to convert a shape() back to a path() or to SVG requires information about the CSS environment.

And guess what? I already created an online converter from SVG to CSS. Save this tool because it will be very handy. If you are already good at creating SVG shapes or you have existing codes, no need to reinvent the wheel. You paste your code in the generator, and you get the CSS code that you can easily tweak later.

Let’s try with the CSS-Tricks logo. Here is the SVG I picked from the website:

<svg width="35px" height="35px" viewBox="0 0 362.62 388.52" > <path d="M156.58,239l-88.3,64.75c-10.59,7.06-18.84,11.77-29.43,11.77-21.19,0-38.85-18.84-38.85-40C0,257.83,14.13,244.88,27.08,239l103.6-44.74L27.08,148.34C13,142.46,0,129.51,0,111.85,0,90.66,18.84,73,40,73c10.6,0,17.66,3.53,28.25,11.77l88.3,64.75L144.81,44.74C141.28,20,157.76,0,181.31,0s40,18.84,36.5,43.56L206,149.52l88.3-64.75C304.93,76.53,313.17,73,323.77,73a39.2,39.2,0,0,1,38.85,38.85c0,18.84-12.95,30.61-27.08,36.5L231.93,194.26,335.54,239c14.13,5.88,27.08,18.83,27.08,37.67,0,21.19-18.84,38.85-40,38.85-9.42,0-17.66-4.71-28.26-11.77L206,239l11.77,104.78c3.53,24.72-12.95,44.74-36.5,44.74s-40-18.84-36.5-43.56Z"></path></svg> You take the value inside the d attribute, paste it in the converter, and boom! You have the following CSS:

.shape { aspect-ratio: 0.933; clip-path: shape(from 43.18% 61.52%,line by -24.35% 16.67%,curve by -8.12% 3.03% with -2.92% 1.82%/-5.2% 3.03%,curve by -10.71% -10.3% with -5.84% 0%/-10.71% -4.85%,curve to 7.47% 61.52% with 0% 66.36%/3.9% 63.03%,line by 28.57% -11.52%,line to 7.47% 38.18%,curve to 0% 28.79% with 3.59% 36.67%/0% 33.33%,curve to 11.03% 18.79% with 0% 23.33%/5.2% 18.79%,curve by 7.79% 3.03% with 2.92% 0%/4.87% 0.91%,line by 24.35% 16.67%,line to 39.93% 11.52%,curve to 50% 0% with 38.96% 5.15%/43.51% 0%,smooth by 10.07% 11.21% with 11.03% 4.85%,line to 56.81% 38.48%,line by 24.35% -16.67%,curve to 89.29% 18.79% with 84.09% 19.7%/86.36% 18.79%,arc by 10.71% 10% of 10.81% 10.09% small cw,curve by -7.47% 9.39% with 0% 4.85%/-3.57% 7.88%,line to 63.96% 50%,line to 92.53% 61.52%,curve by 7.47% 9.7% with 3.9% 1.51%/7.47% 4.85%,curve by -11.03% 10% with 0% 5.45%/-5.2% 10%,curve by -7.79% -3.03% with -2.6% 0%/-4.87% -1.21%,line to 56.81% 61.52%,line by 3.25% 26.97%,curve by -10.07% 11.52% with 0.97% 6.36%/-3.57% 11.52%,smooth by -10.07% -11.21% with -11.03% -4.85%,close);} Note that you don’t need to provide any viewBox data. The converter will automatically find the smallest rectangle for the shape and will calculate the coordinates of the points accordingly. No more viewBox headaches and no need to fight with overflow or extra spacing!

CodePen Embed FallbackHere is another example where I am applying the shape to an image element. I am keeping the original SVG so you can compare both shapes.

CodePen Embed FallbackWhen to use shape()I would be tempted to say “all the time” but in reality, not. In my guide, I distinguish between two types of shapes: The ones with only straight lines and the ones with curves. Each type can either have repetition or not. In the end, we have four categories of shapes.

If we don’t have curves and we don’t have repetition (the easiest case), then clip-path: polygon() should do the job. If we have a repetition (with or without curves), then mask is the way to go. With mask, we can rely on gradients that can have a specific size and repeat, but with clip-path we don’t have such options.

If you have curves and don’t have a repetition, the new shape() is the best option. Previously, we had to rely on mask since clip-path is very limited, but that’s no longer the case. Of course, these are not universal rules, but my own way to identify which option is the most suitable. At the end of the day, it’s always a case-by-case basis as we may have other things to consider, such as the complexity of the code, the flexibility of the method, browser support, etc.

Let’s draw some shapes!Enough talking, let’s move to the interesting part: drawing shapes. I will not write a tutorial to explain the “complex” syntax of shape(). It will be boring and not interesting. Instead, we will draw some common shapes and learn by practice!

RectangleTake the following polygon:

clip-path: polygon( 0 0, 100% 0, 100% 100%, 0 100%); Technically, this will do nothing since it will draw a rectangle that already follows the element shape which is a rectangle, but it’s still the perfect starting point for us.

Now, let’s write it using shape().

clip-path: shape( from 0 0, line to 100% 0, line to 100% 100%, line to 0 100%); The code should be self-explanatory and we already have two commands. The from command is always the first command and is used only once. It simply specifies the starting point. Then we have the line command that draws a line to the next point. Nothing complex so far.

We can still write it differently like below:

clip-path: shape( from 0 0, hline to 100%, vline to 100%, hline to 0); Between the points 0 0 and 100% 0, only the first value is changing which means we are drawing a horizontal line from 0 0 to 100% 0, hence the use of hline to 100% where you only need to specify the horizontal offset. It’s the same logic using vline where we draw a vertical line between 100% 0 and 100% 100%.

I won’t advise you to draw your shape using hline and vline because they can be tricky and are a bit difficult to read. Always start by using line and then if you want to optimize your code you can replace them with hline or vline when applicable.

We have our first shape and we know the commands to draw straight lines:

CodePen Embed FallbackCircular Cut-OutNow, let’s try to add a circular cut-out at the top of our shape:

For this, we are going to rely on the arc command, so let’s understand how it works.

If we have two points, A and B, there are exactly two circles with a given radius that intersect with both points like shown in the figure. The intersection gives us four possible arcs we can draw between points A and B. Each arc is defined by a size and a direction.

There is also the particular case where the radius is equal to half the distance between A and B. In this case, only two arcs can be drawn and the direction will decide which one.

The syntax will look like this:

clip-path: shape( from Xa Ya, arc to Xb Yb of R [large or small] [cw or ccw]); Let’s add this to our previous shape. No need to think about the values. Instead, let’s use random ones and see what happens:

clip-path: shape( from 0 0, arc to 40% 0 of 50px, line to 100% 0, line to 100% 100%, line to 0 100%); CodePen Embed FallbackNot bad, we can already see the arc between 0 0 and 40% 0. Notice how I didn’t define the size and direction of the arc. By default, the browser will use small and ccw.

Let’s explicitly define the size and direction to see the four different cases:

CodePen Embed FallbackHmm, it’s working for the first two blocks but not the other ones. Quite strange, right?

Actually, everything is working fine. The arcs are drawn outside the element area so nothing is visible. If you add some box-shadow, you can see them:

CodePen Embed FallbackArcs can be tricky due to the size and direction thing, so get ready to be confused. If that happens, remember that you have four different cases, and trying all of them will help you find which one you need.

Now let’s try to be accurate and draw half a circle with a specific radius placed at the center:

We can define the radius as a variable and use what we have learned so far:

.shape { --r: 50px; clip-path: shape( from 0 0, line to calc(50% - var(--r)) 0, arc to calc(50% + var(--r)) 0 of var(--r), line to 100% 0, line to 100% 100%, line to 0 100% );} CodePen Embed FallbackIt’s working fine, but the code can still be optimized. We can replace all the line commands with hline and vline like below:

.shape { --r: 50px; clip-path: shape(from 0 0, hline to calc(50% - var(--r)), arc to calc(50% + var(--r)) 0 of var(--r), hline to 100%, vline to 100%, hline to 0 );} We can also replace the radius with 1%:

.shape { --r: 50px; clip-path: shape(from 0 0, hline to calc(50% - var(--r)), arc to calc(50% + var(--r)) 0 of 1%, hline to 100%, vline to 100%, hline to 0 );} When you define a small radius (smaller than half the distance between both points), no circle can meet the condition we explained earlier (an intersection with both points), so we cannot draw an arc. This case falls within an error handling where the browser will scale the radius until we can have a circle that meets the condition. Instead of considering this case as invalid, the browser will fix “our mistake” and draw an arc.

This error handling is pretty cool as it allows us to simplify our shape() function. Instead of specifying the exact radius, I simply put a small value and the browser will do the job for me. This trick only works when the arc we want to draw is half a circle. Don’t try to apply it with any arc command because it won’t always work.

Another optimization is to update the following:

arc to calc(50% + var(--r)) 0 of 1%, …with this:

arc by calc(2 * var(--r)) 0 of 1%, Almost all the commands can either use a to directive or a by directive. The first one defines absolute coordinates like the one we use with polygon(). It’s the exact position of the point we are moving to. The second defines relative coordinates which means we need to consider the previous point to identify the coordinates of the next point.

In our case, we are telling the arc to consider the previous point (50% - R) 0 and move by 2*R 0, so the final point will be (50% - R + 2R) (0 + 0), which is the same as (50% + R) 0.

.shape { --r: 50px; clip-path: shape(from 0 0, hline to calc(50% - var(--r)), arc by calc(2 * var(--r)) 0 of 1px, hline to 100%, vline to 100%, hline to 0 );} This last optimization is great because if we want to move the cutout from the center, we only need to update one value: the 50%.

.shape { --r: 50px; --p: 40%; clip-path: shape( from 0 0, hline to calc(var(--p) - var(--r)), arc by calc(2 * var(--r)) 0 of 1px, hline to 100%, vline to 100%, hline to 0 );} CodePen Embed FallbackHow would you adjust the above to have the cut-out at the bottom, left, or right? That’s your first homework assignment! Try to do it before moving to the next part.

I will give my implementation so that you can compare with yours, but don’t cheat! If you can do this without referring to my work, you will be able to do more complex shapes more easily.

CodePen Embed FallbackRounded TabEnough cut-out, let’s try to create a rounded tab:

Can you see the puzzle of this one? Similar to the previous shape, it’s a bunch of arc and line commands. Here is the code:

.shape { --r: 26px; clip-path: shape( /* left part */ from 0 100%, arc by var(--r) calc(-1 * var(--r)) of var(--r), vline to var(--r), arc by var(--r) calc(-1 * var(--r)) of var(--r) cw, /* right part */ hline to calc(100% - 2 * var(--r)), arc by var(--r) var(--r) of var(--r) cw, vline to calc(100% - var(--r)), arc by var(--r) var(--r) of var(--r) );} It looks a bit scary, but if you follow it command by command, it becomes a lot clearer to see what’s happening. Here is a figure to help you visualize the left part of it.

All the arc commands are using the by directive because, in all the cases, I always need to move by an offset equal to R, meaning I don’t have to calculate the coordinates of the points. And don’t try to replace the radius by 1% because it won’t work since we are drawing a quarter of a circle rather than half of a circle.

CodePen Embed FallbackFrom this, we can easily achieve the left and right variations:

CodePen Embed FallbackNotice how I am only using two arc commands instead of three. One rounded corner can be done with a classic border radius, so this can help us simplify the shape.

Inverted RadiusOne last shape, the classic inner curve at the corner also called an inverted radius. How many arc commands do we need for this one? Check the figure below and think about it.

If your answer is six, you have chosen the difficult way to do it. It’s logical to think about six arcs since we have six curves, but three of them can be done with a simple border radius, so only three arcs are needed. Always take the time to analyze the shape you are creating. Sometimes, basic CSS properties can help with creating the shape.

What are you waiting for? This is your next homework and I won’t help you with a figure this time. You have all that you need to easily create it. If you are struggling, give the article another read and try to study the previous shapes more in depth.

Here is my implementation of the four variations:

CodePen Embed FallbackConclusionThat’s all for this first part. You should have a good overview of the shape() function. We focused on the line and arc commands which are enough to create most of the common shapes.

Don’t forget to bookmark the SVG to CSS converter and keep an eye on my CSS Shape collection where you can find the code of all the shapes I create. And here is a last shape to end this article.

CodePen Embed FallbackBetter CSS Shapes Using shape()1. Lines and Arcs (you are here!) 2. More on Arcs 3. Curves


Better CSS Shapes Using shape() — Part 1: Lines and Arcs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Clever, clever that Andy Bell. He shares a technique for displaying image alt text when the image fails to load. Well, more precisely, it’s a technique to apply styles to the alt when the image doesn’t load, offering a nice UI fallback for what would otherwise be a busted-looking error.

The recipe? First, make sure you’re using alt in the HTML. Then, a little JavaScript snippet that detects when an image fails to load:

const images = document.querySelectorAll("img");if (images) { images.forEach((image) => { image.onerror = () => { image.setAttribute("data-img-loading-error", ""); }; });} That slaps an attribute on the image — data-img-loading-error — that is selected in CSS:

img[data-img-loading-error] { --img-border-style: 0.25em solid color-mix(in srgb, currentColor, transparent 75%); --img-border-space: 1em; border-inline-start: var(--img-border-style); border-block-end: var(--img-border-style); padding-inline-start: var(--img-border-space); padding-block: var(--img-border-space); max-width: 42ch; margin-inline: auto;} And what you get is a lovely little presentation of the alt that looks a bit like a blockquote and is is only displayed when needed.

CodePen Embed FallbackAndy does note, however, that Safari does not render alt text if it goes beyond a single line, which 🤷‍♂️.


You can style alt text like any other text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Shape master Temani Afif has what might be the largest collection of CSS shapes on the planet with all the tools to generate them on the fly. There’s a mix of clever techniques he’s typically used to make those shapes, many of which he’s covered here at CSS-Tricks over the years.

Some of the more complex shapes were commonly clipped with the path() function. That makes a lot of sense because it literally accepts SVG path coordinates that you can draw in an app like Figma and export.

But Temani has gone all-in on the newly-released shape() function which recently rolled out in both Chromium browsers and Safari. That includes a brand-new generator that converts path() shapes in shape() commands instead.

So, if we had a shape that was originally created with an SVG path, like this:

.shape { clip-path: path( M199.6,18.9 c-4.3-8.9-12.5-16.4-22.3-17.8 c-11.9-1.7-23.1,5.4-32.2,13.2 c-9.1,7.8-17.8,16.8-29.3,20.3 c-20.5,6.2-41.7-7.4-63.1-7.5 c38.7,27,24.8,33,15.2,43.3 c-35.5,38.2-0.1,99.4,40.6,116.2 c32.8,13.6,72.1,5.9,100.9-15 c27.4-19.9,44.3-54.9,47.4-88.6 c0.2-2.7,0.4-5.3,0.5-7.9 c204.8,38,203.9,27.8,199.6,18.9 z );} …the generator will spit this out:

.shape { clip-path: shape( from 97.54% 10.91%, curve by -10.93% -10.76% with -2.11% -5.38%/-6.13% -9.91%, curve by -15.78% 7.98% with -5.83% -1.03%/-11.32% 3.26%, curve by -14.36% 12.27% with -4.46% 4.71%/-8.72% 10.15%, curve by -30.93% -4.53% with -10.05% 3.75%/-20.44% -4.47%, curve to 7.15% 25.66% with 18.67% 15.81%/11.86% 19.43%, curve by 19.9% 70.23% with -17.4% 23.09%/-0.05% 60.08%, curve by 49.46% -9.07% with 16.08% 8.22%/35.34% 3.57%, curve by 23.23% -53.55% with 13.43% -12.03%/21.71% -33.18%, curve by 0.25% -4.77% with 0.1% -1.63%/0.2% -3.2%, curve to 97.54% 10.91% with 100.09% 22.46%/99.64% 16.29%, close );} Pretty cool!

CodePen Embed FallbackHonestly, I’m not sure how often I’ll need to convert path() to shape(). Seems like a stopgap sorta thing where the need for it dwindles over time as shape() is used more often — and it’s not like the existing path() function is broken or deprecated… it’s just different. But still, I’m using the generator a LOT as I try to wrap my head around shape() commands. Seeing the commands in context is invaluable which makes it an excellent learning tool.


SVG to CSS Shape Converter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

You’ve played Flexbox Froggy before, right? Or maybe Grid Garden? They’re both absolute musts for learning the basics of modern CSS layout using Flexbox and CSS Grid. I use both games in all of the classes I teach and I never get anything but high-fives from my students because they love them so much.

As widely known as those games are, you may be less familiar with the name of the developer who made them. That would be Thomas Park, and he has a couple of CSS-Tricks articles notched in his belt. He also has a horde of other games in his CodePip collection of free and premium games for learning front-end techniques.

Thomas wrote in to share his latest game with us: Anchoreum.

I’ll bet the two nickels in my pocket that you know this game’s all about CSS Anchor Positioning. I love that Thomas has jumped on this so quickly because the feature is still fresh, and indeed is currently only supported in a couple of browsers at the moment.

This is the perfect time to learn about anchor positioning. It’s still relatively early days, but things are baked enough to be supported in Chrome and Edge so you can access the games. If you haven’t seen Juan’s big ol’ guide on anchor positioning, that’s another dandy way to get up to speed.

The objective is less on-the-nose than Flexbox Froggy and Grid Garden, which both lean heavily into positioning elements to complete game tasks. For example, Flexbox Froggy is about positioning frogs safely on lilypads. Grid Garden wants you to water specific garden areas to feed your carrots. Anchoreum? You’re in a museum and need to anchor labels to museum artifacts. I know, attaching target elements to the same anchor over and again could get boring. But thankfully the game goes beyond simple positioning by getting into multiple anchors, spanning, and position fallbacks.

Whatever the objective, the repetition is good for developing muscle memory and the overall outcome is still the same: learn CSS Anchor Positioning. I’m already planning how and where I’m going to use Anchoreum in my curriculum. It’s not often we get a fun interactive learning resource like this for such a new web feature and I think it’s worth jumping on it sooner rather than later.

Thomas prepped a video trailer for the game so I thought I’d drop that for reference.


Anchoreum: A New Game for Learning Anchor Positioning originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Another title from A Book Apart has been re-released for free. The latest? Tim Brown’s Flexible Typesetting. I may not be the utmost expert on typography and its best practices but I do remember reading this book (it’s still on the shelf next to me!) thinking maybe, just maybe, I might be able to hold a conversation about it with Robin when I finished it.

I still think I’m in “maybe” territory but that’s not Tim’s fault — I found the book super helpful and approachable for noobs like me who want to up our game. For the sake of it, I’ll drop the chapter titles here to give you an idea of what you’ll get.

  • What is typsetting?
  • Preparing text and code (planning is definitely part of the typesetting process)
  • Selecting typefaces (this one helped me a lot!)
  • Shaping text blocks (modern CSS can help here)
  • Crafting compositions (great if you’re designing for long-form content)
  • Relieving pressure

Tim Brown: Flexible Typesetting is now yours, for free originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

If all you have is a hammer, everything looks like a nail.

Abraham Maslow

It’s easy to default to what you know. When it comes to toggling content, that might be reaching for display: none or opacity: 0 with some JavaScript sprinkled in. But the web is more “modern” today, so perhaps now is the right time to get a birds-eye view of the different ways to toggle content — which native APIs are actually supported now, their pros and cons, and some things about them that you might not know (such as any pseudo-elements and other non-obvious stuff).

So, let’s spend some time looking at disclosures (<details> and <summary>), the Dialog API, the Popover API, and more. We’ll look at the right time to use each one depending on your needs. Modal or non-modal? JavaScript or pure HTML/CSS? Not sure? Don’t worry, we’ll go into all that.

Disclosures (<details> and <summary>)Use case: Accessibly summarizing content while making the content details togglable independently, or as an accordion.

CodePen Embed FallbackGoing in release order, disclosures — known by their elements as <details> and <summary> — marked the first time we were able to toggle content without JavaScript or weird checkbox hacks. But lack of web browser support obviously holds new features back at first, and this one in particular came without keyboard accessibility. So I’d understand if you haven’t used it since it came to Chrome 12 way back in 2011. Out of sight, out of mind, right?

Here’s the low-down:

  • It’s functional without JavaScript (without any compromises).
  • It’s fully stylable without appearance: none or the like.
  • You can hide the marker without non-standard pseudo-selectors.
  • You can connect multiple disclosures to create an accordion.
  • Aaaand… it’s fully animatable, as of 2024.

Marking up disclosuresWhat you’re looking for is this:

```

Content summary (always visible) Content (visibility is toggled when summary is clicked on)

`` Behind the scenes, the content’s wrapped in a pseudo-element that as of 2024 we can select using::details-content. To add to this, there’s a::marker` pseudo-element that indicates whether the disclosure’s open or closed, which we can customize.

With that in mind, disclosures actually look like this under the hood:

```

<::marker>Content summary (always visible) <::details-content> Content (visibility is toggled when summary is clicked on)

`` To have the disclosure open by default, give

theopen` attribute, which is what happens behind the scenes when disclosures are opened anyway.

```

...

`` Styling disclosuresLet’s be real: you probably just want to lose that annoying marker. Well, you can do that by setting thedisplayproperty of

to anything butlist-item`:

summary { display: block; /* Or anything else that isn't list-item */} CodePen Embed FallbackAlternatively, you can modify the marker. In fact, the example below utilizes Font Awesome to replace it with another icon, but keep in mind that ::marker doesn’t support many properties. The most flexible workaround is to wrap the content of <summary> in an element and select it in CSS.

```

Content summary Content

```

details { /* The marker */ summary::marker { content: "\f150"; font-family: "Font Awesome 6 Free"; } /* The marker when <details> is open */ &[open] summary::marker { content: "\f151"; } /* Because ::marker doesn’t support many properties */ summary span { margin-left: 1ch; display: inline-block; } } CodePen Embed FallbackCreating an accordion with multiple disclosuresCodePen Embed FallbackTo create an accordion, name multiple disclosures (they don’t even have to be siblings) with a name attribute and a matching value (similar to how you’d implement <input type="radio">):

```

Prequels
  • Episode I: The Phantom Menace
  • Episode II: Attack of the Clones
  • Episode III: Revenge of the Sith
Originals
  • Episode IV: A New Hope
  • Episode V: The Empire Strikes Back
  • Episode VI: Return of the Jedi
Sequels
  • Episode VII: The Force Awakens
  • Episode VIII: The Last Jedi
  • Episode IX: The Rise of Skywalker

``` Using a wrapper, we can even turn these into horizontal tabs:

CodePen Embed Fallback ```

...
...
...

```

div { gap: 1ch; display: flex; position: relative; details { min-height: 106px; /* Prevents content shift */ &[open] summary, &[open]::details-content { background: #eee; } &[open]::details-content { left: 0; position: absolute; } }} …or, using 2024’s Anchor Positioning API, vertical tabs (same HTML):

div { display: inline-grid; anchor-name: --wrapper; details[open] { summary, &::details-content { background: #eee; } &::details-content { position: absolute; position-anchor: --wrapper; top: anchor(top); left: anchor(right); } }} CodePen Embed FallbackIf you’re looking for some wild ideas on what we can do with the Popover API in CSS, check out John Rhea’s article in which he makes an interactive game solely out of disclosures!

Adding JavaScript functionalityWant to add some JavaScript functionality?

// Optional: select and loop multiple disclosuresdocument.querySelectorAll("details").forEach(details => { details.addEventListener("toggle", () => { // The disclosure was toggled if (details.open) { // The disclosure was opened } else { // The disclosure was closed } }); }); Creating accessible disclosuresDisclosures are accessible as long as you follow a few rules. For example, <summary> is basically a <label>, meaning that its content is announced by screen readers when in focus. If there isn’t a <summary> or <summary> isn’t a direct child of <details> then the user agent will create a label for you that normally says “Details” both visually and in assistive tech. Older web browsers might insist that it be the first child, so it’s best to make it so.

To add to this, <summary> has the role of button, so whatever’s invalid inside a <button> is also invalid inside a <summary>. This includes headings, so you can style a <summary> as a heading, but you can’t actually insert a heading into a <summary>.

The Dialog element (<dialog>)Use case: Modals

CodePen Embed FallbackNow that we have the Popover API for non-modal overlays, I think it’s best if we start to think of dialogs as modals even though the show() method does allow for non-modal dialogs. The advantage that the popover attribute has over the <dialog> element is that you can use it to create non-modal overlays without JavaScript, so in my opinion there’s no benefit to non-modal dialogs anymore, which do require JavaScript. For clarity, a modal is an overlay that makes the main document inert, whereas with non-modal overlays the main document remains interactive. There are a few other features that modal dialogs have out-of-the-box as well, including:

  • a stylable backdrop,
  • an autofocus onto the first focusable element within the <dialog> (or, as a backup, the <dialog> itself — include an aria-label in this case),
  • a focus trap (as a result of the main document’s inertia),
  • the esc key closes the dialog, and
  • both the dialog and the backdrop are animatable.Marking up and activating dialogs

Start with the <dialog> element:

<dialog> ... </dialog> It’s hidden by default and, similar to <details>, we can have it open when the page loads, although it isn’t modal in this scenario since it does not contain interactive content because it doesn’t opened with showModal().

<dialog open> ... </dialog> I can’t say that I’ve ever needed this functionality. Instead, you’ll likely want to reveal the dialog upon some kind of interaction, such as the click of a button — so here’s that button:

<button data-dialog="dialogA">Open dialogA</button> Wait, why are we using data attributes? Well, because we might want to hand over an identifier that tells the JavaScript which dialog to open, enabling us to add the dialog functionality to all dialogs in one snippet, like this:

// Select and loop all elements with that data attributedocument.querySelectorAll("[data-dialog]").forEach(button => { // Listen for interaction (click) button.addEventListener("click", () => { // Select the corresponding dialog const dialog = document.querySelector(`#${ button.dataset.dialog }`); // Open dialog dialog.showModal(); // Close dialog dialog.querySelector(".closeDialog").addEventListener("click", () => dialog.close()); });}); Don’t forget to add a matching id to the <dialog> so it’s associated with the <button> that shows it:

<dialog id="dialogA"> <!-- id and data-dialog = dialogA --> ... </dialog> And, lastly, include the “close” button:

<dialog id="dialogA"> <button class="closeDialog">Close dialogA</button></dialog> Note: <form method="dialog"> (that has a <button>) or <button formmethod="dialog"> (wrapped in a <form>) also closes the dialog.

How to prevent scrolling when the dialog is openPrevent scrolling while the modal’s open, with one line of CSS:

body:has(dialog:modal) { overflow: hidden; } Styling the dialog’s backdropAnd finally, we have the backdrop to reduce distraction from what’s underneath the top layer (this applies to modals only). Its styles can be overwritten, like this:

::backdrop { background: hsl(0 0 0 / 90%); backdrop-filter: blur(3px); /* A fun property just for backdrops! */} On that note, the <dialog> itself comes with a border, a background, and some padding, which you might want to reset. Actually, popovers behave the same way.

Dealing with non-modal dialogsTo implement a non-modal dialog, use:

  • show() instead of showModal()
  • dialog[open] (targets both) instead of dialog:modal

Although, as I said before, the Popover API doesn’t require JavaScript, so for non-modal overlays I think it’s best to use that.

The Popover API (<element popover>)Use case: Non-modal overlays

CodePen Embed FallbackPopups, basically. Suitable use cases include tooltips (or toggletips — it’s important to know the difference), onboarding walkthroughs, notifications, togglable navigations, and other non-modal overlays where you don’t want to lose access to the main document. Obviously these use cases are different to those of dialogs, but nonetheless popovers are extremely awesome. Functionally they’re just like just dialogs, but not modal and don’t require JavaScript.

Marking up popoversTo begin, the popover needs an id as well as the popover attribute with the manual value (which means clicking outside of the popover doesn’t close it), the auto value (clicking outside of the popover does close it), or no value (which means the same thing). To be semantic, the popover can be a <dialog>.

<dialog id="tooltipA" popover> ... </dialog> Next, add the popovertarget attribute to the <button> or <input type="button"> that we want to toggle the popover’s visibility, with a value matching the popover’s id attribute (this is optional since clicking outside of the popover will close it anyway, unless popover is set to manual):

<dialog id="tooltipA" popover> <button popovertarget="tooltipA">Hide tooltipA</button></dialog> Place another one of those buttons in your main document, so that you can show the popover. That’s right, popovertarget is actually a toggle (unless you specify otherwise with the popovertargetaction attribute that accepts show, hide, or toggle as its value — more on that later).

Styling popoversCodePen Embed FallbackBy default, popovers are centered within the top layer (like dialogs), but you probably don’t want them there as they’re not modals, after all.

```

``` You can easily pull them into a corner using fixed positioning, but for a tooltip-style popover you’d want it to be relative to the trigger that opens it. CSS Anchor Positioning makes this super easy:

main [popovertarget] { anchor-name: --trigger;}[popover] { margin: 0; position-anchor: --trigger; top: calc(anchor(bottom) + 10px); justify-self: anchor-center;}/* This also works but isn’t neededunless you’re using the display property[popover]:popover-open { ...}*/ The problem though is that you have to name all of these anchors, which is fine for a tabbed component but overkill for a website with quite a few tooltips. Luckily, we can match an id attribute on the button to an anchor attribute on the popover, which isn’t well-supported as of November 2024 but will do for this demo:

CodePen Embed Fallback ```

```

main [popovertarget] { anchor-name: --anchorA; } /* No longer needed */[popover] { margin: 0; position-anchor: --anchorA; /* No longer needed */ top: calc(anchor(bottom) + 10px); justify-self: anchor-center;} The next issue is that we expect tooltips to show on hover and this doesn’t do that, which means that we need to use JavaScript. While this seems complicated considering that we can create tooltips much more easily using ::before/::after/content:, popovers allow HTML content (in which case our tooltips are actually toggletips by the way) whereas content: only accepts text.

Adding JavaScript functionalityWhich leads us to this…

CodePen Embed FallbackOkay, so let’s take a look at what’s happening here. First, we’re using anchor attributes to avoid writing a CSS block for each anchor element. Popovers are very HTML-focused, so let’s use anchor positioning in the same way. Secondly, we’re using JavaScript to show the popovers (showPopover()) on mouseover. And lastly, we’re using JavaScript to hide the popovers (hidePopover()) on mouseout, but not if they contain a link as obviously we want them to be clickable (in this scenario, we also don’t hide the button that hides the popover).

```

Has a link, so we can’t hide it on mouseout Doesn’t have a link, so it’s fine to hide it on mouseout automatically ```

[popover] { margin: 0; top: calc(anchor(bottom) + 10px); justify-self: anchor-center; /* No link? No button needed */ &:not(:has(a)) [popovertarget] { display: none; }}

/* Select and loop all popover triggers */document.querySelectorAll("main [popovertarget]").forEach((popovertarget) => { /* Select the corresponding popover */ const popover = document.querySelector(`#${popovertarget.getAttribute("popovertarget")}`); /* Show popover on trigger mouseover */ popovertarget.addEventListener("mouseover", () => { popover.showPopover(); }); /* Hide popover on trigger mouseout, but not if it has a link */ if (popover.matches(":not(:has(a))")) { popovertarget.addEventListener("mouseout", () => { popover.hidePopover(); }); }}); Implementing timed backdrops (and sequenced popovers)At first, I was sure that popovers having backdrops was an oversight, the argument being that they shouldn’t obscure a focusable main document. But maybe it’s okay for a couple of seconds as long as we can resume what we were doing without being forced to close anything? At least, I think this works well for a set of onboarding tips:

CodePen Embed Fallback ```

  • Tool A
  • Tool B
  • Another tool, “C”
  • Another tool — let’s call this one “D”

onboardingTipA onboardingTipB onboardingTipC onboardingTipD ```

::backdrop { animation: 2s fadeInOut;}[popover] { margin: 0; align-self: anchor-center; left: calc(anchor(right) + 10px);}

/*After users have had a couple ofseconds to breathe, start the onboarding*/setTimeout(() => { document.querySelector("#onboardingTipA").showPopover();}, 2000); Again, let’s unpack. Firstly, setTimeout() shows the first onboarding tip after two seconds. Secondly, a simple fade-in-fade-out background animation runs on the backdrop and all subsequent backdrops. The main document isn’t made inert and the backdrop doesn’t persist, so attention is diverted to the onboarding tips while not feeling invasive.

Thirdly, each popover has a button that triggers the next onboarding tip, which triggers another, and so on, chaining them to create a fully HTML onboarding flow. Typically, showing a popover closes other popovers, but this doesn’t appear to be the case if it’s triggered from within another popover. Also, re-showing a visible popover rolls the onboarding back to that step, and, hiding a popover hides it and all subsequent popovers — although that only appears to work when popover equates to auto. I don’t fully understand it but it’s enabled me to create “restart onboarding” and “cancel onboarding” buttons.

With just HTML. And you can cycle through the tips using esc and return.

Creating modal popoversHear me out. If you like the HTML-ness of popover but the semantic value of <dialog>, this JavaScript one-liner can make the main document inert, therefore making your popovers modal:

document.querySelectorAll("dialog[popover]").forEach(dialog => dialog.addEventListener("toggle", () => document.body.toggleAttribute("inert"))); However, the popovers must come after the main document; otherwise they’ll also become inert. Personally, this is what I’m doing for modals anyway, as they aren’t a part of the page’s content.

```

... ``` Aaaand… breatheYeah, that was a lot. But…I think it’s important to look at all of these APIs together now that they’re starting to mature, in order to really understand what they can, can’t, should, and shouldn’t be used for. As a parting gift, I’ll leave you with a transition-enabled version of each API:

  • Sliding disclosures
  • Popping dialog (with fading backdrop)
  • Sliding popover (hamburger nav, because why not?)

The Different (and Modern) Ways to Toggle Content originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The State of CSS 2024 survey wrapped up and the results are interesting, as always. Even though each section is worth analyzing, we are usually most hyped about the section on the most used CSS features. And if you are interested in writing about web development (maybe start writing with us 😉), you will specifically want to check out the feature’s Reading List section. It holds the features that survey respondents wish to read about after completing the survey and is usually composed of up-and-coming features with low community awareness.

One of the features I was excited to see was my 2024 top pick: CSS Anchor Positioning, ranking in the survey’s Top 4. Just below, you can find Scroll-Driven Animations, another amazing feature that gained broad browser support this year. Both are elegant and offer good DX, but combining them opens up new possibilities that clearly fall into what most of us would have considered JavaScript territory just last year.

I want to show one of those possibilities while learning more about both features. Specifically, we will make the following blog post in which footnotes pop up as comments on the sides of each text.

CodePen Embed FallbackFor this demo, our requirements will be:

  • Pop the footnotes up when they get into the screen.
  • Attach them to their corresponding texts.
  • The footnotes are on the sides of the screen, so we need a mobile fallback.

The FoundationTo start, we will use the following everyday example of a blog post layout: title, cover image, and body of text:

CodePen Embed FallbackThe only thing to notice about the markup is that now and then we have a paragraph with a footnote at the end:

```

Super intereseting information! A footnote about it

``` Positioning the FootnotesIn that demo, the footnotes are located inside the body of the post just after the text we want to note. However, we want them to be attached as floating bubbles on the side of the text. In the past, we would probably need a mix of absolute and relative positioning along with finding the correct inset properties for each footnote.

However, we can now use anchor positioning for the job, a feature that allows us to position absolute elements relative to other elements — rather than just relative to the containment context it is in. We will be talking about “anchors” and “targets” for a while, so a little terminology as we get going:

  • Anchor: This is the element used as a reference for positioning other elements, hence the anchor name.
  • Target: This is an absolutely-positioned element placed relative to one or more anchors. The target is the name we will use from now on, but you will often find it as just an “absolutely positioned element” in other resources.

I won’t get into each detail, but if you want to learn more about it I highly recommend our Anchor Positioning Guide for complete information and examples.

The Anchor and TargetIt’s easy to know that each .footnote is a target element. Picking our anchor, however, requires more nuance. While it may look like each .note element should be an anchor element, it’s better to choose the whole .post as the anchor. Let me explain if we set the .footnote position to absolute:

.footnote { position: absolute;} You will notice that the .footnote elements on the post are removed from the normal document flow and they hover visually above their .note elements. This is great news! Since they are already aligned on the vertical axis, we just have to move them on the horizontal axis onto the sides using the post as an anchor.

This is when we would need to find the correct inset property to place them on the sides. While this is doable, it’s a painful choice since:

  1. You would have to rely on a magic number.
  2. It depends on the viewport.
  3. It depends on the footnote’s content since it changes its width.

Elements aren’t anchors by default, so to register the post as an anchor, we have to use the anchor-name property and give it a dashed-ident (a custom name starting with two dashes) as a name.

.post { anchor-name: --post;} In this case, our target element would be the .footnote. To use a target element, we can keep the absolute positioning and select an anchor element using the position-anchor property, which takes the anchor’s dashed ident. This will make .post the default anchor for the target in the following step.

.footnote { position: absolute; position-anchor: --post;} Moving the Target AroundInstead of choosing an arbitrary inset value for the .footnote‘s left or right properties, we can use the anchor() function. It returns a <length> value with the position of one side of the anchor, allowing us to always set the target’s inset properties correctly. So, we can connect the left side of the target to the right side of the anchor and vice versa:

.footnote { position: absolute; position-anchor: --post; /* To place them on the right */ left: anchor(right); /* or to place them on the left*/ right: anchor(left); /* Just one of them at a time! */} However, you will notice that it’s stuck to the side of the post with no space in between. Luckily, the margin property works just as you are hoping it does with target elements and gives a little space between the footnote target and the post anchor. We can also add a little more styles to make things prettier:

.footnote { /* ... */ background-color: #fff; border-radius: 20px; margin: 0px 20px; padding: 20px;} Lastly, all our .footnote elements are on the same side of the post, if we want to arrange them one on each side, we can use the nth-of-type() selector to select the even and odd notes and set them on opposite sides.

.note:nth-of-type(odd) .footnote { left: anchor(right);}.note:nth-of-type(even) .footnote { right: anchor(left);} We use nth-of-type() instead of nth-child since we just want to iterate over .note elements and not all the siblings.

Just remember to remove the last inset declaration from .footnote, and tada! We have our footnotes on each side. You will notice I also added a little triangle on each footnote, but that’s beyond the scope of this post:

CodePen Embed FallbackThe View-Driven AnimationLet’s get into making the pop-up animation. I find it the easiest part since both view and scroll-driven animation are built to be as intuitive as possible. We will start by registering an animation using an everyday @keyframes. What we want is for our footnotes to start being invisible and slowly become bigger and visible:

@keyframes pop-up { from { opacity: 0; transform: scale(0.5); } to { opacity: 1; }} That’s our animation, now we just have to add it to each .footnote:

.footnote { /* ... */ animation: pop-up linear;} This by itself won’t do anything. We usually would have set an animation-duration for it to start. However, view-driven animations don’t run through a set time, rather the animation progression will depend on where the element is on the screen. To do so, we set the animation-timeline to view().

.footnote { /* ... */ animation: pop-up linear; animation-timeline: view();} This makes the animation finish just as the element is leaving the screen. What we want is for it to finish somewhere more readable. The last touch is setting the animation-range to cover 0% cover 40%. This translates to, “I want the element to start its animation when it’s 0% in the view and end when it’s at 40% in the view.”

.footnote { /* ... */ animation: pop-up linear; animation-timeline: view(); animation-range: cover 0% cover 40%;} This amazing tool by Bramus focused on scroll and view-driven animation better shows how the animation-range property works.

What About Mobile?You may have noticed that this approach to footnotes doesn’t work on smaller screens since there is no space at the sides of the post. The fix is easy. What we want is for the footnotes to display as normal notes on small screens and as comments on larger screens, we can do that by making our comments only available when the screen is bigger than a certain threshold, which is about 1000px. If it isn’t, then the notes are displayed on the body of the post as any other note you may find on the web.

.footnote { display: flex; gap: 10px; border-radius: 20px; padding: 20px; background-color: #fce6c2; &::before { content: "Note:"; font-weight: 600; }}@media (width > 1000px) { /* Styles */} Now our comments should be displayed on the sides only when there is enough space for them:

CodePen Embed FallbackWrapping UpIf you also like writing about something you are passionate about, you will often find yourself going into random tangents or wanting to add a comment in each paragraph for extra context. At least, that’s my case, so having a way to dynamically show comments is a great addition. Especially when we achieved using only CSS — in a way that we couldn’t just a year ago!


Popping Comments With CSS Anchor Positioning and View-Driven Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

We all know how to do responsive design, right? We use media queries. Well no, we use container queries now, don’t we? Sometimes we get inventive with flexbox or autoflowing grids. If we’re feeling really adventurous we can reach for fluid typography.

I’m a bit uncomfortable that responsive design is often pushed into discreet chunks, like “layout A up to this size, then layout B until there’s enough space for layout C.” It’s OK, it works and fits into a workflow where screens are designed as static layouts in PhotoFigVa (caveat, I made that up). But the process feels like a compromise to me. I’ve long believed that responsive design should be almost invisible to the user. When they visit my site on a mobile device while waiting in line for K-Pop tickets, they shouldn’t notice that it’s different from just an hour ago, sitting at the huge curved gaming monitor they persuaded their boss they needed.

Consider this simple hero banner and its mobile equivalent. Sorry for the unsophisticated design. The image is AI generated, but It’s the only thing about this article that is.

The meerkat and the text are all positioned and sized differently. The traditional way to pull this off is to have two layouts, selected by a ~~media~~, sorry, container query. There might be some flexibility in each layout, perhaps centering the content, and a little fluid typography on the font-size, but we’re going to choose a point at which we flip the layout in and out of the stacked version. As a result, there are likely to be widths near the breakpoint where the layout looks either a little empty or a little congested.

Is there another way?

It turns out there is. We can apply the concept of fluid typography to almost anything. This way we can have a layout that fluidly changes with the size of its parent container. Few users will ever see the transition, but they will all appreciate the results. Honestly, they will.

Let’s get this styled upFor the first step, let’s style the layouts individually, a little like we would when using width queries and a breakpoint. In fact, let’s use a container query and a breakpoint together so that we can easily see what properties need to change.

This is the markup for our hero, and it won’t change:

```

LookOut

Eagle Defense System

``` This is the relevant CSS for the wide version:

```

hero { container-type: inline-size; max-width: 1200px; min-width: 360px; .details { position: absolute; z-index: 2; top: 220px; left: 565px; h1 { font-size: 5rem; } p { font-size: 2.5rem; } } &::before { content: ''; position: absolute; z-index: 1; top: 0; left: 0; right: 0; bottom: 0; background-image: url(../meerkat.jpg); background-origin: content-box; background-repeat: no-repeat; background-position-x: 0; background-position-y: 0; background-size: auto 589px; }}

`` I’ve attached the background image to a::beforepseudo-element so I can use container queries on it (because containers cannot query themselves). We’ll keep this later on so that we can use inline container query (cqi`) units. For now, here’s the container query that just shows the values we’re going to make fluid:

@container (max-width: 800px) { #hero { .details { top: 50px; left: 20px; h1 { font-size: 3.5rem; } p { font-size: 2rem; } } &::before { background-position-x: -310px; background-position-y: -25px; background-size: auto 710px; } }} You can see the code running in a live demo — it’s entirely static to show the limitations of a typical approach.

Let’s get fluidNow we can take those start and end points for the size and position of both the text and background and make them fluid. The text size uses fluid typography in a way you are already familiar with. Here’s the result — I’ll explain the expressions once you’ve looked at the code.

First the changes to the position and size of the text:

/* Line changes * -12,27 +12,32 */ .details { /* ... lines 14-16 unchanged */ /* Evaluates to 50px for a 360px wide container, and 220px for 1200px */ top: clamp(50px, 20.238cqi - 22.857px, 220px); /* Evaluates to 20px for a 360px wide container, and 565px for 1200px */ left: clamp(20px, 64.881cqi - 213.571px, 565px); /* ... lines 20-25 unchanged */ h1 { /* Evaluates to 3.5rem for a 360px wide container, and 5rem for 1200px */ font-size: clamp(3.5rem, 2.857rem + 2.857cqi, 5rem); /* ... font-weight unchanged */ } p { /* Evaluates to 2rem for a 360px wide container, and 2.5rem for 1200px */ font-size: clamp(2rem, 1.786rem + 0.952cqi, 2.5rem); }} And here’s the background position and size for the meerkat image:

/* Line changes * -50,3 +55,8 *//* Evaluates to -310px for a 360px wide container, and 0px for 1200px */background-position-x: clamp(-310px, 36.905cqi - 442.857px, 0px);/* Evaluates to -25px for a 360px wide container, and 0px for 1200px */background-position-y: clamp(-25px, 2.976cqi);/* Evaluates to 710px for a 360px wide container, and 589px for 1200px */background-size: auto clamp(589px, 761.857px - 14.405cqi, 710px); Now we can drop the container query entirely.

Let’s explain those clamp() expressions. We’ll start with the expression for the top property.

/* Evaluates to 50px for a 360px wide container, and 220px for 1200px */top: clamp(50px, 20.238cqi - 22.857px, 220px); You’ll have noticed there’s a comment there. These expressions are a good example of how magic numbers are a bad thing. But we can’t avoid them here, as they are the result of solving some simultaneous equations — which CSS cannot do!

The upper and lower bounds passed to clamp() are clear enough, but the expression in the middle comes from these simultaneous equations:

f + 12v = 220f + 3.6v = 50 …where f is the number of fixed-size length units (i.e., px) and v is the variable-sized unit (cqi). In the first equation, we are saying that we want the expression to evaluate to 220px when 1cqi is equal to 12px. In the second equation, we’re saying we want 50px when 1cqi is 3.6px, which solves to:

f = -22.857v = 20.238 …and this tidies up to 20.238cqi – 22.857px in a calc()-friendly expression.

When the fixed unit is different, we must change the size of the variable units accordingly. So for the <h1> element’s font-size we have;

/* Evaluates to 2rem for a 360px wide container, and 2.5rem for 1200px */font-size: clamp(2rem, 1.786rem + 0.952cqi, 2.5rem); This is solving these equations because, at a container width of 1200px, 1cqi is the same as 0.75rem (my rems are relative to the default UA stylesheet, 16px), and at 360px wide, 1cqi is 0.225rem.

f + 0.75v = 2.5f + 0.225v = 2 This is important to note: The equations are different depending on what unit you are targeting.

Honestly, this is boring math to do every time, so I made a calculator you can use. Not only does it solve the equations for you (to three decimal places to keep your CSS clean) it also provides that helpful comment to use alongside the expression so that you can see where they came from and avoid magic numbers. Feel free to use it. Yes, there are many similar calculators out there, but they concentrate on typography, and so (rightly) fixate on rem units. You could probably port the JavaScript if you’re using a CSS preprocessor.

The clamp() function isn’t strictly necessary at this point. In each case, the bounds of clamp() are set to the values of when the container is either 360px or 1200px wide. Since the container itself is constrained to those limits — by setting min-width and max-width values — the clamp() expression should never invoke either bound. However, I prefer to keep clamp() there in case we ever change our minds (which we are about to do) because implicit bounds like these are difficult to spot and maintain.

Avoiding injuryWe could consider our work finished, but we aren’t. The layout still doesn’t quite work. The text passes right over the top of the meerkat’s head. While I have been assured this causes the meerkat no harm, I don’t like the look of it. So, let’s make some changes to make the text avoid hitting the meerkat.

The first is simple. We’ll move the meerkat to the left more quickly so that it gets out of the way. This is done most easily by changing the lower end of the interpolation to a wider container. We’ll set it so that the meerkat is fully left by 450px rather than down to 360px. There’s no reason the start and end points for all of our fluid expressions need to align with the same widths, so we can keep the other expressions fluid down to 360px.

Using my trusty calculator, all we need to do is change the clamp() expressions for the background-position properties:

/* Line changes * -55,5 +55,5 *//* Evaluates to -310px for a 450px wide container, and 0px for 1200px */background-position-x: clamp(-310px, 41.333cqi - 496px, 0px);/* Evaluates to -25px for a 450px wide container, and 0px for 1200px */background-position-y: clamp(-25px, 3.333cqi - 40px, 0px); This improves things, but not totally. I don’t want to move it any quicker, so next we’ll look at the path the text takes. At the moment it moves in a straight line, like this:

But can we bend it? Yes, we can.

A Bend in the pathOne way we can do this is by defining two different interpolations for the top coordinate that places the line at different angles and then choosing the smallest one. This way, it allows the steeper line to “win” at larger container widths, and the shallower line becomes the value that wins when the container is narrower than about 780px. The result is a line with a bend that misses the meerkat.

All we’re changing is the top value, but we must calculate two intermediate values first:

/* Line changes * -18,2 +18,9 @@ *//* Evaluates to 220px for a 1200px wide container, and -50px for 360px */--top-a: calc(32.143cqi - 165.714px);/* Evaluates to 120px for a 1200px wide container, and 50px for 360px */--top-b: calc(20px + 8.333cqi);/* By taking the max, --topA is used at lower widths, with --topB taking over when wider.We only need to apply clamp when the value is actually used */top: clamp(50px, max(var(--top-a), var(--top-b)), 220px); For these values, rather than calculating them formally using a carefully chosen midpoint, I experimented with the endpoints until I got the result I wanted. Experimentation is just as valid as calculation as a way of getting the result you need. In this case, I started with duplicates of the interpolation in custom variables. I could have split the path into explicit sections using a container query, but that doesn’t reduce the math overhead, and using the min() function is cleaner to my eye. Besides, this article isn’t strictly about container queries, is it?

Now the text moves along this path. Open up the live demo to see it in action.

CSS can’t do everythingAs a final note on the calculations, it’s worth pointing out that there are restrictions as far as what we can and can’t do. The first, which we have already mitigated a little, is that these interpolations are linear. This means that easing in or out, or other complex behavior, is not possible.

Another major restriction is that CSS can only generate length values this way, so there is no way in pure CSS to apply, for example, opacity or a rotation angle that is fluid based on the container or viewport size. Preprocessors can’t help us here either because the limitation is on the way calc() works in the browser.

Both of these restrictions can be lifted if you’re prepared to rely on a little JavaScript. A few lines to observe the width of the container and set a CSS custom property that is unitless is all that’s needed. I’m going to use that to make the text follow a quadratic Bezier curve, like this:

There’s too much code to list here, and too much math to explain the Bezier curve, but go take a look at it in action in this live demo.

We wouldn’t even need JavaScript if expressions like calc(1vw / 1px) didn’t fail in CSS. There is no reason for them to fail since they represent a ratio between two lengths. Just as there are 2.54cm in 1in, there are 8px in 1vw when the viewport is 800px wide, so calc(1vw / 1px) should evaluate to a unitless 8 value.

They do fail though, so all we can do is state our case and move on.

Fluid everything doesn’t solve all layoutsThere will always be some layouts that need size queries, of course; some designs will simply need to snap changes at fixed breakpoints. There is no reason to avoid that if it’s right. There is also no reason to avoid mixing the two, for example, by fluidly sizing and positioning the background while using a query to snap between grid definitions for the text placement. My meerkat example is deliberately contrived to be simple for the sake of demonstration.

One thing I’ll add is that I’m rather excited by the possibility of using the new Anchor Positioning API for fluid positioning. There’s the possibility of using anchor positioning to define how two elements might flow around the screen together, but that’s for another time.


Fluid Everything Else originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

We had fun in my previous article exploring the goodness of scrolly animations supported in today’s versions of Chrome and Edge (and behind a feature flag in Firefox for now). Those are by and large referred to as “scroll-driven” animations. However, “scroll triggering” is something the Chrome team is still working on. It refers to the behavior you might have seen in the wild in which a point of no return activates a complete animation like a trap after our hapless scrolling user ventures past a certain point. You can see JavaScript examples of this on the Wow.js homepage which assembles itself in a sequence of animated entrances as you scroll down. There is no current official CSS solution for scroll-triggered animations — but Ryan Mulligan has shown how we can make it work by cleverly combining the animation-timeline property with custom properties and style queries.

That is a very cool way to combine new CSS features. But I am not done being overly demanding toward the awesome emergent animation timeline technology I didn’t know existed before I read up on it last month. I noticed scroll timelines and view timelines are geared toward animations that play backward when you scroll back up, unlike the Wow.js example where the dogs roll in and then stay. Bramus mentions the same point in his exploration of scroll-triggered animations. The animations run in reverse when scrolling back up. This is not always feasible. As a divorced Dad, I can attest that the Tinder UI is another example of a pattern in which scrolling and swiping can have irreversible consequences.

Scroll till the cows come home with Web-Slinger.cssBelieve it or not, with a small amount of SCSS and no JavaScript, we can build a pure CSS replacement of the Wow.js library, which I hereby christen “Web-Slinger.css.” It feels good to use the scroll-driven optimized standards already supported by some major browsers to make a prototype library. Here’s the finished demo and then we will break down how it works. I have always enjoyed the deliberately lo-fi aesthetic of the original Wow.js page, so it’s nice to have an excuse to create a parody. Much profession, so impress.

CodePen Embed FallbackTeach scrolling elements to roll over and stayWeb-Slinger.css introduces a set of class names in the format .scroll-trigger-n and .on-scroll-trigger-n. It also defines --scroll-trigger-n custom properties, which are inherited from the document root so we can access them from any CSS class. These conventions are more verbose than Wow.js but also more powerful. The two types of CSS classes decouple the triggers of our one-off animations from the elements they trigger, which means we can animate anything on the page based on the user reaching any scroll marker.

Here’s a basic example that triggers the Animate.css animation “flipInY” when the user has scrolled to the <div> marked as .scroll-trigger-8.

```

``` A more advanced use is the sticky “Cownter” (trademark pending) at the top of the demo page, which takes advantage of the ability of one trigger to activate an arbitrary number of animations anywhere in the document. The Cownter increments as new cows appear then displays a reset button once we reach the final scroll trigger at the bottom of the page.

Here is the markup for the Cownter:

```

``` …and the CSS:

.header { .cownter::after { --cownter: calc(var(--scroll-trigger-2) + var(--scroll-trigger-4) + var(--scroll-trigger-8) + var(--scroll-trigger-11)); --pluralised-cow: 'cows'; counter-set: cownter var(--cownter); content: "Have " counter(cownter) " " var(--pluralised-cow) ", man"; } @container style(--scroll-trigger-2: 1) and style(--scroll-trigger-4: 0) { .cownter::after { --pluralised-cow: 'cow'; } } a { text-decoration: none; color:blue; }}:root:has(.reset:active) * { animation-name: none;} The demo CodePen references Web-Slinger.css from a separate CodePen, which I reference in my final demo the same way I would an external resource.

Sidenote: If you have doubts about the utility of style queries, behold the age-old cow pluralization problem solved in pure CSS.

How does Web Slinger like to sling it?The secret is based on an iconic thought experiment by the philosopher Friedrich Nietzsche who once asked: If the view() function lets you style an element once it comes into view, what if you take that opportunity to style it so it can never be scrolled out of view? Would that element not stare back into you for eternity?

.scroll-trigger { animation-timeline: view(); animation-name: stick-to-the-top; animation-fill-mode: both; animation-duration: 1ms;}@keyframes stick-to-the-top { .1%, to { position: fixed; top: 0; }} This idea sounded too good to be true, reminiscent of the urge when you meet a genie to ask for unlimited wishes. But it works! The next puzzle piece is how to use this one-way animation technique to control something we’d want to display to the user. Divs that instantly stick to the ceiling as soon as they enter the viewport might have their place on a page discussing the movie Alien, but most of the time this type of animation won’t be something we want the user to see.

That’s where named view progress timelines come in. The empty scroll trigger element only has the job of sticking to the top of the viewport as soon as it enters. Next, we set the timeline-scope property of the <body> element so that it matches the sticky element’s view-timeline-name. Now we can apply Ryan’s toggle custom property and style query tricks to let each sticky element trigger arbitrary one-off animations anywhere on the page!

View CSS code
/** Each trigger element will cause a toggle named with * the convention `--scroll-trigger-n` to be flipped * from 0 to 1, which will unpause the animation on * any element with the class .on-scroll-trigger-n **/:root { animation-name: run-scroll-trigger-1, run-scroll-trigger-2 /*etc*/; animation-duration: 1ms; animation-fill-mode: forwards; animation-timeline: --trigger-timeline-1, --trigger-timeline-2 /*etc*/; timeline-scope: --trigger-timeline-1, --trigger-timeline-2 /*etc*/;}@property --scroll-trigger-1 { syntax: "<integer>"; initial-value: 0; inherits: true;}@keyframes run-scroll-trigger-1 { to { --scroll-trigger-1: 1; }}/** Add this class to arbitrary elements we want * to only animate once `.scroll-trigger-1` has come * into view, default them to paused state otherwise **/.on-scroll-trigger-1 { animation-play-state: paused;}/** The style query hack will run the animations on * the element once the toggle is set to true **/@container style(--scroll-trigger-1: 1) { .on-scroll-trigger-1 { animation-play-state: running; }}/** The trigger element which sticks to the top of * the viewport and activates the one-way animation * that will unpause the animation on the * corresponding element marked with `.on-scroll-trigger-n` **/.scroll-trigger-1 { view-timeline-name: --trigger-timeline-1;} Trigger warningWe generate the genericized Web-Slinger.css in 95 lines of SCSS, which isn’t too bad. The drawback is that the more triggers we need, the larger the compiled CSS file. The numbered CSS classes also aren’t semantic, so it would be great to have native support for linking a scroll-triggered element to its trigger based on IDs, reminiscent of the popovertarget attribute for HTML buttons — except this hypothetical attribute would go on each target element and specify the ID of the trigger, which is the opposite of the way popovertarget works.

```

Hello world

``` Do androids dream of standardized scroll triggers?As I mentioned at the start, Bramus has teased that scroll-triggered animations are something we’d like to ship in a future version of Chrome, but it still needs a bit of work before we can do that. I’m looking forward to standardized scroll-triggered animations built into the browser. We could do worse than a convention resembling Web-Slinger.css for declaratively defining scroll-triggered animations, but I know I am not objective about Web Slinger as its creator. It’s become a bit of a sacred cow for me so I shall stop milking the topic — for now.

Feel free to reference the prototype Web-Slinger.css library in your experimental CodePens, or fork the library itself if you have better ideas about how scroll-triggered animations could be standardized.


Web-Slinger.css: Like Wow.js But With CSS-y Scroll Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

They’re out! Like many of you, I look forward to these coming out each year. I don’t put much stock in surveys but they can be insightful and give a snapshot of the CSS zeitgeist. There are a few little nuggets in this year’s results that I find interesting. But before I get there, you’ll want to also check out what others have already written about it.

  • Josh Comeau digested his takeaways in a recent newsletter.

Oh, I guess that’s it — at least it’s the most formal write-up I’ve seen. There’s a little summary by Ahmad Shadeed at the end of the survey that generally rounds things up. I’ll drop in more links as I find ’em.

In no particular order…

DemographicsJosh has way more poignant thoughts on this than I do. He rightfully calls out discrepancies in gender pay and regional pay, where men are way more compensated than women (a nonsensical and frustratingly never-ending trend) and the United States boasts more $100,000 salaries than anywhere else. The countries with the highest salaries were also the most represented in survey responses, so perhaps the results are no surprise. We’re essentially looking at a snapshot of what it’s like to be a rich, white male developer in the West.

Besides pay, my eye caught the Age Group demographics. As an aging front-ender, I often wonder what we all do when we finally get to retirement age. I officially dropped from the most represented age group (30-39, 42%) a few years ago into the third most represented tier (40-49, 21%). Long gone are my days being with the cool kids (20-29, 27%).

And if the distribution is true to life, I’m riding fast into my sunset years and will be only slightly more represented than those getting into the profession. I don’t know if anyone else feels similarly anxious about aging in this industry — but if you’re one of the 484 folks who identify with the 50+ age group, I’d love to talk with you.

Before we plow ahead, I think it’s worth calling out how relatively “new” most people are to front-end development.

Wow! Forty-freaking-four percent of respondents have less than 10 years of experience. Yes, 10 years is a high threshold, but we’re still talking about a profession that popped up in recent memory.

For perspective, someone developing for 10 years came to the field around 2014. That’s just when we were getting Flexbox, and several years after the big bang of CSS 3 and HTML 5. That’s just under half of developers who never had to deal with the headaches of table layouts, clearfix hacks, image sprites, spacer images, and rasterized rounded corners. Ethan Marcotte’s seminal article on “Responsive Web Design” predates these folks by a whopping four years!

That’s just wild. And exciting. I’m a firm believer in the next generation of front-enders but always hope that they learn from our past mistakes and become masters at the basics.

FeaturesI’m not entirely sure what to make of this section. When there are so many CSS features, how do you determine which are most widely used? How do you pare it down to just 50 features? Like, are filter effects really the most widely used CSS feature? So many questions, but the results are always interesting nonetheless.

What I find most interesting are the underused features. For example, hanging-punctuation comes in dead last in usage (1.57%) but is the feature that most developers (52%) have on their reading list. (If you need some reading material on it, Chris initially published the Almanac entry for hanging-punctuation back in 2013.)

I also see Anchor Positioning at the end of the long tail with reported usage at 4.8%. That’ll go up for sure now that we have at least one supporting browser engine (Chromium) but also given all of the tutorials that have sprung up in the past few months. Yes, we’ve contributed to that noise… but it’s good noise! I think Juan published what might be the most thorough and thoughtful guide on the topic yet.

I’m excited to see Cascade Layers falling smack dab in the middle of the pack at a fairly robust 18.7%. Cascade Layers are super approachable and elegantly designed that I have trouble believing anybody these days when they say that the CSS Cascade is difficult to manage. And even though @scope is currently low on the list (4.8%, same as Anchor Positioning), I’d bet the crumpled gum wrapper in my pocket that the overall sentiment of working with the Cascade will improve dramatically. We’ll still see “CSS is Awesome” memes galore, but they’ll be more like old familiar dad jokes in good time.

(Aside: Did you see the proposed designs for a new CSS logo? You can vote on them as of yesterday, but earlier versions played off the “CSS is Awesome” mean quite beautifully.)

Interestingly enough, viewport units come in at Number 11 with 44.2% usage… which lands them at Number 2 for most experience that developers have with CSS layout. Does that suggest that layout features are less widely used than CSS filters? Again, so many questions.

FrameworksHow many of you were surprised that Tailwind blew past Bootstrap as Top Dog framework in CSS Land? Nobody, right?

More interesting to me is that “No CSS framework” clocks in at Number 13 out of 21 list frameworks. Sure, its 46 votes are dwarfed by the 138 for Material UI at Number 10… but the fact that we’re seeing “no framework” as a ranking option at all would have been unimaginable just three years ago.

The same goes for CSS pre/post-processing. Sass (67%) and PostCSS (38%) are the power players, but “None” comes in third at 19%, ahead of Less, Stylus, and Lightning CSS.

It’s a real testament to the great work the CSSWG is doing to make CSS better every day. We don’t thank the CSSWG enough — thank you, team! Y’all are heroes around these parts.

CSS UsageJosh already has a good take on the fact that only 67% of folks say they test their work on mobile phones. It should be at least tied with the 99% who test on desktops, right? Right?! Who knows, maybe some responses consider things like “Responsive Design Mode” desktop features to be the equivalent of testing on real mobile devices. I find it hard to believe that only 67% of us test mobile.

Oh, and The Great Divide is still alive and well if the results are true and 53% write more JavsScript than CSS in their day-to-day.

Missing CSS FeaturesThis is always a fun topic to ponder. Some of the most-wanted CSS features have been lurking around 10+ years. But let’s look at the top three form this year’s survey:

  • Mixins
  • Conditional Logic
  • Masonry

We’re in luck team! There’s movement on all three of those fronts:

  • A new CSS Functions and Mixins Module draft was published in late June after the CSSWG resolved to adopt the proposal back in February. (Read our notes.)
  • The CSS Working Group (CSSWG) resolved to add an if() conditional to the CSS Values Module Level 5 specification. (Read our notes.)
  • There are competing proposals for how to forge ahead with a CSS-y approach to masonry layouts. One is based on the CSS Grid Layout Module Level 3 draft specifcation and the other is a fresh new module dedicated to masonry. Apple has planted its flag. So has Chrome. Let the cage-match continue!

ResourcesThis is where I get to toot our own horn a bit because CSS-Tricks continues to place first among y’all when it comes to the blogs you follow for CSS happenings.

I’m also stoked to see Smashing Magazine right there as well. It was fifth in 2023 and I’d like to think that rise is due to me joining the team last year. Correlation implies causation, amirite?

But look at Kevin Powell and Josh in the Top 10. That’s just awesome. It speaks volumes about their teaching talents and the hard work they put into “helping people fall in love with CSS” as Kevin might say it. I was able to help Kevin with a couple of his videos last year (here’s one) and can tell you the guy cares a heckuva lot about making CSS approachable and fun.

Honestly, the rankings are not what we live for. Now that I’ve been given a second wind to work on CSS-Tricks, all I want is to publish things that are valuable to your everyday work as front-enders. That’s traditionally happened as a stream of daily articles but is shifting to more tutorials and resources, whether it’s guides (we’ve published four new ones this year), taking notes on interesting developments, spotlighting good work with links, or expanding the ol’ Almanac to account for things like functions, at-rules, and pseudos (we have lots of work to do).

My 2024 PickNo one asked my opinion but I’ll say it anyway: Personal blogging. I’m seeing more of us in the front-end community getting back behind the keyboards of their personal websites and I’ve never been subscribed to more RSS feeds than I am today. Some started blogging as a “worry stone” during the 2020 lockdown. Some abandoned socials when ~~Twitter~~ X imploded. Some got way into the IndieWeb. Webrings and guestbooks are even gaining new life. Sure, it can be tough keeping up, but what a good problem to have! Let’s make RSS king once and for all.

That’s a wrap!Seriously, a huge thanks to Sacha Greif and the entire Devographics team for the commitment to putting this survey together every year. It’s always fun. And the visualizations are always to die for.


State of CSS 2024 Results originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

In this article, I try to summarize the best practices mentioned by various accessibility experts and their work (like this, this, and this) into a single article that’s easy to read, understand, and apply.

Let’s begin.

What are tooltips?Tooltips are used to provide simple text hints for UI controls. Think of them as tips for tools. They’re basically little bubbles of text content that pop up when you hover over an unnamed control (like the bell icon in Stripe).

The “Notifications” text that pops up when you hover over Stripe’s bell icon is a tooltip.If you prefer more of a formal definition, Sarah Highley provides us with a pretty good one:

A “tooltip” is a non-modal (or non-blocking) overlay containing text-only content that provides supplemental information about an existing UI control. It is hidden by default, and becomes available on hover or focus of the control it describes.

She further goes on to say:

That definition could even be narrowed down even further by saying tooltips must provide only descriptive text.

This narrowed definition is basically (in my experience) how every accessibility expert defines tooltips:

  • A tooltip is a popover.
  • Tooltips must not contain interactive content.

Heydon Pickering takes things even further, saying: If you’re thinking of adding interactive content (even an ok button), you should be using dialog instead.

In his words:

You’re thinking of dialogs. Use a dialog.

Two kinds of tooltipsTooltips are basically only used for two things:

  1. Labeling an icon
  2. Providing a contextual description of an icon

Heydon separates these cleanly into two categories, “Primary Label” and “Auxiliary description” in his Inclusive Components article on tooltips and toggletips).

LabelingIf your tooltip is used to label an icon — using only one or two words — you should use the aria-labelledby attribute to properly label it since it is attached to nothing else on the page that would help identify it.

<button aria-labelledby="notifications"> ... </button><div role="tooltip" id="notifications">Notifications</div> You could provide contextual information, like stating the number of notifications, by giving a space-separated list of ids to aria-labelledby.

<button aria-labelledby="notifications-count notifications-label"> <!-- bell icon here --> <span id="notifications-count">3</span></button> <div role="tooltip" id="notifications-label">Notifications</div> Providing contextual descriptionIf your tooltip provides a contextual description of the icon, you should use aria-describedby. But, when you do this, you also need to provide an accessible name for the icon.

In this case, Heydon recommends including the label as the text content of the button. This label would be hidden visually from sighted users but read for screen readers.

Then, you can add aria-describedby to include the auxiliary description.

<button class="notifications" aria-describedby="notifications-desc"> <!-- icon for bell here --> <span id="notifications-count">3</span> <span class="visually-hidden">Notifications</span></button> <div role="tooltip" id="notifications-desc">View and manage notifications settings</div> Here, screen readers would say “3 notifications” first, followed by “view and manage notifications settings” after a brief pause.

Additional tooltip dos and don’tsHere are a couple of additional points you should be aware of:

Do:

  • Use aria-labellebdy or aria-describedby attributes depending on the type of tooltip you’re building.
  • Use the tooltip role even if it doesn’t do much in screen readers today, because it may extend accessibility support for some software.
  • Open tooltips on mouseover or focus, and close them on mouseout or blur.
  • Allow a mouse user to move their mouse over the tooltip content without dismissing the tooltip.
  • Allow a keyboard user to close the tooltip on the Escape button, per WCAG Success Criterion 1.4.13.

Don’t:

  • Don’t use the title attribute. Much has been said about this so I shall not repeat them.
  • Don’t use the aria-haspopup attribute with the tooltip role because aria-haspopup signifies interactive content while tooltip should contain non-interactive content.
  • Don’t include essential content inside tooltips because some screen readers may ignore aria-labelledby or aria-describedby. (It’s rare, but possible.)

Tooltip limitations and alternativesTooltips are inaccessible to most touch devices because:

  • users cannot hover over a button on a touch device, and
  • users cannot focus on a button on a touch device.

The best alternative is not to use tooltips, and instead, find a way to include the label or descriptive text in the design.

If the “tooltip” contains a lot of content — including interactive content — you may want to display that information with a Toggletip (or just use a <dialog> element).

Heydon explains toggletips nicely and concisely:

Toggletips exist to reveal information balloons. Often they take the form of little “i” icons.

These informational icons should be wrapped within a <button> element. When opened, screen readers can announce the text contained in it through a live region with the status role.

<span class="tooltip-container"> <button type="button" aria-label="more info">i</button> <span role="status">This clarifies whatever needs clarifying</span></span> Speaking anymore about toggletips detracts this article from tooltips so I’ll point you to Heydon’s “Tooltips and Toggletips” article if you’re interested in chasing this short rabbit hole.

That’s all you need to know about tooltips and their current best practices!

Further reading* Clarifying the Relationship Between Popovers and Dialogs (Zell Liew) * Tooltips and Toggletips (Inclusive Components) * Tooltips in the time of WCAG 2.1 (Sarah Higley) * Short note on aria-label, aria-labelledby, and aria-describedby (Léonie Watson) * Some Hands-On with the HTML Dialog Element (Chris Coyier)


Tooltip Best Practices originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

You’d be forgiven for thinking coding up both a dark and a light mode at once is a lot of work. You have to remember @media queries based on prefers-color-scheme as well as extra complications that arise when letting visitors choose whether they want light or dark mode separately from the OS setting. And let’s not forget the color palette itself! Switching from a “light” mode to a “dark” mode may involve new variations to get the right amount of contrast for an accessible experience.

It is indeed a lot of work. But I’m here to tell you it’s now a lot simpler with modern CSS!

Default HTML color scheme(s)We all know the “naked” HTML theme even if we rarely see it as we’ve already applied a CSS reset or our favorite boilerplate CSS before we even open localhost. But here’s a news flash: HTML doesn’t only have the standard black-on-white theme, there is also a native white-on-black version.

We have two color schemes available to use right out of the box!If you want to create a dark mode interface, this is a great base to work with and saves you from having to account for annoying details, like dark inputs, buttons, and other interactive elements.

Live Demo on CodePenSwitching color schemes automatically based on OS preferenceWithout any @media queries — or any other CSS at all — if all we did was declare color-scheme: light dark on the root element, the page will apply either the light or dark color scheme automatically by looking at the visitor’s operating system (OS) preferences. Most OSes have a built-in accessibility setting for your preferred color scheme — “light”, “dark”, or even “auto” — and browsers respect that setting.

html { color-scheme: light dark;} We can even accomplish this without CSS directly in the HTML document in a <meta> tag:

<meta name="color-scheme" content="light dark"> Whether you go with CSS or the HTML route, it doesn’t matter — they both work the same way: telling the browser to make both light and dark schemes available and apply the one that matches the visitor’s preferences. We don’t even need to litter our styles with prefers-color-scheme instances simply to swap colors because the logic is built right in!

You can apply light or dark values to the color-scheme property. At the same time, I’d say that setting color-scheme: light is redundant, as this is the default color scheme with or without declaring it.

You can, of course, control the <meta> tag or the CSS property with JavaScript.

There’s also the possibility of applying the color-scheme property on specific elements instead of the entire page in one fell swoop. Then again, that means you are required to explicitly declare an element’s color and background-color properties; otherwise the element is transparent and inherits its text color from its parent element.

What values should you give it? Try:

Default text and background color variablesThe “black” colors of these native themes aren’t always completely black but are often off-black, making the contrast a little easier on the eyes. It’s worth noting, too, that there’s variation in the blackness of “black” between browsers.

What is very useful is that this default not-pure-black and maybe-not-pure-white background-color and text color are available as <system-color> variables. They also flip their color values automatically with color-scheme!

They are: Canvas and CanvasText.

These two variables can be used anywhere in your CSS to call up the current default background color (Canvas) or text color (CanvasText) based on the current color scheme. If you’re familiar with the currentColor value in CSS, it seems to function similarly. CanvasText, meanwhile, remains the default text color in that it can’t be changed the way currentColor changes when you assign something to color.

In the following examples, the only change is the color-scheme property:

Not bad! There are many, many more of these system variables. They are case-insensitive, often written in camelCase or PascalCase for readability. MDN lists 19 <system-color> variables and I’m dropping them in below for reference.

Open to view 19 system color names and descriptions * AccentColor: The background color for accented user interface controls * AccentColorText: The text color for accented user interface controls * ActiveText: The text color of active links * ButtonBorder: The base border color for controls * ButtonFace: The background color for controls * ButtonText: The text color for controls * Canvas: The background color of an application’s content or documents * CanvasText: The text color used in an application’s content or documents * Field: The background color for input fields * FieldText: The text color inside form input fields * GrayText: The text color for disabled items (e.g., a disabled control) * Highlight: The background color for selected items * HighlightText: The text color for selected items * LinkText: The text color used for non-active, non-visited links * Mark: The background color for text marked up in a <mark> element * MarkText: The text color for text marked up in a <mark> element * SelectedItem: The background color for selected items (e.g., a selected checkbox) * SelectedItemText: The text color for selected items * VisitedText: The text visited links Cool, right? There are many of them! There are, unfortunately, also discrepancies as far as how these color keywords are used and rendered between different OSes and browsers. Even though “evergreen” browsers arguably support all of them, they don’t all actually match what they’re supposed to, and fail to flip with the CSS color-scheme property as they should.

Egor Kloos (also known as dutchcelt) is keeping an eye on the current status of system colors, including which ones exist and the browsers that support them, something he does as part of a classless CSS framework cleverly called system.css.

CodePen Embed FallbackDeclaring colors for both modes togetherOK good, so now you have a page that auto-magically flips dark and light colors according to system preferences. Whether you choose to use these system colors or not is up to you. I just like to point out that “dark” doesn’t always have to mean pure “black” just as “light” doesn’t have to mean pure “white.” There are lots more colors to pair together!

But what’s the best or simplest way to declare colors so they work in both light and dark mode?

In my subjective reverse-best order:

Third place: Declare color opacityYou could keep all the same background colors in dark and light modes, but declare them with an opacity (i.e. rgb(128 0 0 / 0.5) or #80000080). Then they’ll have the Canvas color shine through.

It’s unusable in this way for text colors, and you may end up with somewhat muted colors. But it is a nice easy way to get some theming done fast. I did this for the code blocks on this old light and dark mode demo.

Second place: Use color-mix()Like this:

color-mix(in oklab, Canvas 75%, RebeccaPurple); Similar (but also different) to using opacity to mute a color is mixing colors in CSS. We can even mix the system color variables! For example, one of the colors can be either Canvas or CanvasText so that the background color always mixes with Canvas and the text color always mixes with CanvasText.

We now have the CSS color-mix() function to help us with this. The first argument in the function defines the color space where the color mixing happens. For example, we can tell the function that we are working in the OKLAB color space, which is a rectangular color space like sRGB making it ideal to mix with sRGB color values for predictable results. You can certainly mix colors from different color spaces — the OKLAB/sRGB combination happens to work for me in this instance.

The second and third arguments are the colors you want to mix, and in what proportion. Proportions are optional but expressed in percentages. Without declaring a proportion, the mix is an even 50%-50% split. If you add percentages for both colors and they don’t match up to 100%, it does a little math for you to prevent breakages.

The color-mix() approach is useful if you’re happy to keep the same hues and color saturations regardless of whether the mode is light or dark.

In this example, as you change the value of the hue slider, you’ll see color changes in the themed boxes, following the theme color but mixed with Canvas and CanvasText:

CodePen Embed FallbackYou may have noticed that I used OKLCH and HSL color spaces in that last example. You may also have noticed that the HSL-based theme color and the themed paragraph were a lot more “flashy” as you moved the hue slider.

I’ve declared colors using a polar color space, like HSL, for years, loving that you can easily take a hue and go up or down the saturation and lightness scales based on need. But, I concede that it’s problematic if you’re working with multiple hues while trying to achieve consistent perceived lightness and saturation across them all. It can be difficult to provide ample contrast across a spectrum of colors with HSL.

The OKLCH color space is also polar just like HSL, with the same benefits. You can pick your hue and use the chroma value (which is a bit like saturation in HSL) and the lightness scales accurately in the same way. Both OKLCH and OKLAB are designed to better match what our eyes perceive in terms of brightness and color compared to transitioning between colors in the sRGB space.

While these color spaces may not explicitly answer the age-old question, Is my blue the same as your blue? the colors are much more consistent and require less finicking when you decide to base your whole website’s palette on a different theme color. With these color spaces, the contrasts between the computed colors remain much the same.

First place (winner!): Use light-dark()Like this:

light-dark(lavender, saddlebrown); With the previous color-mix() example, if you choose a pale lavender in light mode, its dark mode counterpart is very dark lavender.

The light-dark() function, conversely, provides complete control. You might want that element to be pale lavender in light mode and a deep burnt sienna brown in dark mode. Why not? You can still use color-mix() within light-dark() if you like — declare the colors however you like, and gain much more fine-grained control over your colors.

Feel free to experiment in the following editable demo:

CodePen Embed FallbackUsing color-scheme: light dark; — or the corresponding meta tag in HTML on your page —is a prerequisite for the light-dark() function because it allows the function to respect a person’s system preference, or whichever single light or dark value you have set on color-scheme.

Another consideration is that light-dark() is newly available across browsers, with just over 80% coverage across all users at the time I’m writing this. So, you might consider including a fallback in your CSS for browsers that lack support for the function.

What makes using color-scheme and light-dark() better than using @media queries?@media queries have been excellent tools, but using them to query prefers-color-scheme only ever follows the preference set within the person’s operating system. This is fine until you (rightfully) want to offer the visitor more choices, decoupled from whether they prefer the UI on their device to be dark or light.

We’re already capable of doing that, of course. We’ve become used to a lot of jiggery-pokery with extra CSS classes, using duplicated styles, or employing custom properties to make it happen.

The joy of using color-scheme is threefold:

  • It gives you the basic monochrome dark mode for free!
  • It can natively do the mode switching based on OS mode preference.
  • You can use JavaScript to toggle between light and dark mode, and the colors declared in the light-dark() functions will follow it.

Light, dark, and auto mode controlsEssentially, all we are doing is setting one of three options for whether the color-scheme is light, dark, or updates auto-matically.

I advise offering all three as discrete options, as it removes some complications for you! Any new visitor to the site will likely be in auto mode because accepting the visitor’s OS setting is the least jarring default state. You then give that person the choice to stay with that or swap it out for a different color scheme. This way, there’s no need to sniff out what mode someone prefers to, for example, display the correct icon on a toggle and make it perform the correct action. There is also no need to keep an event listener on prefers-color-scheme in case of changes — your color-scheme: light dark declaration in CSS handles that for you.

Adjusting color-scheme in pure CSSYes, this is totally possible! But the approach comes with a few caveats:

  • You can’t use <button> — only radio inputs, or <options> in a <select> element.
  • It only works on a per page basis, not per website, which means changes are lost on reload or refresh.
  • The browser needs to support the :has() pseudo-selector. Most modern browsers do, but some folks using older devices might miss out on the experience.

Using the :has() pseudo-selectorThis approach is almost alarmingly simple and is fantastic for a simple one-pager! Most of the heavy lifting is done with this:

/* default, or 'auto' */html { color-scheme: light dark;}html:has([value="light"]:checked) { color-scheme: light;}html:has([value="dark"]:checked) { color-scheme: dark;} The second and third rulesets above look for an attribute called value on any element that has “light” or “dark” assigned to it, then change the color-scheme to match only if that element is :checked.

This approach is not very efficient if you have a huge page full of elements. In those cases, it’s better to be more specific. In the following two examples, the CSS selectors check for value only within an element containing id="mode-switcher".

html:has(#mode-switcher [value="light"]:checked) { color-scheme: light }/* Did you know you don't need the ";" for a one-liner? Now you do! */ Using a <select> element:

CodePen Embed FallbackUsing <input type="radio">:

CodePen Embed FallbackWe could theoretically use checkboxes for this, but since checkboxes are not supposed to be used for mutually exclusive options, I won’t provide an example here. What happens in the case of more than one option being checked? The last matching CSS declaration wins (which is dark in the examples above).

Adjusting color-scheme in HTML with JavaScriptI subscribe to Jeremy Keith’s maxim when it comes to reaching for JavaScript:

JavaScript should only do what only JavaScript can do.

This is exactly that kind of situation.

If you want to allow visitors to change the color scheme using buttons, or you would like the option to be saved the next time the visitor comes to the site, then we do need at least some JavaScript. Rather than using the :has() pseudo-selector in CSS, we have a few alternative approaches for changing the color-scheme when we add JavaScript to the mix.

Using <meta> tagsIf you have set your color-scheme within a meta tag in the <head> of your HTML:

<meta name="color-scheme" content="light dark"> …you might start by making a useful constant like so:

const colorScheme = document.querySelector('meta[name="color-scheme"]'); And then you can manipulate that, assigning it light or dark as you see fit:

colorScheme.setAttribute("content", "light"); // to light modecolorScheme.setAttribute("content", "dark"); // to dark modecolorScheme.setAttribute("content", "light dark"); // to auto mode This is a very similar approach to using <meta> tags but is different if you are setting the color-scheme property in CSS:

html { color-scheme: light dark; } Instead of setting a colorScheme constant as we just did in the last example with the <meta> tag, you might select the <html> element instead:

const html = document.querySelector('html'); Now your manipulations look like this:

html.style.setProperty("color-scheme", "light"); // to light modehtml.style.setProperty("color-scheme", "dark"); // to dark modehtml.style.setProperty("color-scheme", "light dark"); // to auto mode I like to turn those manipulations into functions so that I can reuse them:

function switchAuto() { html.style.setProperty("color-scheme", "light dark");}function switchLight() { html.style.setProperty("color-scheme", "light");}function switchDark() { html.style.setProperty("color-scheme", "dark");} Alternatively, you might like to stay as DRY as possible and do something like this:

function switchMode(mode) { html.style.setProperty("color-scheme", mode === "auto" ? "light dark" : mode);} The following demo shows how this JavaScript-based approach can be used with buttons, radio buttons, and a <select> element. Please note that not all of the controls are hooked up to update the UI — the demo would end up too complicated since there’s no world where all three types of controls would be used in the same UI!

CodePen Embed FallbackI opted to use onchange and onclick in the HTML elements mainly because I find them readable and neat. There’s nothing wrong with instead attaching a change event listener to your controls, especially if you need to trigger other actions when the options change. Using onclick on a button doesn’t only work for clicks, the button is still keyboard-focusable and can be triggered with Spacebar and Enter too, as usual.

Remembering the selection for repeat visitsThe biggest caveat to everything we’ve covered so far is that this only works once. In other words, once the visitor has left the site, we’re doing nothing to remember their color scheme preference. It would be a better user experience to store that preference and respect it anytime the visitor returns.

The Web Storage API is our go-to for this. And there are two available ways for us to store someone’s color scheme preference for future visits.

localStorageLocal storage saves values directly on the visitor’s device. This makes it a nice way to keep things off your server, as the stored data never expires, allowing us to call it anytime. That said, we’re prone to losing that data whenever the visitor clears cookies and cache and they’ll have to make a new selection that is freshly stored in localStorage.

You pick a key name and give it a value with .setItem():

localStorage.setItem("mode", "dark"); The key and value are saved by the browser, and can be called up again for future visits:

const mode = localStorage.getItem("mode"); You can then use the value stored in this key to apply the person’s preferred color scheme.

sessionStorageSession storage is thrown away as soon as a visitor browses away to another site or closes the current window/tab. However, the data we capture in sessionStorage persists while the visitor navigates between pages or views on the same domain.

It looks a lot like localStorage:

sessionStorage.setItem("mode", "dark");const mode = sessionStorage.getItem("mode"); Which storage method should I use?Personally, I started with sessionStorage because I wanted my site to be as simple as possible, and to avoid anything that would trigger the need for a GDPR-compliant cookie banner if we were holding onto the person’s preference after their session ends. If most of your traffic comes from new visitors, then I suggest using sessionStorage to prevent having to do extra work on the GDPR side of things.

That said, if your traffic is mostly made up of people who return to the site again and again, then localStorage is likely a better approach. The convenience benefits your visitors, making it worth the GDPR work.

The following example shows the localStorage approach. Open it up in a new window or tab, pick a theme other than what’s set in your operating system’s preferences, close the window or tab, then re-open the demo in a new window or tab. Does the demo respect the color scheme you selected? It should!

CodePen Embed FallbackChoose the “Auto” option to go back to normal.

If you want to look more closely at what is going on, you can open up the developer tools in your browser (F12 for Windows, CTRL+ click and select “Inspect” for macOS). From there, go into the “Application” tab and locate https://cdpn.io in the list of items stored in localStorage. You should see the saved key (mode) and the value (dark or light). Then start clicking on the color scheme options again and watch the mode update in real-time.

AccessibilityCongratulations! If you have got this far, you are considering or already providing versions of your website that are more comfortable for different people to use.

For example:

  • People with strong floaters in their eyes may prefer to use dark mode.
  • People with astigmatism may be able to focus more easily in light mode.

So, providing both versions leaves fewer people straining their eyes to access the content.

Contrast levelsI want to include a small addendum to this provision of a light and dark mode. An easy temptation is to go full monochrome black-on-white or white-on-black. It’s striking and punchy! I get it. But that’s just it — striking and punchy can also trigger migraines for some people who do a lot better with lower contrasts.

Providing high contrast is great for the people who need it. Some visual impairments do make it impossible to focus and get a sharp image, and a high contrast level can help people to better make out the word shapes through a blur. Minimum contrast levels are important and should be exceeded.

Thankfully, alongside other media queries, we can also query prefers-contrast which accepts values for no-preference, more, less, or custom.

In the following example (which uses :has() and color-mix()), a <select> element is displayed to offer contrast settings. When “Low” is selected, a filter of contrast(75%) is placed across the page. When “High” is selected, CanvasText and Canvas are used unmixed for text color and background color:

CodePen Embed FallbackAdding a quick high and low contrast theme gives your visitors even more choice for their reading comfort. Look at that — now you have three contrast levels in both dark and light modes — six color schemes to choose from!

ARIA-pressedARIA stands for Accessible Rich Internet Applications and is designed for adding a bit of extra info where needed to screen readers and other assistive tech.

The words “where needed” do heavy lifting here. It has been said that, like apostrophes, no ARIA is better than bad ARIA. So, best practice is to avoid putting it everywhere. For the most part (with only a few exceptions) native HTML elements are good to go out of the box, especially if you put useful text in your buttons!

The little bit of ARIA I use in this demo is for adding the aria-pressed attribute to the buttons, as unlike a radio group or select element, it’s otherwise unclear to anyone which button is the “active” one, and ARIA helps nicely with this use case. Now a screen reader will announce both its accessible name and whether it is in a pressed or unpressed state along with a button.

Following is an example code snippet with all the ARIA code bolded — yes, suddenly there’s lots more! You may find more elegant (or DRY-er) ways to do this, but showing it this way first makes it more clear to demonstrate what’s happening.

Our buttons have ids, which we have used to target them with some more handy consts at the top. Each time we switch mode, we make the button’s aria-pressed value for the selected mode true, and the other two false:

const html = document.querySelector("html");const mode = localStorage.getItem("mode");const lightSwitch = document.querySelector('#lightSwitch');const darkSwitch = document.querySelector('#darkSwitch');const autoSwitch = document.querySelector('#autoSwitch');if (mode === "light") switchLight();if (mode === "dark") switchDark();function switchAuto() { html.style.setProperty("color-scheme", "light dark"); localStorage.removeItem("mode"); lightSwitch.setAttribute("aria-pressed","false"); darkSwitch.setAttribute("aria-pressed","false"); autoSwitch.setAttribute("aria-pressed","true");}function switchLight() { html.style.setProperty("color-scheme", "light"); localStorage.setItem("mode", "light"); lightSwitch.setAttribute("aria-pressed","true"); darkSwitch.setAttribute("aria-pressed","false"); autoSwitch.setAttribute("aria-pressed","false");}function switchDark() { html.style.setProperty("color-scheme", "dark"); localStorage.setItem("mode", "dark"); lightSwitch.setAttribute("aria-pressed","false"); darkSwitch.setAttribute("aria-pressed","true"); autoSwitch.setAttribute("aria-pressed","false");} On load, the buttons have a default setting, which is when the “Auto” mode button is active. Should there be any other mode in the localStorage, we pick it up immediately and run either switchLight() or switchDark(), both of which contain the aria-pressed changes relevant to that mode.

<button id="autoSwitch" aria-pressed="true" type="button" onclick="switchAuto()">Auto</button><button id="lightSwitch" aria-pressed="false" type="button" onclick="switchLight()">Light</button><button id="darkSwitch" aria-pressed="false" type="button" onclick="switchDark()">Dark</button> The last benefit of aria-pressed is that we can also target it for styling purposes:

button[aria-pressed="true"] { background-color: transparent; border-width: 2px;} Finally, we have a nice little button switcher, with its state clearly shown and announced, that remembers your choice when you come back to it. Done!

CodePen Embed FallbackOutroductionOr whatever the opposite of an introduction is…

…don’t let yourself get dragged into the old dark vs light mode argument. Both are good. Both are great! And both modes are now easy to create at once. At the start of your next project, work or hobby, do not give in to fear and pick a side — give both a try, and give in to choice.


Come to the light-dark() Side originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A whole bunch of years ago, we posted on this idea here on CSS-Tricks. We figured it was time to update that and do the subject justice.

Imagine a scenario where you need to split a layout in half. Content on the left and content on the right. Basically two equal height columns are needed inside of a container. Each side takes up exactly half of the container, creating a distinct break between one. Like many things in CSS, there are a number of ways to go about this and we’re going to go over many of them right now!

Update (Oct. 25, 2024): Added an example that uses CSS Anchor Positioning.

Using Background GradientOne simple way we can create the appearance of a changing background is to use gradients. Half of the background is set to one color and the other half another color. Rather than fade from one color to another, a zero-space color stop is set in the middle.

.container { background: linear-gradient( to right, #ff9e2c 0%, #ff9e2c 50%, #b6701e 50%, #b6701e 100% );} This works with a single container element. However, that also means that it will take working with floats or possibly some other layout method if content needs to fill both sides of the container.

CodePen Embed FallbackUsing Absolute PositioningAnother route might be to set up two containers inside of a parent container, position them absolutely, split them up in halves using percentages, then apply the backgrounds. The benefit here is that now we have two separate containers that can hold their own content.

CodePen Embed FallbackAbsolute positioning is sometimes a perfect solution, and sometimes untenable. The parent container here will need to have a set height, and setting heights is often bad news for content (content changes!). Not to mention absolute positioned elements are out of the document flow. So it would be hard to get this to work while, say, pushing down other content below it.

Using (fake) TablesYeah, yeah, tables are so old school (not to mention fraught with accessibility issues and layout inflexibility). Well, using the display: table-cell; property can actually be a handy way to create this layout without writing table markup in HTML. In short, we turn our semantic parent container into a table, then the child containers into cells inside the table — all in CSS!

CodePen Embed FallbackYou could even change the display properties at breakpoints pretty easily here, making the sides stack on smaller screens. display: table; (and friends) is supported as far back as IE 8 and even old Android, so it’s pretty safe!

Using FloatsWe can use our good friend the float to arrange the containers beside each other. The benefit here is that it avoids absolute positioning (which as we noted, can be messy).

CodePen Embed FallbackIn this example, we’re explicitly setting heights to get them to be even. But you don’t really get that ability with floats by default. You could use the background gradient trick we already covered so they just look even. Or look at fancy negative margin tricks and the like.

Also, remember you may need to clear the floats on the parent element to keep the document flow happy.

Using Inline-BlockIf clearing elements after floats seems like a burden, then using display: inline-block is another option. The trick here is to make sure that the elements for the individual sides have no breaks or whitespace in between them in the HTML. Otherwise, that space will be rendered as a literal space and the second half will break and fall.

CodePen Embed FallbackAgain there is nothing about inline-block that helps us equalize the heights of the sides, so you’ll have to be explicit about that.

There are also other potential ways to deal with that spacing problem described above.

Using FlexboxFlexbox is a pretty fantastic way to do this, just note that it’s limited to IE 10 and up and you may need to get fancy with the prefixes and values to get the best support.

Using this method, we turn our parent container into a flexible box with the child containers taking up an equal share of the space. No need to set widths or heights! Flexbox just knows what to do, because the defaults are set up perfectly for this. For instance, flex-direction: row; and align-items: stretch; is what we’re after, but those are the defaults so we don’t have to set them. To make sure they are even though, setting flex: 1; on the sides is a good plan. That forces them to take up equal shares of the space.

CodePen Embed FallbackIn this demo we’re making the side flex containers as well, just for fun, to handle the vertical and horizontal centering.

Using Grid LayoutFor those living on the bleeding edge, the CSS Grid Layout technique is like the Flexbox and Table methods merged into one. In other words, a container is defined, then split into columns and cells which can be filled flexibly with child elements.

CodePen Embed FallbackCSS Anchor PositioningThis started rolling out in 2024 and we’re still waiting for full browser support. But we can use CSS Anchor Positioning to “attach” one element to another — even if those two elements are completely unrelated in the markup.

The idea is that we have one element that’s registered as an “anchor” and another element that’s the “target” of that anchor. It’s like the target element is pinned to the anchor. And we get to control where we pin it!

.anchor { anchor-name: --anchor;}.target { anchor-position: --anchor; position: absolute; /* required */} This sets up an .anchor and establishes a relationship with a .target element. From here, we can tell the target which side of the anchor it should pin to.

.anchor { anchor-name: --anchor;}.target { anchor-position: --anchor; position: absolute; /* required */ left: anchor(right);} CodePen Embed FallbackIsn’t it cool how many ways there are to do things in CSS?


Left Half and Right Half Layout – Many Different Ways originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Terence Eden on using text-wrap: balance for more than headings:

But the name is, I think, slightly misleading. It doesn’t only work on text. It will work on any content. For example – I have a row of icons at the bottom of this page. If the viewport is too narrow, a single icon might drop to the next line. That can look a bit weird.

Heck yeah. I may have reached for some sort of auto-fitting grid approach, but hey, may as well go with a one-liner if you can! And while we’re on the topic, I just wanna mention that, yes, text-wrap: balance will work on any content. — just know that the spec is a little opinionated on this and make sure that the content is fewer than five lines.

There’s likely more nuance to come if the note for Issue 6 in the spec is any indication about possibly allowing for a line length minimum:

Suggestion for value space is match-indent | <length> | <percentage> (with Xch given as an example to make that use case clear). Alternately <integer> could actually count the characters.


You can use text-wrap: balance; on icons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The difference between Popovers (i.e., the popover attribute) and Dialogs (i.e., both the <dialog> element and the dialog accessible role) is incredibly confusing — so much that many articles (like this, this, and this) have tried to shed some light on the issue.

If you’re still feeling confused, I hope this one clears up that confusion once and for all.

Distinguishing Popovers From DialogsLet’s pull back on the technical implementations and consider the greater picture that makes more sense and puts everything into perspective.

The reason for this categorization comes from a couple of noteworthy points.

First, we know that a popover is content that “pops” up when a user clicks a button (or hovers over it, or focuses on it). In the ARIA world, there is a useful attribute called aria-haspopup that categorizes such popups into five different roles:

  • menu
  • listbox
  • tree
  • grid
  • dialog

Strictly speaking, there’s a sixth value, true, that evaluates to menu. I didn’t include it above since it’s effectively just menu.

By virtue of dialog being on this list, we already know that dialog is a type of popover. But there’s more evidence behind this too.

The Three Types of DialoguesSince we’re already talking about the dialog role, let’s further expand that into its subcategories:

Dialogs can be categorized into three main kinds:

  • Modal: A dialog with an overlay and focus trapping
  • Non-Modal: A dialog with neither an overlay nor focus trapping
  • Alert Dialog: A dialog that alerts screen readers when shown. It can be either modal or non-modal.

This brings us to another reason why a dialog is considered a popover.

Some people may say that popovers are strictly non-modal, but this seems to be a major misunderstanding — because popovers have a ::backdrop pseudo-element on the top layer. The presence of ::backdrop indicates that popovers are modal. Quoting the CSS-Tricks almanac:

The ::backdrop CSS pseudo-element creates a backdrop that covers the entire viewport and is rendered immediately below a <dialog>, an element with the popup attribute, or any element that enters fullscreen mode using the Fullscreen API.

That said, I don’t recommend using the Popover API for modality because it doesn’t have a showModal() method (that <dialog> has) that creates inertness, focus trapping, and other necessary features to make it a real modal. If you only use the Popover API, you’ll need to build those features from scratch.

So, the fact that popovers can be modal means that a dialog is simply one kind of popover.

A Popover Needs an Accessible RolePopovers need a role to be accessible. Hidde has a great article on selecting the right role, but I’m going to provide some points in this article as well.

To start, you can use one of the aria-haspopup roles mentioned above:

  • menu
  • listbox
  • tree
  • grid
  • dialog

You could also use one of the more complex roles like:

  • treegrid
  • alertdialog

There are two additional roles that are slightly more contentious but may do just fine.

  • tooltip
  • status

To understand why tooltip and status could be valid popover roles, we need to take a detour into the world of tooltips.

A Note on TooltipsFrom a visual perspective, a tooltip is a popover because it contains a tiny window that pops up when the tooltip is displayed.

I included tooltip in the mental model because it is reasonable to implement tooltip with the Popover API.

```

...

`` Thetooltiprole doesn’t do much in screen readers today so you need to usearia-describedby` to create accessible tooltips. But it is still important because it may extend accessibility support for some software.

But, from an accessibility standpoint, tooltips are not popovers. In the accessibility world, tooltips must not contain interactive content. If they contain interactive content, you’re not looking at a tooltip, but a dialog.

You’re thinking of dialogs. Use a dialog.

Heydon Pickering, “Your Tooltips are Bogus”

This is also why aria-haspopup doesn’t include tooltiparia-haspopup is supposed to signify interactive content but a tooltip must not contain interactive content.

With that, let’s move on to status which is an interesting role that requires some explanation.

Why status?Tooltips have a pretty complex history in the world of accessible interfaces so there’s a lot of discussion and contention over it.

To keep things short (again), there’s an accessibility issue with tooltips since tooltips should only show on hover. This means screen readers and mobile phone users won’t be able to see those tooltips (since they can’t hover on the interface).

Steve Faulkner created an alternative — toggletips — to fill the gap. In doing so, he explained that toggletip content must be announced by screen readers through live regions.

When initially displayed content is announced by (most) screen readers that support aria-live

Heydon Pickering later added that status can be used in his article on toggletips.

We can supply an empty live region, and populate it with the toggletip “bubble” when it is invoked. This will both make the bubble appear visually and cause the live region to announce the tooltip’s information.

```

This clarifies whatever needs clarifying `` This is whystatuscan be a potential role for apopover`, but you must use discretion when creating it.

That said, I’ve chosen not to include the status role in the Popover mental model because status is a live region role and hence different from the rest.

In SummaryHere’s a quick summary of the mental model:

  • Popover is an umbrella term for any kind of on-demand popup.
  • Dialog is one type of popover — a kind that creates a new window (or card) to contain some content.

When you internalize this, it’s not hard to see why the Popover API can be used with the dialog element.

```

...

...... `` When choosing arole` for your popover, you can use one of these roles safely.

  • menu
  • listbox
  • tree
  • grid
  • treegrid
  • dialog
  • alertdialog

The added benefit is most of these roles work together with aria-haspopup which gained decent support in screen readers last year.

Of course, there are a couple more you can use like status and tooltip, but you won’t be able to use them together with aria-haspopup.

Further Reading* aria-haspopup property (WAI-ARIA Specification, Version 1.2) * Semantics and the popover attribute: which role to use when? (Hidde de Vries) * aria-hasPopUp less is more (html5accessibility.com) * Tooltips & Toggletips (Inclusive Components) * What’s the Difference Between HTML’s Dialog Element and Popovers? (Chris Coyier)


Clarifying the Relationship Between Popovers and Dialogs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

There’s a lot of math behind fluid typography. CSS does make the math a lot easier these days, but even if you’re comfortable with that, writing the full declaration can be verbose and tough to remember. I know I often have to look it back up, despite having written it maybe a hundred times.

Silvestar made a little VS Code helper to abstract all that. Type in the target values you’re aiming for and the helper expands it on the spot.

He says ChatGPT did the initial lifting before he refined it. I can get behind this sort of AI-flavored usage. Start with an idea, find a starting point, look deeper at it, and shape it into something incredibly useful for a small, single purpose.


Clamp it! VS Code extension originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I’m utterly behind in learning about scroll-driven animations apart from the “reading progress bar” experiments all over CodePen. Well, I’m not exactly “green” on the topic; we’ve published a handful of articles on it including this neat-o one by Lee Meyer published the other week.

Our “oldest” article about the feature is by Bramus, dated back to July 2021. We were calling it “scroll-linked” animation back then. I specifically mention Bramus because there’s no one else working as hard as he is to discover practical use cases where scroll-driven animations shine while helping everyone understand the concept. He writes about it exhaustively on his personal blog in addition to writing the Chrome for Developers documentation on it.

But there’s also this free course he calls “Unleash the Power of Scroll-Driven Animations” published on YouTube as a series of 10 short videos. I decided it was high time to sit, watch, and learn from one of the best. These are my notes from it.


Introduction A scroll-driven animation is an animation that responds to scrolling. There’s a direct link between scrolling progress and the animation’s progress. * Scroll-driven animations are different than scroll-triggered* animations, which execute on scroll and run in their entirety. Scroll-driven animations pause, play, and run with the direction of the scroll. It sounds to me like scroll-triggered animations are a lot like the CSS version of the JavaScript intersection observer that fires and plays independently of scroll. * Why learn this? It’s super easy to take an existing CSS animation or a WAAPI animation and link it up to scrolling. The only “new” thing to learn is how to attach an animation to scrolling. Plus, hey, it’s the platform! * There are also performance perks. JavsScript libraries that establish scroll-driven animations typically respond to scroll events on the main thread, which is render-blocking… and JANK! We’re working with hardware-accelerated animations… and NO JANK. Yuriko Hirota has a case study on the performance of scroll-driven animations published on the Chrome blog. * Supported in Chrome 115+. Can use @supports (animation-timeline: scroll()). However, I recently saw Bramus publish an update saying we need to look for animation-range support as well.

@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) { /* Scroll-Driven Animations related styles go here */ /* This check excludes Firefox Nightly which only has a partial implementation at the moment of posting (mid-September 2024). */} * Remember to use prefers-reduced-motion and be mindful of those who may not want them.

Video OneCore Concepts: scroll() and ScrollTimelineLet’s take an existing CSS animation.

@keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); }}#progress { animation: grow-progress 2s linear forwards;} Translation: Start with no width and scale it to its full width. When applied, it takes two seconds to complete and moves with linear easing just in the forwards direction.

This just runs when the #progress element is rendered. Let’s attach it to scrolling.

  • animation-timeline: The timeline that controls the animation’s progress.
  • scroll(): Creates a new scroll timeline set up to track the nearest ancestor scroller in the block direction.

```

progress { animation: grow-progress 2s linear forwards; animation-timeline: scroll();}

`` That’s it! We’re linked up. Now we can remove theanimation-durationvalue from the mix (or set it toauto`):

```

progress { animation: grow-progress linear forwards; animation-timeline: scroll();}

`` Note that we’re unable to plop theanimation-timelineproperty on theanimationshorthand, at least for now. Bramus calls it a “reset-only sub-property of the shorthand” which is a new term to me. Its value gets reset when you use the shorthand the same waybackground-coloris reset bybackground. That means the best practice is to declareanimation-timeline*after*animation`.

/* YEP! */#progress { animation: grow-progress linear forwards; animation-timeline: scroll();}/* NOPE! */#progress { animation-timeline: scroll(); animation: grow-progress linear forwards;} Let’s talk about the scroll() function. It creates an anonymous scroll timeline that “walks up” the ancestor tree from the target element to the nearest ancestor scroll. In this example, the nearest ancestor scroll is the :root element, which is tracked in the block direction.

We can name scroll timelines, but that’s in another video. For now, know that we can adjust which axis to track and which scroller to target in the scroll() function.

animation-timeline: scroll(<axis> <scroller>); * <axis>: The axis — be it block (default), inline, y, or x. * <scroller>: The scroll container element that defines the scroll position that influences the timeline’s progress, which can be nearest (default), root (the document), or self.

If the root element does not have an overflow, then the animation becomes inactive. WAAPI gives us a way to establish scroll timelines in JavaScript with ScrollTimeline.

const $progressbar = document.querySelector(#progress);$progressbar.style.transformOrigin = '0% 50%';$progressbar.animate( { transform: ['scaleX(0)', 'scaleY()'], }, { fill: 'forwards', timeline: new ScrollTimeline({ source: document.documentElement, // root element // can control `axis` here as well }), }) Video TwoCore Concepts: view() and ViewTimelineFirst, we oughta distinguish a scroll container from a scroll port. Overflow can be visible or clipped. Clipped could be scrolling.

Those two bordered boxes show how easy it is to conflate scrollports and scroll containers. The scrollport is the visible part and coincides with the scroll container’s padding-box. When a scrollbar is present, that plus the scroll container is the root scroller, or the scroll container.

A view timeline tracks the relative position of a subject within a scrollport. Now we’re getting into IntersectionObserver territory! So, for example, we can begin an animation on the scroll timeline when an element intersects with another, such as the target element intersecting the viewport, then it progresses with scrolling.

Bramus walks through an example of animating images in long-form content when they intersect with the viewport. First, a CSS animation to reveal an image from zero opacity to full opacity (with some added clipping).

@keyframes reveal { from { opacity: 0; clip-path: inset(45% 20% 45% 20%); } to { opacity: 1; clip-path: inset(0% 0% 0% 0%); }}.revealing-image { animation: reveal 1s linear both;} This currently runs on the document’s timeline. In the last video, we used scroll() to register a scroll timeline. Now, let’s use the view() function to register a view timeline instead. This way, we’re responding to when a .revealing-image element is in, well, view.

.revealing-image { animation: reveal 1s linear both; /* Rember to declare the timeline after the shorthand */ animation-timeline: view();} At this point, however, the animation is nice but only completes when the element fully exits the viewport, meaning we don’t get to see the entire thing. There’s a recommended way to fix this that Bramus will cover in another video. For now, we’re speeding up the keyframes instead by completing the animation at the 50% mark.

@keyframes reveal { from { opacity: 0; clip-path: inset(45% 20% 45% 20%); } 50% { opacity: 1; clip-path: inset(0% 0% 0% 0%); }} More on the view() function:

animation-timeline: view(<axis> <view-timeline-inset>); We know <axis> from the scroll() function — it’s the same deal. The <view-timeline-inset> is a way of adjusting the visibility range of the view progress (what a mouthful!) that we can set to auto (default) or a <length-percentage>. A positive inset moves in an outward adjustment while a negative value moves in an inward adjustment. And notice that there is no <scroller> argument — a view timeline always tracks its subject’s nearest ancestor scroll container.

OK, moving on to adjusting things with ViewTimeline in JavaScript instead.

const $images = document.querySelectorAll(.revealing-image);$images.forEach(($image) => { $image.animate( [ { opacity: 0, clipPath: 'inset(45% 20% 45% 20%)', offset: 0 } { opacity: 1; clipPath: 'inset(0% 0% 0% 0%)', offset: 0.5 } ], { fill: 'both', timeline: new ViewTimeline({ subject: $image, axis: 'block', // Do we have to do this if it's the default? }), } }) This has the same effect as the CSS-only approach with animation-timeline.

Video ThreeTimeline Ranges DemystifiedLast time, we adjusted where the image’s reveal animation ends by tweaking the keyframes to end at 50% rather than 100%. We could have played with the inset(). But there is an easier way: adjust the animation attachment range,

Most scroll animations go from zero scroll to 100% scroll. The animation-range property adjusts that:

animation-range: normal normal; Those two values: the start scroll and end scroll, default:

animation-range: 0% 100%; Other length units, of course:

animation-range: 100px 80vh; The example we’re looking at is a “full-height cover card to fixed header”. Mouthful! But it’s neat, going from an immersive full-page header to a thin, fixed header while scrolling down the page.

@keyframes sticky-header { from { background-position: 50% 0; height: 100vh; font-size: calc(4vw + 1em); } to { background-position: 50% 100%; height: 10vh; font-size: calc(4vw + 1em); background-color: #0b1584; }} If we run the animation during scroll, it takes the full animation range, 0%-100%.

.sticky-header { position: fixed; top: 0; animation: sticky-header linear forwards; animation-timeline: scroll();} Like the revealing images from the last video, we want the animation range a little narrower to prevent the header from animating out of view. Last time, we adjusted the keyframes. This time, we’re going with the property approach:

.sticky-header { position: fixed; top: 0; animation: sticky-header linear forwards; animation-timeline: scroll(); animation-range: 0vh 90vh;} We had to subtract the full height (100vh) from the header’s eventual height (10vh) to get that 90vh value. I can’t believe this is happening in CSS and not JavaScript! Bramus sagely notes that font-size animation happens on the main thread — it is not hardware-accelerated — and the entire scroll-driven animation runs on the main as a result. Other properties cause this as well, notably custom properties.

Back to the animation range. It can be diagrammed like this:

The animation “cover range”. The dashed area represents the height of the animated target element.Notice that there are four points in there. We’ve only been chatting about the “start edge” and “end edge” up to this point, but the range covers a larger area in view timelines. So, this:

animation-range: 0% 100%; /* same as 'normal normal' */ …to this:

animation-range: cover 0% cover 100%; /* 'cover normal cover normal' */ …which is really this:

animation-range: cover; So, yeah. That revealing image animation from the last video? We could have done this, rather than fuss with the keyframes or insets:

animation-range: cover 0% cover 50%; So nice. The demo visualization is hosted at scroll-driven-animations.style. Oh, and we have keyword values available: contain, entry, exit, entry-crossing, and exit-crossing.

contain``entry``exitThe examples so far are based on the scroller being the root element. What about ranges that are taller than the scrollport subject? The ranges become slightly different.

Just have to be aware of the element’s size and how it impacts the scrollport.This is where the entry-crossing and entry-exit values come into play. This is a little mind-bendy at first, but I’m sure it’ll get easier with use. It’s clear things can get complex really quickly… which is especially true when we start working with multiple scroll-driven animation with their own animation ranges. Yes, that’s all possible. It’s all good as long as the ranges don’t overlap. Bramus uses a contact list demo where contact items animate when they enter and exit the scrollport.

@keyframes animate-in { 0% { opacity: 0; transform: translateY: 100%; } 100% { opacity: 1; transform: translateY: 0%; }}@keyframes animate-out { 0% { opacity: 1; transform: translateY: 0%; } 100% { opacity: 0; transform: translateY: 100%; }}.list-view li { animation: animate-in linear forwards, animate-out linear forwards; animation-timeline: view(); animation-range: entry, exit; /* animation-in, animation-out */} Another way, using entry and exit keywords directly in the keyframes:

@keyframes animate-in { entry 0% { opacity: 0; transform: translateY: 100%; } entry 100% { opacity: 1; transform: translateY: 0%; }}@keyframes animate-out { exit 0% { opacity: 1; transform: translateY: 0%; } exit 100% { opacity: 0; transform: translateY: 100%; }}.list-view li { animation: animate-in linear forwards, animate-out linear forwards; animation-timeline: view();} Notice that animation-range is no longer needed since its values are declared in the keyframes. Wow.

OK, ranges in JavaScript.:

const timeline = new ViewTimeline({ subjext: $li, axis: 'block',})// Animate in$li.animate({ opacity: [ 0, 1 ], transform: [ 'translateY(100%)', 'translateY(0)' ],}, { fill: 'forwards', // One timeline instance with multiple ranges timeline, rangeStart: 'entry: 0%', rangeEnd: 'entry 100%',}) Video FourCore Concepts: Timeline Lookup and Named TimelinesThis time, we’re learning how to attach an animation to any scroll container on the page without needing to be an ancestor of that element. That’s all about named timelines.

But first, anonymous timelines track their nearest ancestor scroll container.

<html> <!-- scroll --> <body> <div class="wrapper"> <div style="animation-timeline: scroll();"></div> </div> </body></html> Some problems might happen like when overflow is hidden from a container:

<html> <!-- scroll --> <body> <div class="wrapper" style="overflow: hidden;"> <!-- scroll --> <div style="animation-timeline: scroll();"></div> </div> </body></html> Hiding overflow means that the element’s content block is clipped to its padding box and does not provide any scrolling interface. However, the content must still be scrollable programmatically meaning this is still a scroll container. That’s an easy gotcha if there ever was one! The better route is to use overflow: clip rather than hidden because that prevents the element from becoming a scroll container.

Hiding oveflow = scroll container. Clipping overflow = no scroll container. Bramus says he no longer sees any need to use overflow: hidden these days unless you explicitly need to set a scroll container. I might need to change my muscle memory to make that my go-to for ~~hiding~~ clipping overflow.

Another funky thing to watch for: absolute positioning on a scroll animation target in a relatively-positioned container. It will never match an outside scroll container that is scroll(inline-nearest) since it is absolute to its container like it’s unable to see out of it.

We don’t have to rely on the “nearest” scroll container or fuss with different overflow values. We can set which container to track with named timelines.

.gallery { position: relative;}.gallery__scrollcontainer { overflow-x: scroll; scroll-timeline-name: --gallery__scrollcontainer; scroll-timeline-axis: inline; /* container scrolls in the inline direction */}.gallery__progress { position: absolute; animation: progress linear forwards; animation-timeline: scroll(inline nearest);} We can shorten that up with the scroll-timeline shorthand:

.gallery { position: relative;}.gallery__scrollcontainer { overflow-x: scroll; scroll-timeline: --gallery__scrollcontainer inline;}.gallery__progress { position: absolute; animation: progress linear forwards; animation-timeline: scroll(inline nearest);} Note that block is the scroll-timeline-axis initial value. Also, note that the named timeline is a dashed-ident, so it looks like a CSS variable.

That’s named scroll timelines. The same is true of named view timlines.

.scroll-container { view-timeline-name: --card; view-timeline-axis: inline; view-timeline-inset: auto; /* view-timeline: --card inline auto */} Bramus showed a demo that recreates Apple’s old cover-flow pattern. It runs two animations, one for rotating images and one for setting an image’s z-index. We can attach both animations to the same view timeline. So, we go from tracking the nearest scroll container for each element in the scroll:

.covers li { view-timeline-name: --li-in-and-out-of-view; view-timeline-axis: inline; animation: adjust-z-index linear both; animation-timeline: view(inline);}.cards li > img { animation: rotate-cover linear both; animation-timeline: view(inline);} …and simply reference the same named timelines:

.covers li { view-timeline-name: --li-in-and-out-of-view; view-timeline-axis: inline; animation: adjust-z-index linear both; animation-timeline: --li-in-and-out-of-view;;}.cards li > img { animation: rotate-cover linear both; animation-timeline: --li-in-and-out-of-view;;} In this specific demo, the images rotate and scale but the updated sizing does not affect the view timeline: it stays the same size, respecting the original box size rather than flexing with the changes.

Phew, we have another tool for attaching animations to timelines that are not direct ancestors: timeline-scope.

timeline-scope: --example; This goes on an parent element that is shared by both the animated target and the animated timeline. This way, we can still attach them even if they are not direct ancestors.

```

...

``` It accepts multiple comma-separated values:

timeline-scope: --one, --two, --three;/* or */timeline-scope: all; /* Chrome 116+ */ There’s no Safari or Firefox support for the all kewword just yet but we can watch for it at Caniuse (or the newer BCD Watch!).

This video is considered the last one in the series of “core concepts.” The next five are more focused on use cases and examples.

Video FiveAdd Scroll Shadows to a Scroll ContainerIn this example, we’re conditionally showing scroll shadows on a scroll container. Chris calls scroll shadows one his favorite CSS-Tricks of all time and we can nail them with scroll animations.

Here is the demo Chris put together a few years ago:

CodePen Embed FallbackThat relies on having a background with multiple CSS gradients that are pinned to the extremes with background-attachment: fixed on a single selector. Let’s modernize this, starting with a different approach using pseudos with sticky positioning:

.container::before,.container::after { content: ""; display: block; position: sticky; left: 0em; right 0em; height: 0.75rem; &::before { top: 0; background: radial-gradient(...); } &::after { bottom: 0; background: radial-gradient(...); }} The shadows fade in and out with a CSS animation:

@keyframes reveal { 0% { opacity: 0; } 100% { opacity: 1; }}.container { overflow:-y auto; scroll-timeline: --scroll-timeline block; /* do we need `block`? */ &::before, &::after { animation: reveal linear both; animation-timeline: --scroll-timeline; }} This example rocks a named timeline, but Bramus notes that an anonymous one would work here as well. Seems like anonymous timelines are somewhat fragile and named timelines are a good defensive strategy.

The next thing we need is to set the animation’s range so that each pseudo scrolls in where needed. Calculating the range from the top is fairly straightforward:

.container::before { animation-range: 1em 2em;} The bottom is a little tricker. It should start when there are 2em of scrolling and then only travel for 1em. We can simply reverse the animation and add a little calculation to set the range based on it’s bottom edge.

.container::after { animation-direction: reverse; animation-range: calc(100% - 2em) calc(100% - 1em);} Still one more thing. We only want the shadows to reveal when we’re in a scroll container. If, for example, the box is taller than the content, there is no scrolling, yet we get both shadows.

This is where the conditional part comes in. We can detect whether an element is scrollable and react to it. Bramus is talking about an animation keyword that’s new to me: detect-scroll.

@keyframes detect-scroll { from, to { --can-scroll: ; /* value is a single space and acts as boolean */ }}.container { animation: detect-scroll; animation-timeline: --scroll-timeline; animation-fill-mode: none;} Gonna have to wrap my head around this… but the general idea is that --can-scroll is a boolean value we can use to set visibility on the pseudos:

.content::before,.content::after { --vis-if-can-scroll: var(--can-scroll) visible; --vis-if-cant-scroll: hidden; visibility: var(--vis-if-can-scroll, var(--vis-if-cant-scroll));} Bramus points to this CSS-Tricks article for more on the conditional toggle stuff.

Video SixAnimate Elements in Different DirectionsThis should be fun! Let’s say we have a set of columns:

```

...
...
...

`` The goal is getting the two outerreverse` columns to scroll in the opposite direction as the inner column scrolls in the other direction. Classic JavaScript territory!

The columns are set up in a grid container. The columns flex in the column direction.

/* run if the browser supports it */@supports (animation-timeline: scroll()) { .column-reverse { transform: translateY(calc(-100% + 100vh)); flex-direction: column-reverse; /* flows in reverse order */ } .columns { overflow-y: clip; /* not a scroll container! */ }} First, the outer columns are pushed all the way up so the bottom edges are aligned with the viewport’s top edge. Then, on scroll, the outer columns slide down until their top edges re aligned with the viewport’s bottom edge.

The CSS animation:

@keyframes adjust-position { from /* the top */ { transform: translateY(calc(-100% + 100vh)); } to /* the bottom */ { transform: translateY(calc(100% - 100vh)); }}.column-reverse { animation: adjust-position linear forwards; animation-timeline: scroll(root block); /* viewport in block direction */} The approach is similar in JavaScript:

const timeline = new ScrollTimeline({ source: document.documentElement,});document.querySelectorAll(".column-reverse").forEach($column) => { $column.animate( { transform: [ "translateY(calc(-100% + 100vh))", "translateY(calc(100% - 100vh))" ] }, { fill: "both", timeline, } );} Video SevenAnimate 3D Models and More on ScrollThis one’s working with a custom element for a 3D model:

<model-viewer alt="Robot" src="robot.glb"></model-viewer> First, the scroll-driven animation. We’re attaching an animation to the component but not defining the keyframes just yet.

@keyframes foo {}model-viewer { animation: foo linear both; animation-timeline: scroll(block root); /* root scroller in block direction */} There’s some JavaScript for the full rotation and orientation:

// Bramus made a little helper for handling the requested animation framesimport { trackProgress } from "https://esm.sh/@bramus/sda-utilities";// Select the componentconst $model = document.QuerySelector("model-viewer");// Animation begins with the first iterationconst animation = $model.getAnimations()[0];// Variable to get the animation's timing infolet progress = animation.effect.getComputedTiming().progress * 1;// If when finished, $progress = 1if (animation.playState === "finished") progress = 1;progress = Math.max(0.0, Math.min(1.0, progress)).toFixed(2);// Convert this to degrees$model.orientation = `0deg 0deg $(progress * -360)deg`; We’re using the effect to get the animation’s progress rather than the current timed spot. The current time value is always measured relative to the full range, so we need the effect to get the progress based on the applied animation.

Video EightScroll Velocity DetectionThe video description is helpful:

Bramus goes full experimental and uses Scroll-Driven Animations to detect the active scroll speed and the directionality of scroll. Detecting this allows you to style an element based on whether the user is scrolling (or not scrolling), the direction they are scrolling in, and the speed they are scrolling with … and this all using only CSS.

First off, this is a hack. What we’re looking at is expermental and not very performant. We want to detect the animations’s velocity and direction. We start with two custom properties.

@keyframes adjust-pos { from { --scroll-position: 0; --scroll-position-delayed: 0; } to { --scroll-position: 1; --scroll-position-delayed: 1; }}:root { animation: adjust-pos linear both; animation-timeline: scroll(root);} Let’s register those custom properties so we can interpolate the values:

@property --scroll-position { syntax: "<number>"; inherits: true; initial-value: 0;}@property --scroll-position-delayed { syntax: "<number>"; inherits: true; initial-value: 0;} As we scroll, those values change. If we add a little delay, then we can stagger things a bit:

:root { animation: adjust-pos linear both; animation-timeline: scroll(root);}body { transition: --scroll-position-delayed 0.15s linear;} The fact that we’re applying this to the body is part of the trick because it depends on the parent-child relationship between html and body. The parent element updates the values immediately while the child lags behind just a tad. The evaluate to the same value, but one is slower to start.

We can use the difference between the two values as they are staggered to get the velocity.

:root { animation: adjust-pos linear both; animation-timeline: scroll(root);}body { transition: --scroll-position-delayed 0.15s linear; --scroll-velocity: calc( var(--scroll-position) - var(--scroll-position-delayed) );} Clever! If --scroll-velocity is equal to 0, then we know that the user is not scrolling because the two values are in sync. A positive number indicates the scroll direction is down, while a negative number indicates scrolling up,.

There’s a little discrepancy when scrolling abruptly changes direction. We can fix this by tighening the transition delay of --scroll-position-delayed but then we’re increasing the velocity. We might need a multiplier to further correct that… that’s why this is a hack. But now we have a way to sniff the scrolling speed and direction!

Here’s the hack using math functions:

body { transition: --scroll-position-delayed 0.15s linear; --scroll-velocity: calc( var(--scroll-position) - var(--scroll-position-delayed) ); --scroll-direction: sign(var(--scroll-velocity)); --scroll-speed: abs(var(--scroll-velocity));} This is a little funny because I’m seeing that Chrome does not yet support sign() or abs(), at least at the time I’m watching this. Gotta enable chrome://flags. There’s a polyfill for the math brought to you by Ana Tudor right here on CSS-Tricks.

So, now we could theoretically do something like skew an element by a certain amount or give it a certain level of background color saturation depending on the scroll speed.

.box { transform: skew(calc(var(--scroll-velocity) * -25deg)); transition: background 0.15s ease; background: hsl( calc(0deg + (145deg * var(--scroll-direction))) 50 % 50% );} We could do all this with style queries should we want to:

@container style(--scroll-direction: 0) { /* idle */ .slider-item { background: crimson; }}@container style(--scroll-direction: 1) { /* scrolling down */ .slider-item { background: forestgreen; }}@container style(--scroll-direction: -1) { /* scrolling down */ .slider-item { background: lightskyblue; }} Custom properties, scroll-driven animations, and style queries — all in one demo! These are wild times for CSS, tell ya what.

Video NineOutroThe tenth and final video! Just a summary of the series, so no new notes here. But here’s a great demo to cap it off.

CodePen Embed FallbackVideo Ten


Unleash the Power of Scroll-Driven Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Change can certainly be scary whenever a beloved, independent software library becomes a part of a larger organization. I’m feeling a bit more excitement than concern this time around, though.

If you haven’t heard, GSAP (GreenSock Animation Platform) is teaming up with the visual website builder, Webflow. This mutually beneficial advancement not only brings GSAP’s powerful animation capabilities to Webflow’s graphical user interface but also provides the GSAP team the resources necessary to take development to the next level.

GSAP has been independent software for nearly 15 years (since the Flash and ActionScript days!) primarily supported by Club GSAP memberships, their paid tiers which offer even more tools and plugins to enhance GSAP further. GSAP is currently used on more than 12 million websites.

I chatted with Cassie Evans — GSAP’s Lead Bestower of Animation Superpowers and CSS-Tricks contributor — who confidently expressed that GSAP will remain available for the wider web.

It’s a big change, but we think it’s going to be a good one – more resources for the core library, more people maintaining the GSAP codebase, money for events and merch and community support, a VISUAL GUI in the pipeline.

The Webflow community has cause for celebration as well, as direct integration with GSAP has been a wishlist item for a while.

The webflow community is so lovely and creative and supportive and friendly too. It’s a good fit.

I’m so happy for Jack, Cassie, and Rodrigo, as well as super excited to see what happens next. If you don’t want to take my word for it, check out what Brody has to say about it.


Combining forces, GSAP & Webflow! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Kate Kaplan hits on something over at Nielsen Norman Group’s blog that’s been bugging me:

The challenge with this icon is sparkle ambiguity: Participants in our recent research study generally agreed that it represented something a little special. But, what was that something? And why was it special? That was less obvious. We encountered widely and wildly varied interpretations.

Man, I hate those sparkles. Correction: I loathe it as an icon but I use the heck out the ✨ emoji. I may even go so far as to say that my favorite thing about Dave Rupert’s introduction to web components is that it is littered with emoji every time the word “superpower” is evoked.

(Correction for the correction: I love everything about Dave’s introduction to web components. Take the full course!)

Sparkles get closer to becoming the unofficial AI icon every time a new one makes its way into some new app’s UI — the same way some menus became hamburgers and others became kebabs.

It’s ambiguous, right? I will say that I was stoked to see Notion roll out a fresh new icon for their AI feature just this week:

A face is interesting! I find human heads less compelling, especially when they’re realistic. Same deal with robot heads, which is another theme you can spot in the wild. But a face, particularly one that’s on the whimsical side as a line drawing, looks like it could work in this context that’s specific to Notion. I imagine another company or app having a tough time pulling off the same icon because this one is so closely tied to Notion’s overall branding:

See how nice it is next to the rest of Notion’s icons?I also like how Notion has several versions of the icon for use in different situations.

And, yes, it animates as well:

It’s the button that persists in the bottom-right corner.I’m not saying Notion’s landed on a silver bullet. What I am saying is that they’re doing a great job transitioning from an ambiguous one to a more meaningful one, something that Kate articulates extremely well:

[S]parkles are used frequently to represent not only AI features and capabilities but also completely unrelated features and content, such as visual effects, deals or rewards, personalized ads, and new content.

I get worked up about this because I own a pessimistic and assumptive view that the proliferation of sparkles icons reeks of marketing. There’s no way for me to know this, of course, but I’ll unabashedly don my tinfoil hat for this one.

Anyway, Kate’s article is a much more thorough investigation that’s worth the deep dive — it’s a selection pulled from an entire book on the topic of successful icon design. I’ll leave you with a sobering quote:

Finally, I also predict that the icon’s association with AI-driven features will get stronger in the immediate future. So, for the time being, using it to indicate AI-driven features (or even simply new features) may be useful. Over time, as AI-driven features become more common or even expected across interfaces, there will be less of a need to call them out. It won’t matter that the features are AI-driven; it will matter only that they are present and meet user needs.


The Proliferation and Problem of the ✨ Sparkles ✨ Icon originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Not long ago, if we wanted a tooltip or popover positioned on top of another element, we would have to set our tooltip’s position to something other than static and use its inset/transform properties to place it exactly where we want. This works, but the element’s position is susceptible to user scrolls, zooming, or animations since the tooltip could overflow off of the screen or wind up in an awkward position. The only way to solve this was using JavaScript to check whenever the tooltip goes out of bounds so we can correct it… again in JavaScript.

CSS Anchor Positioning gives us a simple interface to attach elements next to others just by saying which sides to connect — directly in CSS. It also lets us set a fallback position so that we can avoid the overflow issues we just described. For example, we might set a tooltip element above its anchor but allow it to fold underneath the anchor when it runs out of room to show it above.

Anchor positioning is different from a lot of other features as far as how quickly it’s gained browser support: its first draft was published on June 2023 and, just a year later, it was released on Chrome 125. To put it into perspective, the first draft specification for CSS variables was published in 2012, but it took four years for them to gain wide browser support.

So, let’s dig in and learn about things like attaching target elements to anchor elements and positioning and sizing them.

Quick reference /* Define an anchor element */.anchor { anchor-name: --my-anchor;}

/* Anchor a target element */.target { position: absolute; position-anchor: --my-anchor;}

/* Position a target element */.target { position-area: start end;} Table of contents 1. Basics and terminology 2. Attaching targets to anchors 3. Positioning targets 4. Setting fallback positions 5. Custom position and size fallbacks 6. Multiple anchors 7. Accessibility 8. Browser support 9. Spec changes 10. Known bugs 11. Almanac references 12. Further reading Basics and terminology At its most basic, CSS Anchor Positioning introduces a completely new way of placing elements on the page relative to one another. To make our lives easier, we’re going to use specific names to clarify which element is connecting to which:

  • Anchor: This is the element used as a reference for positioning other elements, hence the anchorname.
  • Target: This is an absolutely positioned element placed relative to one or more anchors. The target is the name we will use from now on, but you will often find it as just an “absolutely positioned element” in the spec.

For the following code examples and demos, you can think of these as just two <div> elements next to one another.

```

anchor
target

`` CSS Anchor Positioning is all about elements with absolute positioning (i.e.,display: absolute`), so there are also some concepts we have to review before diving in.

  • Containing Block: This is the box that contains the elements. For an absolute element, the containing block is the viewport the closest ancestor with a position other than static or certain values in properties like contain or filter.
  • Inset-Modified Containing Block (IMCB): For an absolute element, inset properties (top, right, bottom, left, etc.) reduce the size of the containing block into which it is sized and positioned, resulting in a new box called the inset-modified containing block, or IMCB for short. This is a vital concept to know since properties we’re covering in this guide — like position-area and position-try-order — rely on this concept. Attaching targets to anchors We’ll first look at the two properties that establish anchor positioning. The first, anchor-name, establishes the anchor element, while the second, position-anchor, attaches a target element to the anchor element.

anchor-nameA normal element isn’t an anchor by default — we have to explicitly make an element an anchor. The most common way is by giving it a name, which we can do with the anchor-name property.

anchor-name: none | <dashed-ident># The name must be a <dashed-ident>, that is, a custom name prefixed with two dashes (--), like --my-anchor or --MyAnchor.

.anchor { anchor-name: --my-anchor;} This gives us an anchor element. All it needs is something anchored to it. That’s what we call the “target” element which is set with the position-anchor property.

position-anchorThe target element is an element with an absolute position linked to an anchor element matching what’s declared on the anchor-name property. This attaches the target element to the anchor element.

position-anchor: auto | <anchor-element> It takes a valid <anchor-element>. So, if we establish another element as the “anchor” we can set the target with the position-anchor property:

.target { position: absolute; position-anchor: --my-anchor;} Normally, if a valid anchor element isn’t found, then other anchor properties and functions will be ignored.

Positioning targets Now that we know how to establish an anchor-target relationship, we can work on positioning the target element in relation to the anchor element. The following two properties are used to set which side of the anchor element the target is positioned on (position-area) and conditions for hiding the target element when it runs out of room (position-visibility).

position-areaThe next step is positioning our target relative to its anchor. The easiest way is to use the position-area property, which creates an imaginary 3×3 grid around the anchor element and lets us place the target in one or more regions of the grid.

position-area: auto | <position-area> It works by setting the row and column of the grid using logical values like start and end (dependent on the writing mode); physical values like top, left, right, bottom and the center shared value, then it will shrink the target’s IMCB into the region of the grid we chose.

.target { position-area: top right; /* or */ position-area: start end;} Logical values refer to the containing block’s writing mode, but if we want to position our target relative to its writing mode we would prefix it with the self value.

.target { position-area: self-start self-end;} There is also the center value that can be used in every axis.

.target { position-area: center right; /* or */ position-area: start center;} To place a target across two adjacent grid regions, we can use the prefix span- on any value (that isn’t center) a row or column at a time.

.target { position-area: span-top left; /* or */ position-area: span-start start;} Finally, we can span a target across three adjacent grid regions using the span-all value.

.target { position-area: bottom span-all; /* or */ position-area: end span-all;} You may have noticed that the position-area property doesn’t have a strict order for physical values; writing position-area: top left is the same as position-area: left top, but the order is important for logical value since position-area: start end is completely opposite to position-area: end start.

We can make logical values interchangeable by prefixing them with the desired axis using y-, x-, inline- or block-.

.target { position-area: inline-end block-start; /* or */ position-area: y-start x-end;} CodePen Embed FallbackCodePen Embed Fallbackposition-visibilityIt provides certain conditions to hide the target from the viewport.

position-visibility: always | anchors-visible | no-overflow * always: The target is always displayed without regard for its anchors or its overflowing status. * no-overflow: If even after applying the position fallbacks, the target element is still overflowing its containing block, then it is strongly hidden. * anchors-visible: If the anchor (not the target) has completely overflowed its containing block or is completely covered by other elements, then the target is strongly hidden.

position-visibility: always | anchors-visible | no-overflow CodePen Embed Fallback Setting fallback positions Once the target element is positioned against its anchor, we can give the target additional instructions that tell it what to do if it runs out of space. We’ve already looked at the position-visibility property as one way of doing that — we simply tell the element to hide. The following two properties, however, give us more control to re-position the target by trying other sides of the anchor (position-try-fallbacks) and the order in which it attempts to re-position itself (position-try-order).

The two properties can be declared together with the position-try shorthand property — we’ll touch on that after we look at the two constituent properties.

position-try-fallbacksThis property accepts a list of comma-separated position fallbacks that are tried whenever the target overflows out of space in its containing block. The property attempts to reposition itself using each fallback value until it finds a fit or runs out of options.

position-try-fallbacks: none | [ [<dashed-ident> || <try-tactic>] | <'inset-area'> ]# * none: Leaves the target’s position options list empty. * <dashed-ident>: Adds to the options list a custom @position-try fallback with the given name. If there isn’t a matching @position-try, the value is ignored. * <try-tactic>: Creates an option list by flipping the target’s current position on one of three axes, each defined by a distinct keyword. They can also be combined to add up their effects. + The flip-block keyword swaps the values in the block axis. + The flip-inline keyword swaps the values in the inline axis. + The flip-start keyword swaps the values diagonally. * <dashed-ident> || <try-tactic>: Combines a custom @try-option and a <try-tactic> to create a single-position fallback. The <try-tactic> keywords can also be combined to sum up their effects. * <"position-area"> Uses the position-area syntax to move the anchor to a new position.

.target { position-try-fallbacks: --my-custom-position, --my-custom-position flip-inline, bottom left;} position-try-orderThis property chooses a new position from the fallback values defined in the position-try-fallbacks property based on which position gives the target the most space. The rest of the options are reordered with the largest available space coming first.

position-try-order: normal | most-width | most-height | most-block-size | most-inline-size What exactly does “more space” mean? For each position fallback, it finds the IMCB size for the target. Then it chooses the value that gives the IMCB the widest or tallest size, depending on which option is selected:

  • most-width
  • most-height
  • most-block-size
  • most-inline-size

.target { position-try-fallbacks: --custom-position, flip-start; position-try-order: most-width;} position-tryThis is a shorthand property that combines the position-try-fallbacks and position-try-order properties into a single declaration. It accepts first the order and then the list of possible position fallbacks.

position-try: < "position-try-order" >? < "position-try-fallbacks" >; So, we can combine both properties into a single style rule:

.target { position-try: most-width --my-custom-position, flip-inline, bottom left;} Custom position and size fallbacks @position-tryThis at-rule defines a custom position fallback for the position-try-fallbacks property.

@position-try <dashed-ident> { <declaration-list>} It takes various properties for changing a target element’s position and size and grouping them as a new position fallback for the element to try.

Imagine a scenario where you’ve established an anchor-target relationship. You want to position the target element against the anchor’s top-right edge, which is easy enough using the position-area property we saw earlier:

.target { position: absolute; position-area: top right; width: 100px;} See how the .target is sized at 100px? Maybe it runs out of room on some screens and is no longer able to be displayed at anchor’s the top-right edge. We can supply the .target with the fallbacks we looked at earlier so that it attempts to re-position itself on an edge with more space:

.target { position: absolute; position-area: top right; position-try-fallbacks: top left; position-try-order: most-width; width: 100px;} And since we’re being good CSSer’s who strive for clean code, we may as well combine those two properties with the position-try shorthand property:

.target { position: absolute; position-area: top right; position-try: most-width, flip-inline, bottom left; width: 100px;} So far, so good. We have an anchored target element that starts at the top-right corner of the anchor at 100px. If it runs out of space there, it will look at the position-try property and decide whether to reposition the target to the anchor’s top-left corner (declared as flip-inline) or the anchor’s bottom-left corner — whichever offers the most width.

But what if we want to simulataneously re-size the target element when it is re-positioned? Maybe the target is simply too dang big to display at 100px at either fallback position and we need it to be 50px instead. We can use the @position-try to do exactly that:

@position-try --my-custom-position { position-area: top left; width: 50px;} With that done, we now have a custom property called --my-custom-position that we can use on the position-try shorthand property. In this case, @position-try can replace the flip-inline value since it is the equivalent of top left:

@position-try --my-custom-position { position-area: top left; width: 50px;}.target { position: absolute; position-area: top right; position-try: most-width, --my-custom-position, bottom left; width: 100px;} This way, the .target element’s width is re-sized from 100px to 50px when it attempts to re-position itself to the anchor’s top-right edge. That’s a nice bit of flexibility that gives us a better chance to make things fit together in any layout.

Anchor functions anchor()You might think of the CSS anchor() function as a shortcut for attaching a target element to an anchor element — specify the anchor, the side we want to attach to, and how large we want the target to be in one fell swoop. But, as we’ll see, the function also opens up the possibility of attaching one target element to multiple anchor elements.

This is the function’s formal syntax, which takes up to three arguments:

anchor( <anchor-element>? && <anchor-side>, <length-percentage>? ) So, we’re identifying an anchor element, saying which side we want the target to be positioned on, and how big we want it to be. It’s worth noting that anchor() can only be declared on inset-related properties (e.g. top, left, inset-block-end, etc.)

.target { top: anchor(--my-anchor bottom); left: anchor(--my-anchor end, 50%);} Let’s break down the function’s arguments.

This argument specifies which anchor element we want to attach the target to. We can supply it with either the anchor’s name (see “Attaching targets to anchors”).

We also have the choice of not supplying an anchor at all. In that case, the target element uses an implicit anchor element defined in position-anchor. If there isn’t an implicit anchor, the function resolves to its fallback. Otherwise, it is invalid and ignored.

This argument sets which side of the anchor we want to position the target element to, e.g. the anchor’s top, left, bottom, right, etc.

But we have more options than that, including logical side keywords (inside, outside), logical direction arguments relative to the user’s writing mode (start, end, self-start, self-end) and, of course, center.

  • <anchor-side>: Resolves to the <length> of the corresponding side of the anchor element. It has physical arguments (top, left, bottom right), logical side arguments (inside, outside), logical direction arguments relative to the user’s writing mode (start, end, self-start, self-end) and the center argument.
  • <percentage>: Refers to the position between the start (0%) and end (100%). Values below 0% and above 100% are allowed. This argument is totally optional, so you can leave it out if you’d like. Otherwise, use it as a way of re-sizing the target elemenrt whenever it doesn’t have a valid anchor or position. It positions the target to a fixed <length> or <percentage> relative to its containing block.

Let’s look at examples using different types of arguments because they all do something a little different.

Using physical argumentsPhysical arguments (top, right, bottom, left) can be used to position the target regardless of the user’s writing mode. For example, we can position the right and bottom inset properties of the target at the anchor(top) and anchor(left) sides of the anchor, effectively positioning the target at the anchor’s top-left corner:

.target { bottom: anchor(top); right: anchor(left);} Using logical side keywordsLogical side arguments (i.e., inside, outside), are dependent on the inset property they are in. The inside argument will choose the same side as its inset property, while the outside argument will choose the opposite. For example:

.target { left: anchor(outside); /* is the same as */ left: anchor(right); top: anchor(inside); /* is the same as */ top: anchor(top);} Using logical directionsLogical direction arguments are dependent on two factors:

  1. The user’s writing mode: they can follow the writing mode of the containing block (start, end) or the target’s own writing mode (self-start, self-end).
  2. The inset property they are used in: they will choose the same axis of their inset property.

So for example, using physical inset properties in a left-to-right horizontal writing would look like this:

.target { left: anchor(start); /* is the same as */ left: anchor(left); top: anchor(end); /* is the same as */ top: anchor(bottom);} In a right-to-left writing mode, we’d do this:

.target { left: anchor(start); /* is the same as */ left: anchor(right); top: anchor(end); /* is the same as */ top: anchor(bottom);} That can quickly get confusing, so we should also use logical arguments with logical inset properties so the writing mode is respected in the first place:

.target { inset-inline-start: anchor(end); inset-block-start: anchor(end);} Using percentage valuesPercentages can be used to position the target from any point between the start (0%) and end (100% ) sides. Since percentages are relative to the user writing mode, is preferable to use them with logical inset properties.

.target { inset-inline-start: anchor(100%); /* is the same as */ inset-inline-start: anchor(end); inset-block-end: anchor(0%); /* is the same as */ inset-block-end: anchor(start);} Values smaller than 0% and bigger than 100% are accepted, so -100% will move the target towards the start and 200% towards the end.

.target { inset-inline-start: anchor(200%); inset-block-end: anchor(-100%);} Using the center keywordThe center argument is equivalent to 50%. You could say that it’s “immune” to direction, so there is no problem if we use it with physical or logical inset properties.

.target { position: absolute; position-anchor: --my-anchor; left: anchor(center); bottom: anchor(top);} anchor-size()The anchor-size() function is unique in that it sizes the target element relative to the size of the anchor element. This can be super useful for ensuring a target scales in size with its anchor, particularly in responsive designs where elements tend to get shifted, re-sized, or obscured from overflowing a container.

The function takes an anchor’s side and resolves to its <length>, essentially returning the anchor’s width, height, inline-size or block-size.

anchor-size( [ <anchor-element> || <anchor-size> ]? , <length-percentage>? ) Here are the arguments that can be used in the anchor-size() function:

  • <anchor-size>: Refers to the side of the anchor element.
  • <length-percentage>: This optional argument can be used as a fallback whenever the target doesn’t have a valid anchor or size. It returns a fixed <length> or <percentage> relative to its containing block.

And we can declare the function on the target element’s width and height properties to size it with the anchor — or both at the same time!

.target { width: anchor-size(width, 20%); /* uses default anchor */` height: anchor-size(--other-anchor inline-size, 100px);} Multiple anchors We learned about the anchor() function in the last section. One of the function’s quirks is that we can only declare it on inset-based properties, and all of the examples we saw show that. That might sound like a constraint of working with the function, but it’s actually what gives anchor() a superpower that anchor positioning properties don’t: we can declare it on more than one inset-based property at a time. As a result, we can set the function multiple anchors on the same target element!

Here’s one of the first examples of the anchor() function we looked at in the last section:

.target { top: anchor(--my-anchor bottom); left: anchor(--my-anchor end, 50%);} We’re declaring the same anchor element named --my-anchor on both the top and left inset properties. That doesn’t have to be the case. Instead, we can attach the target element to multiple anchor elements.

.anchor-1 { anchor-name: --anchor-1; }.anchor-2 { anchor-name: --anchor-2; }.anchor-3 { anchor-name: --anchor-3; }.anchor-4 { anchor-name: --anchor-4; }.target { position: absolute; inset-block-start: anchor(--anchor-1); inset-inline-end: anchor(--anchor-2); inset-block-end: anchor(--anchor-3); inset-inline-start: anchor(--anchor-4);} Or, perhaps more succintly:

.anchor-1 { anchor-name: --anchor-1; }.anchor-2 { anchor-name: --anchor-2; }.anchor-3 { anchor-name: --anchor-3; }.anchor-4 { anchor-name: --anchor-4; }.target { position: absolute; inset: anchor(--anchor-1) anchor(--anchor-2) anchor(--anchor-3) anchor(--anchor-4);} The following demo shows a target element attached to two <textarea> elements that are registered anchors. A <textarea> allows you to click and drag it to change its dimensions. The two of them are absolutely positioned in opposite corners of the page. If we attach the target to each anchor, we can create an effect where resizing the anchors stretches the target all over the place almost like a tug-o-war between the two anchors.

CodePen Embed FallbackThe demo is only supported in Chrome at the time we’re writing this guide, so let’s drop in a video so you can see how it works.

Accessibility The most straightforward use case for anchor positioning is for making tooltips, info boxes, and popovers, but it can also be used for decorative stuff. That means anchor positioning doesn’t have to establish a semantic relationship between the anchor and target elements. You can probably spot the issue right away: non-visual devices, like screen readers, are left in the dark about how to interpret two seemingly unrelated elements.

As an example, let’s say we have an element called .tooltip that we’ve set up as a target element anchored to another element called .anchor.

```

anchor
toolip

```

.anchor { anchor-name: --my-anchor;}.toolip { position: absolute; position-anchor: --my-anchor; position-area: top;} We need to set up a connection between the two elements in the DOM so that they share a context that assistive technologies can interpret and understand. The general rule of thumb for using ARIA attributes to describe elements is generally: don’t do it. Or at least avoid doing it unless you have no other semantic way of doing it.

This is one of those cases where it makes sense to reach for ARIA atributes. Before we do anything else, a screen reader currently sees the two elements next to one another without any remarking relationship. That’s a bummer for accessibility, but we can easily fix it using the corresponding ARIA attribute:

```

anchor

``` And now they are both visually and semantically linked together! If you’re new to ARIA attributes, you ought to check out Adam Silver’s “Why, How, and When to Use Semantic HTML and ARIA” for a great introduction.

Browser support This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

| Chrome | Firefox | IE | Edge | Safari | | --- | --- | --- | --- | --- | | 125 | No | No | 125 | No |

Mobile / Tablet

| Android Chrome | Android Firefox | Android | iOS Safari | | --- | --- | --- | --- | | 129 | No | 129 | No |

Spec changes CSS Anchor Positioning has undergone several changes since it was introduced as an Editor’s Draft. The Chrome browser team was quick to hop on board and implement anchor positioning even though the feature was still being defined. That’s caused confusion because Chromium-based browsers implemented some pieces of anchor positioning while the specification was being actively edited.

We are going to outline specific cases for you where browsers had to update their implementations in response to spec changes. It’s a bit confusing, but as of Chrome 129+, this is the stuff that was shipped but changed:

position-areaThe inset-area property was renamed to position-area (#10209), but it will be supported until Chrome 131.

.target { /* from */ inset-area: top right; /* to */ position-area: top right;} position-try-fallbacksThe position-try-options was renamed to position-try-fallbacks (#10395).

.target { /* from */ position-try-options: flip-block, --smaller-target; /* to */ position-try-fallbacks: flip-block, --smaller-target;} inset-area()The inset-area() wrapper function doesn’t exist anymore for the position-try-fallbacks (#10320), you can just write the values without the wrapper:

.target { /* from */ position-try-options: inset-area(top left); /* to */ position-try-fallbacks: top left;} anchor(center)In the beginning, if we wanted to center a target from the center, we would have to write this convoluted syntax:

.target { --center: anchor(--x 50%); --half-distance: min(abs(0% - var(--center)), abs(100% - var(--center))); left: calc(var(--center) - var(--half-distance)); right: calc(var(--center) - var(--half-distance));} The CWSSG working group resolved (#8979) to add the anchor(center) argument to prevent us from having to do all that mental juggling:

.target { left: anchor(center);} Known bugs Yes, there are some bugs with CSS Anchor Positioning, at least at the time this guide is being written. For example, the specification says that if an element doesn’t have a default anchor element, then the position-area does nothing. This is a known issue (#10500), but it’s still possible to replicate.

So, the following code…

.container { position: relative;}.element { position: absolute; position-area: center; margin: auto;} …will center the .element inside its container, at least in Chrome:

CodePen Embed FallbackCredit to Afif13 for that great demo!

Another example involves the position-visibility property. If your anchor element is out of sight or off-screen, you typically want the target element to be hidden as well. The specification says that property’s the default value is anchors-visible, but browsers default to always instead.

The current implemenation in Chrome isn’t reflecting the spec; it indeed is using always as the initial value. But the spec is intentional: if your anchor is off-screen or otherwise scrolled off, you usually want it to hide. (#10425)

Almanac references Anchor position properties Almanac on Sep 17, 2024 anchor-name .anchor { anchor-name: --my-anchor; } anchor positioning Geoff Graham Almanac on Sep 12, 2024 position-anchor .target { position-anchor: --my-anchor; } anchor positioning Juan Diego Rodríguez Almanac on Sep 8, 2024 position-area .target { position-area: bottom end; } anchor positioning Juan Diego Rodríguez Almanac on Sep 14, 2024 position-try-fallbacks .target { position-try-fallbacks: flip-inline, bottom left; } anchor positioning Juan Diego Rodríguez Almanac on Sep 8, 2024 position-try-order .element { position-try-order: most-width; } anchor positioning Juan Diego Rodríguez Almanac on Sep 8, 2024 position-visibility .target { position-visibility: no-overflow; } anchor positioning Juan Diego Rodríguez Anchor position functions Almanac on Sep 14, 2024 anchor() .target { top: anchor(--my-anchor bottom); } anchor positioning Juan Diego Rodríguez Almanac on Sep 18, 2024 anchor-size() .target { width: anchor-size(width); } anchor positioning Juan Diego Rodríguez Anchor position at-rules Almanac on Sep 28, 2024 @position-try @position-try --my-position { position-area: top left; } anchor positioning Juan Diego Rodríguez Further reading * “CSS Anchor Positioning” (CSSWG) * “Using CSS anchor positioning” (MDN) * “Introducing the CSS anchor positioning API” (Una Kravets)


CSS Anchor Positioning Guide originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

An approach for creating masonry layouts in vanilla CSS is one of those “holy grail” aspirations. I actually tend to plop masonry and the classic “Holy Grail” layout in the same general era of web design. They’re different types of layouts, of course, but the Holy Grail was a done deal when we got CSS Grid.

That leaves masonry as perhaps the last standing layout from the CSS 3 era that is left without a baked-in solution. I might argue that masonry is no longer en vogue so to speak, but there clearly are use cases for packing items with varying sizes into columns based on available space. And masonry is still very much in the wild.

Steam is picking up on a formal solution. We even have a CSSWG draft specification for it. But notice how the draft breaks things out.

Grid-integrated syntax? Grid-independent syntax? We’ve done gone and multiplied CSS!

That’s the context for this batch of notes. There are two competing proposals for CSS masonry at the time of writing and many opinions are flying around advocating one or the other. I have personal thoughts on it, but that’s not important. I’ll be happy with whatever the consensus happens to be. Both proposals have merits and come with potential challenges — it’s a matter of what you prioritize which, in this case, I believe is a choice between leveraging existing CSS layout features and the ergonomics of a fresh new approach.

But let’s get to some notes from discussions that are already happening to help get a clearer picture of things!

What is masonry layout?Think of it like erecting a wall of stones or bricks.

The sizes of the bricks and stones don’t matter — the column (or less commonly a row) is the boss of sizing things. Pack as many stones or bricks in the nearest column and then those adapt to the column’s width. Or more concisely, we’re laying out unevenly sized items in a column such that there aren’t uneven gaps between them.

Examples, please?Here’s perhaps the most widely seen example in a CodePen, courtesy of Dave DeSandro, using his Masonry.js tool:

CodePen Embed FallbackI use this example because, if I remember correctly, Masonry.js was what stoked the masonry trend in, like 2010 or something. Dave implemented it on Beyoncé’s website which certainly gave masonry a highly visible profile. Sometimes you might hear masonry called a “Pinterest-style” layout because, well, that’s been the site’s signature design — perhaps even its brand — since day one.

Here’s a faux example Jhey put together using flexbox:

CodePen Embed FallbackChris also rounded up a bunch of other workarounds in 2019 that get us somewhat there, under ideal conditions. But none of these are based on standardized approaches or features. I mean, columns and flexbox are specced but weren’t designed with masonry in mind. But with masonry having a long track record of being used, it most certainly deserves a place in the CSS specs.

There are two competing proposalsThis isn’t exactly news. In fact, we can get earlier whiffs of this looking back to 2020. Rachel Andrew introduced the concept of making masonry a sub-feature of grid in a Smashing Magazine article.

Let’s fast-forward to 2022. We had an editor’s draft for CSS Masonry baked into the CSS Grid Layout Module 3 specification. Jenn Simmons motioned for the CSSWG to move it forward to be a first public working draft. Five days later, Chromium engineer Ian Kilpatrick raised two concerns about moving things forward as part of the CSS Grid Layout module, the first being related to sizing column tracks and grid’s layout algorithm:

Grid works by placing everything in the grid ahead of time, then sizing the rows/columns to fit the items. Masonry fundamentally doesn’t work this way as you need to size the rows/columns ahead of time – then place items within those rows/columns.

As a result the way the current specification re-uses the grid sizing logic leads to poor results when intrinsically sizing tracks, and if the grid is intrinsically-sized itself (e.g. if its within a grid/flex/table, etc).

Good point! Grid places grid items in advance ahead of sizing them to fit into the available space. Again, it’s the column’s size that bosses things around in masonry. It logically follows that we would need to declare masonry and configure the column track sizes in advance to place things according to space. The other concern concerns accessibility as far as visual and reading order.

That stopped Jenn’s motion for first public working draft status dead in its tracks in early 2023. If we fast-forward to July of this year, we get Ian’s points for an alternative path forward for masonry. That garnered support from all sorts of CSS heavyweights, including Rachel Andrew who authored the CSS Grid specification.

And, just a mere three weeks ago from today, fantasai shared a draft for an alternate proposal put together with Tab Atkins. This proposal, you’ll see, is specific to masonry as its own module.

And thus we have two competing proposals to solve masonry in CSS.

The case for merging masonry and gridRounding up comments from GitHub tickets and blog posts…

Flexbox is really designed for putting things into a line and distributing spare space. So that initial behaviour of putting all your things in a row is a great starting point for whatever you might want to do. It may be all you need to do. It’s not difficult as a teacher to then unpack how to add space inside or outside items, align them, or make it a column rather than a row. Step by step, from the defaults.

I want to be able to take the same approach with display: masonry.

[…]

We can’t do that as easily with grid, because of the pre-existing initial values. The good defaults for grid don’t work as well for masonry. Currently you’d need to:

  1. Add display: grid, to get a single column grid layout.
  2. Add grid-template-columns: <track-listing>, and at the moment there’s no way to auto-fill auto sized tracks so you’ll need to decide on how many. Using grid-template-columns: repeat(3, auto), for example.
  3. Add grid-template-rows: masonry.
  4. Want to define rows instead? Switch the masonry value to apply to grid-template-columns and now define your rows. Once again, you have to explicitly define rows.

Rachel Andrew, “Masonry and good defaults”

For what it’s worth, Rachel has been waving this flag since at least 2020. The ergonomics of display: masonry with default configurations that solve baseline functionality are clear and compelling. The default behavior oughta match the feature’s purpose and grid just ain’t a great set of default configurations to jump into a masonry layout. Rachel’s point is that teaching and learning grid to get to understand masonry behavior unnecessarily lumps two different formatting contexts into one, which is a certain path to confusion. I find it tough to refute this, as I also come at this from a teaching perspective. Seen this way, we might say that merging features is another lost entry point into front-end development.

In recent years, the two primary methods we’ve used to pull off masonry layouts are:

  • Flexbox for consistent row sizes. We adjust the flex-basis based on the item’s expected percentage of the total row width.
  • Grid for consistent column sizes. We set the row span based on the expected aspect ratio of the content, either server-side for imagery or client-side for dynamic content.

What I’ve personally observed is:

  • Neither feels more intuitive than the other as a starting point for masonry. So it feels a little itchy to single out Grid as a foundation.
  • While there is friction when teaching folks when to use a Flexbox versus a Grid, it’s a much bigger leap for contributors to wrap their heads around properties that significantly change behavior (such as flex-wrap or grid-auto-flow: dense).

Tyler Sticka, commenting on GitHub Issue #9041

It’s true! If I had to single out either flexbox or grid as the starting poit for masonry (and I doubt I would either way), I might lean flexbox purely for the default behavior of aligning flexible items in a column.

The syntax and semantics of the CSS that will drive masonry layout is a concern that is separate from the actual layout mechanics itself, which internally in implementation by user agents can still re-use parts of the existing mechanics for grids, including subgrids. For cases where masonry is nested inside grid, or grid inside masonry, the relationship between the two can be made explicit.

@jgotten, commenting on GitHub Issue #9041

Rachel again, this time speaking on behalf of the Chrome team:

There are two related reasons why we feel that masonry is better defined outside of grid layout—the potential of layout performance issues, and the fact that both masonry and grid have features that make sense in one layout method but not the other.

The case for keeping masonry separate from grid

One of the key benefits of integrating masonry into the grid layout (as in CASE 2) is the ability to leverage existing grid features, such as subgrids. Subgrids allow for cohesive designs among child elements within a grid, something highly desirable in many masonry layouts as well. Additionally, I believe that future enhancements to the grid layout will also be beneficial for masonry, making their integration even more valuable. By treating masonry as an extension of the grid layout, developers would be able to start using it immediately, without needing to learn a completely new system.

Kokomi, commenting on GitHub Issue #9041

It really would be a shame if keeping masonry separate from grid prevents masonry from being as powerful as it could be with access to grid’s feature set:

I think the arguments for a separate display: masonry focus too much on the potential simplicity at the expense of functionality. Excluding Grid’s powerful features would hinder developers who want or need more than basic layouts. Plus, introducing another display type could lead to confusion and fragmentation in the layout ecosystem.

Angel Ponce, commenting on GitHub Issue #9041

Rachel counters that, though.

I want express my strong support for adding masonry to display:grid. The fact that it gracefully degrades to a traditional grid is a huge benefit IMO. But also, masonry layout is already possible (with some constraints) in Grid layout today!

Naman Goel, Angel Ponce, commenting on GitHub Issue #9041

Chris mildly voiced interest in merging the two in 2020 before the debate got larger and more heated. Not exactly a ringing endorsement, but rather an acknowledgment that it could make sense:

I like the grid-template-rows: masonry; syntax because I think it clearly communicates: “You aren’t setting these rows. In fact, there aren’t even really rows at all anymore, we’ll take care of that.” Which I guess means there are no rows to inherit in subgrid, which also makes sense.

Where we at?Collecting feedback. Rachel, Ian, and Tab published a joint call for folks like you and me to add our thoughts to the bag. That was eight days ago as of this writing. Not only is it a call to action, but it’s also an excellent overview of the two competing ideas and considerations for each one. You’ll want to add your feedback to GitHub Issue #9041.


CSS Masonry & CSS Grid originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The creator of CSS has said he originally envisaged CSS as the main web technology to control behavior on web pages, with scripting as a fallback when things weren’t possible declaratively in CSS. The rationale for a CSS-first approach was that “scripting is programming and programming is hard.” Since introducing the :hover pseudo-class, CSS has been standardizing patterns developers create in JavaScript and “harvesting” them into CSS standards. When you think about it like that, it’s almost as if JavaScript is the hack and CSS is the official way.

We can, therefore, feel less dirty implementing script-like behavior with CSS, and we shouldn’t be surprised that something like the new scroll-timeline feature has appeared with pretty good browser support. Too many developers implemented clever parallax scrolling websites, which has summoned the CSS feature genie we cannot put back in its bottle. If you don’t want janky main-thread animations for your next parallax-scrolling website, you must now come to the dark side of hacking CSS. Just kidding, there is also a new JavaScript API for scroll-linked animations if imperative programming better fits your use case.

Migrating a JavaScript sample to CSSIt was satisfyingly simple to fork Chris Coyier’s pre-scroll-timeline example of a scroll-linked animation by replacing the CSS Chris was using to control the animations with just one line of CSS and completely deleting the JavaScript!

body, .progress, .cube { animation-timeline: scroll();} CodePen Embed FallbackUsing the scroll() function without parameters sets up an “anonymous scroll progress timeline” meaning the browser will base the animation on the nearest ancestor that can scroll vertically if our writing mode is English. Unfortunately, it seems we can only choose to animate based on scrolling along the x or y-axis of a particular element but not both, which would be useful. Being a function, we can pass parameters to scroll(), which provides more control over how we want scrolling to run our animation.

Experimenting with multiple dimensionsEven better is the scroll-scope property. Applying that to a container element means we can animate properties on any chosen ancestor element based on any scrollable element that has the same assigned scope. That got me thinking… Since CSS Houdini lets us register animation-friendly, inheritable properties in CSS, we can combine animations on the same element based on multiple scrollable areas on the page. That opens the door for interesting instructional design possibilities such as my experiment below.

CodePen Embed FallbackScrolling the horizontal narrative on the light green card rotates the 3D NES console horizontally and scrolling the vertical narrative on the dark green card rotates the NES console vertically. In my previous article, I noted that my past CSS hacks have always boiled down to hiding and showing finite possibilities using CSS. What interests me about this scroll-based experiment is the combinatorial explosion of combined vertical and horizontal rotations. Animation timelines provide an interactivity in pure CSS that hasn’t been possible in the past.

The implementation details are less important than the timeline-scope usage and the custom properties. We register two custom angle properties:

@property --my-y-angle { syntax: "<angle>"; inherits: true; initial-value: 0deg;}@property --my-x-angle { syntax: "<angle>"; inherits: true; initial-value: -35deg;} Then, we “borrow” the NES 3D model from the samples in Julian Garner’s amazing CSS 3D modeling app. We update the .scene class for the 3D to base the rotation on our new variables like this:

.scene { transform: rotateY(var(--my-y-angle)) rotateX(var(--my-x-angle));} Next, we give the <body> element a timeline-scope with two custom-named scopes.

body { timeline-scope: --myScroller,--myScroller2; } I haven’t seen anything officially documented about passing in multiple scopes, but it does work in Google Chrome and Edge. If it’s not a formally supported feature, I hope it will become part of the standard because it is ridiculously handy.

Next, we define the named timelines for the two scrollable cards and the axes we want to trigger our animations.

.card:first-child { scroll-timeline-axis: x; scroll-timeline-name: --myScroller;}.card:nth-child(2) { scroll-timeline-axis: y; scroll-timeline-name: --myScroller2;} And add the animations to the scene:

.scene { animation: rotateHorizontal,rotateVertical; animation-timeline: --myScroller,--myScroller2;}@keyframes rotateHorizontal { to { --my-y-angle: 360deg; }}@keyframes rotateVertical { to { --my-x-angle: 360deg; }} Since the 3D model inherits the x and y angles from the document body, scrolling the cards now rotates the model in combinations of vertical and horizontal angle changes.

User-controlled animations beyond scrollbarsWhen you think about it, this behavior isn’t just useful for scroll-driven animations. In the above experiment, we are using the scrollable areas more like sliders that control the properties of our 3D model. After getting it working, I went for a walk and was daydreaming about how cool it would be if actual range inputs could control animation timelines. Then I found out they can! At least in Chrome. Pure CSS CMS anyone?

While we’re commandeering 3D models from Julian Garner, let’s see if we can use range inputs to control his X-wing model.

CodePen Embed FallbackIt’s mind-boggling that we can achieve this with just CSS, and we could do it with an arbitrary number of properties. It doesn’t go far enough for me. I would love to see other input controls that can manipulate animation timelines. Imagine text fields progressing animations as you fill them out, or buttons able to play or reverse animations. The latter can be somewhat achieved by combining the :active pseudo-class with the animation-play-state property. But in my experience when you try to use that to animate multiple custom properties, the browser can get confused. By contrast, animation timelines have been implemented with this use case in mind and therefore work smoothly and exactly as I expected.

I’m not the only one who has noticed the potential for hacking this emergent CSS feature. Someone has already implemented this clever Doom clone by combining scroll-timeline with checkbox hacks. The problem I have is it still doesn’t go far enough. We have enough in Chrome to implement avatar builders using scrollbars and range inputs as game controls. I am excited to experiment with unpredictable, sophisticated experiences that are unprecedented in the era before the scroll-timeline feature. After all, if you had to explain the definition of a video game to an alien, wouldn’t you say it is just a hyper-interactive animation?


Slide Through Unlimited Dimensions With CSS Scroll Timelines originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Miriam Suzanne’s in the middle of a redesign of her personal website. It began in August 2022. She’s made an entire series out of the work that’s worth your time, but I wanted to call out the fifth and latest installment because she presents a problem that I think we can all relate to:

But the walls got in my way. Instead of minimal renovation, I got just far enough to live with it and then started a brand new Eleventy repo.

The plan was to prototype […] and bring back well-formed solutions. To echo Dave Rupert, prototyping is useful. It’s easier to play with new ideas when you’re not carrying a decade of content and old code along with you.

But prototyping evolved into what I would call tinkering (complimentary). Maybe I mean procrastinating (also complimentary), but it’s a wandering process that also helps me better understand what I want from a website. I might not make visible progress over two years, but I start to form a point of view […]. Keeping things easy is always where things get complicated. And it brings me back to where my redesign started – a desire to clarify the information architecture. Not only for visitors, but for myself.

Don’t even tell me you’ve never been there! Jim Neilsen blogged along similar lines. You get a stroke of inspiration that’s the kernel of some idea that motivates you to start, you know, working on it. There’s no real plan, perhaps. The idea and inspiration are more than enough to get you going… that is until you hit a snag. And what I appreciate about Miriam’s post is that she’s calling out content as the snag. Well, not so much a snag as a return to the founding principle for the redesign: a refined content architecture.

  • Sometimes I do events where I speak, or teach a workshop, or perform. Events happen at a time and place.
  • Sometimes I create artifacts like a book or an album, a website, or specification. Artifacts often have a home URL. They might have a launch date, but they are not date-specific.
  • Some of my projects are other channels with their own feeds, their own events and artifacts.
  • Those channels are often maintained by an organization that I work with long-term. A band, a web agency, a performance company, etc.

These boundaries aren’t always clean. A post that remains relevant could be considered an artifact. Events can generate artifacts, and vice versa. An entire organization might exist to curate a single channel.

So, Miriam’s done poking at visual prototypes and ready to pour the filling into the pie crust. I relate with this having recently futzed with the content architecure of this site. I find it tough to start with a solidified design before I know what content is going into it. But I also find it tough to work with no shape at all. In my case, CSS-Tricks has a well-established design that’s evolved, mostly outside of me. I love the design but it’s an inherited one and I’m integrating content around it. Design is the constraint. If I had the luxury of stripping the building to the studs, I might take a different approach because then I could “paint” around it. Content would be the constraint.

It’s yet another version of the Chicken-Egg dilemma. I still think of the (capital-W) Web as a content medium at least in a UA style sense in that it’s the default. It’s more than that, of course. I’m a content designer at heart (and trade) but I’m hesitant to cry “content is king” which reminded me of something I wrote for an end-of-year series we did here answering the question: What is one thing people can do to make their website better? My answer: Read your website.

We start to see the power of content when we open up our understanding of what it is, what it does, and where it’s used. That might make content one of the most extensible problem-solving tools in your metaphorical shed—it makes sites more accessible, extracts Google-juicing superpowers, converts sales, and creates pathways for users to accomplish what they need to do.

And as far as prioritizing content or design, or…?

The two work hand-in-hand. I’d even go so far as to say that a lot of design is about enhancing what is communicated on a page. There is no upstaging one or the other. Think of content and design as supporting one another, where the sum of both creates a compelling call-to-action, long-form post, hero banner, and so on. We often think of patterns in a design system as a collection of components that are stitched together to create something new. Pairing content and design works much the same way.

I’d forgotten those words, so I appreciate Miriam giving me a reason to revisit them. We all need to be recalibrated every so often — swap out air filters, top off the fluids, and rotate the ol’ tires. And an old dog like me needs it a little more often. I spent a few more minutes in that end-of-year series and found a few other choice quotes about the content-design continuum that may serve as inspiration for you, me, or maybe even Miriam as she continues the process of aggragating her distributed self.

This sounds serious, but don’t worry — the site’s purpose is key. If you’re building a personal portfolio, go wild! However, if someone’s trying to file a tax return, whimsical loading animations aren’t likely to be well-received. On the other hand, an animated progress bar could be a nice touch while providing visual feedback on the user’s action.

Cassie Evans, “Empathetic Animation”

Remember, the web is an interactive platform — take advantage of that, where appropriate (less is more, accessibility is integral, and you need to know your audience). Whether that’s scrollytelling, captioned video, and heck, maybe for your audience, now’s the time to start looking into AR/VR! Who knows. Sometimes you just need to try stuff out and see what sticks. Just be careful. Experimentation is great, but we need to make sure we’re bringing everyone along for the ride.

Mel Choyce, “Show, Don’t Tell”

Your personal site is a statement of who you are and what you want to do. If you showcase your favorite type of work, you’ll get more requests for similar projects or jobs — feeding back into a virtuous cycle of doing more of what you love.

Amelia Wattenberger, “Exactly What You Want”

And one of my favorites:

But the prime reason to have a personal website is in the name: it is your personalhome on the web. Since its early days, the web has been about sharing information and freedom of expression. Personal websites still deliver on that promise. Nowhere else do you have that much freedom to create and share your work and to tell your personal story. It is your chance to show what you stand for, to be different, and to be specific. Your site lets you be uniquely you and it can be whatever you imagine it to be.

So if you have a personal site, make sure to put in the work and attention to make it truly yours. Make it personal. Fine-tune the typography, add a theme switcher, or incorporate other quirky little details that add personality. As Sarah Drasner writes, you can feel it if a site is done with care and excitement. Those are the sites that are a joy to visit and will be remembered.

Matthias Ott, “Make it Personal”

That last one has the added perk of reminding me how incredibly great Sarah Drasner is.


Aggregating my distributed self originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Many of you — perhaps most of you — have been sitting on the sidelines while WordPress and WP Engine trade legal attacks on one another. It’s been widely covered as we watch it unfold in the open; ironically, in a sense.

These things can take twists and turns and it doesn’t help that this just so happens to be an emotionally charged topic in certain circles. WordPress is still the leading CMS after all these years and by a long shot. Many developers make their living in the WordPress ecosystem. All of those developers need hosting. WP Engine is still the leading WordPress-flavored host after many years. Many developers host their agencies there and use it to administrate their clients’ sites.

And I haven’t even gotten to the drama. That’s not really the point. The point is that there’s a bunch of heated words flying around and it can be difficult to know where they’re coming from, who they are aimed at, and most importantly, why they’re being said in the first place. So, I’m going to round up a few key voices contributing to the discussion for the sake of context and to help catch up.

Editor’s Note: Even though CSS-Tricks has no involvement with either company, I think it’s mentioning that Automattic was a looooooong time sponsor. This site was also once hosted by Flywheel, a company acquired by WP Engine before we moved to Cloudways following the DigitalOcean acquisition. Me? My personal site runs on WP Engine, but I’m not precious about it having only been there one year.

Prelude to a tweetWe had fair warning that something was coming up when WordPress co-founder Matt Mullenweg sent this out over X:

I know private equity and investors can be brutal (read the book Barbarians at the Gate). Please let me know if any employee faces firing or retaliation for speaking up about their company's participation (or lack thereof) in WordPress. We'll make sure it's a big public deal and…

— Matt Mullenweg (@photomatt) September 19, 2024

There’s the ammo: Don’t let private equity bully you into speaking up against the company you work for when its contributions to WordPress are on the slim side of things.

Private equity. Lack of participation in the WordPress community. Making a big public deal of it. Remember these things because this is one day before…

WordCamp US 2024Matt spoke at WordCamp US (WCUS) 2024 in Portland, OR, last week. September 20 to be exact. Making big, bold statements at WCUS isn’t a new thing for Matt, as many of us still have “Learn JavaScript deeply” tattooed on the brain from 2016.

Matt’s statements this year were less inspirational (instructional?) as they took direct aim at WP Engine as part of a presentation on the merits of open-source collaboration. You can watch and listen to where the first jab was thrown roughly around the 10:05 marker of the recording.

Let’s break down the deal. Matt begins by comparing the open-source contributions to WordPress from his company, Automattic, to those of WP Engine. These things are tracked on WordPress.org as part of a campaign called “Five for the Future” that’s designed to give organizations an influential seat at the table to form the future of WordPress in exchange for open-source contributions back to the WordPress project. Automattic has a page totaling its contributions. So does WP Engine.

Before Matt reveals the numbers, he goes out of his way to call out the fact that both Automattic and WP Engine are large players in the neighborhood of $500 million dollars. That’s the setup for Matt to demonstrate how relatively little WP Engine contributes to WordPress against Matt’s own company. Granted, I have absolutely no clue what factors into contributions, nor how the pages are administrated or updated. But here’s what they show…

Quite the discrepancy! I’d imagine Automattic dwarfs every other company that’s pledged to the campaign. Maybe it would be better to compare the contributions of another non-Automattic pledge that has a fairly strong reputation for participating in WordPress community. 10up is one of the companies that comes straight to my mind and they are showing up for 191 hours per week, or roughly five times WP Engine’s reported time. I get conflicting info on 10up’s revenue, valuation, and size, so maybe the comparison isn’t fair. Or maybe it is fair because 10up is certainly smaller than WP Engine, and no estimate I saw was even close to the $500 million mark.

Whatever the case, bottom line: Matt calls out WP Engine for its lack of effort on a very public stage — maybe the largest — in WordPress Land. He doesn’t stop there, going on to namecheck Silver Lake, a ginormous private equity firm bankrolling the company. The insinuation is clear: there’s plenty of money and resources, so pony up.

That’s bad enough for attendees to raise eyebrows, but it doesn’t end there. Matt encourages users and developers alike to vote with money by not purchasing hosting from WP Engine (11:31) and seems to suggest (23:05) that he’ll provide financial support to any WP Engine employees who lose their jobs from speaking up against their employer.

I think I can get behind the general idea that some companies need a little prodding to pull their weight to something like the Five for the Future campaign. Encouraging developers to pull their projects from a company and employees to jeopardize their careers? Eek.

“WP Engine is not WordPress”This is when I believe things got noisy. It’s one thing to strong-arm a company (or its investors) into doing more for the community. But in a post on his personal blog the day after WCUS, Matt ups the ante alleging that “WP Engine isn’t WordPress.” You’d think this is part of the tough-guy stance he had from the stage, but his argument is much different in this post. Notice it’s about how WP Engine uses WordPress in its business rather than how much the company invests in it:

WordPress is a content management system, and the content is sacred. Every change you make to every page, every post, is tracked in a revision system, just like the Wikipedia. This means if you make a mistake, you can always undo it. It also means if you’re trying to figure out why something is on a page, you can see precisely the history and edits that led to it. These revisions are stored in our database. This is very important, it’s at the core of the user promise of protecting your data, and it’s why WordPress is architected and designed to never lose anything.

WP Engine turns this off. They disable revisions because it costs them more money to store the history of the changes in the database, and they don’t want to spend that to protect your content. It strikes to the very heart of what WordPress does, and they shatter it, the integrity of your content.

OK, gloves off. This is more personal. It’s no longer about community contributions but community trust and how WP Engine erodes trust by preventing WordPress users from accessing core WordPress features for their own profit.

Required readingThat’s where I’d like to end this, at least for now. Several days have elapsed since Matt’s blog post and there are many, many more words flying around from him, community members, other companies, and maybe even your Great Aunt. But if you’re looking for more signal than noise, I’ve rounded up a few choice selections that I feel contribute to the (heated) discussion.

Reddit: Matt Mullenweg needs to step down from WordPress.org leadership ASAPMatt responds to the requisite calls for him to step down, starting with:

To be very clear, I was 100% cordial and polite to everyone at the booth, my message was:

  • I know this isn’t about them, it’s happening several levels above, it’s even above their CEO, it’s coming from their owner, Silver Lake and particularly their board member Lee Wittlinger.

  • Several people inside WP Engine have been anonymously leaking information to me about their bad behavior, and I wanted to let them know if they were caught or faced retaliation that I would support them in every way possible, including covering their salaries until they found a new job.

  • That if we had to take down the WP Engine booth and ban WP Engine that evening, my colleague Chloé could print them all new personal badges if they still wanted to attend the conference personally, as they are community members, not just their company.

This was delivered calmly, and they said thank you, and their head of comms, Lauren Cox, who was there asked that they have time to regroup and discuss.

Automattic’s Actionable Misconduct Directed to WP EngineWP Engine issues a cease and desist letter designed to stop Matt from disparaging them publicly. But hold up, because there’s another juicy claim in there:

In the days leading up to Mr. Mullenweg’s September 20th keynote address at the WordCamp US Convention, Automattic suddenly began demanding that WP Engine pay Automattic large sums of money, and if it didn’t, Automattic would wage a war against WP Engine.

And yes, they did issue it from their own site’s /wp-content directory. That’s easy to lose, so I’ve downloaded it to link it for posterity.

Open Source, Trademarks, and WP EngineJust today, Matt published a cease and desist letter to the Auttomatic blog where he alleges that WP Engine’s commercial modifications to WordPress Core violate the WordPress trademark. Again, this has become about licensing, not contributions:

WP Engine’s business model is based on extensive and unauthorized use of these trademarks in ways that mislead consumers into believing that WP Engine is synonymous with WordPress. It’s not.

This is trademark abuse, not fair competition.

This is no longer WordPress vs. WP Engine. It’s more like Automattic vs. WP Engine. But with Matt’s name quite literally in the name Automattic, let’s be real and call this Matt Mullenweg vs. WP Engine.

WP Tavern coverageWP Tavern is still the closest thing we have to an official WordPress news outlet. Nevermind that it’s funded and hired by Automattic (among others). I respect it, though I honestly have been less attentive to it since the team turned over earlier this year. It’s still a great spot to catch up on the post-event coverage:

  • Highlights from Matt Mullenweg’s Spiciest Word Camp Presentation at WordCamp US 2024 (Sep. 24)
  • Automattic Responds to WP Engine’s Cease and Desist with Legal Action (Sep. 24)

There’s another more recent WP Tavern article I want to call out because it’s a huge development in this saga…

WP Engine Banned from Using WordPress.org ResourcesDang. This is the point of no return. It not only affects WP Engine proper, but the Flywheel hosting it also owns.

WordPress.org has blocked WP Engine customers from updating and installing plugins and themes via WP Admin.

I was able to update plugins on my site as recently as yesterday, but let’s see as of this morning.

Aww, biscuits.Maybe I can still see details about my installed plugins…

Double biscuits!This is a bad, bad situation. I have thoughts about it and neither side looks good. Using real people with no dog in the fight to make a point is never gonna be a good look. Then again, both sides have valid points and I can see where they’re coming from. I just hate to see it come to a head like this.


Catching Up on the WordPress 🚫 WP Engine Sitch originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A new tool from Eric Meyer, Brian Kardell, and Stephanie Stimac backed with Igalia’s support. Brian announced it on his blog, as did Eric, describing it like this:

What BCD Watch does is, it grabs releases of the Browser Compatibility Data (BCD) repository that underpins the support tables on MDN and services like caniuse.com. It then analyzes what’s changed since the previous release.

Every Monday, BCD Watch produces two reports. The Weekly Changes Report lists all the changes to BCD that happened in the previous week — what’s been added, removed, or renamed in the whole of BCD. It also tells you which of the Big Three browsers newly support (or dropped support for) each listed feature, along with a progress bar showing how close the feature is to attaining Baseline status.

Browser support data is at MDN. There’s also plenty at Caniuse.com. The two share data, in fact, though not all of it. We now have Baseline, which is also cited in MDN and Caniuse alike. It’s nice to see an effort at cracking a central spot for all this — organized by date, no less.

Oh, and hey, there’s a feed. Even better.

You can also poke at its repo. Thanks a bunch, Eric, Brian, Stephanie, and Igalia! This is super helpful and already part of my toolkit.


BCD Watch originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The <select> element is a fairly straightforward concept: focus on it to reveal a set of <option>s that can be selected as the input’s value. That’s a great pattern and I’m not suggesting we change it. That said, I do enjoy poking at things and found an interesting way to turn a <select> into a dial of sorts — where options are selected by scrolling them into position, not totally unlike a combination lock or iOS date pickers. Anyone who’s expanded a <select> for selecting a country knows how painfully long lists can be and this could be one way to prevent that.

Here’s what I’m talking about:

CodePen Embed FallbackIt’s fairly common knowledge that styling <select> in CSS is not the easiest thing in the world. But here’s the trick: we’re not working with by jamming a bunch of JavaScript into a <div>. We’re still working with semantic form controls, only it’s radio buttons.

```

``` What we need is to style the list of selectable controls where we are capable of managing their sizes and spacing in CSS. I’ve gone with a group of labels with nested radio boxes as far as the markup goes. The exact styling is totally up to you, of course, but you can use these base styles I wrote up if you want a starting point.

.scroll-container { /* SIZING & LAYOUT */ --itemHeight: 60px; --itemGap: 10px; --containerHeight: calc((var(--itemHeight) * 7) + (var(--itemGap) * 6)); width: 400px; height: var(--containerHeight); align-items: center; row-gap: var(--itemGap); border-radius: 4px; /* PAINT */ --topBit: calc((var(--containerHeight) - var(--itemHeight))/2); --footBit: calc((var(--containerHeight) + var(--itemHeight))/2); background: linear-gradient( rgb(254 251 240), rgb(254 251 240) var(--topBit), rgb(229 50 34 / .5) var(--topBit), rgb(229 50 34 / .5) var(--footBit), rgb(254 251 240) var(--footBit)); box-shadow: 0 0 10px #eee;} A couple of details on this:

  • --itemHeight is the height of each item in the list.
  • --itemGap is meant to be the space between two items.
  • The --containerHeight variable is the .scroll-container’s height. It’s the sum of the item sizes and the gaps between them, ensuring that we display, at maximum, seven items at once. (An odd number of items gives us a nice balance where the selected item is directly in the vertical center of the list).
  • The background is a striped gradient that highlights the middle area, i.e., the location of the currently selected item.
  • The --topBit and –-footBit variables are color stops that visually paint in the middle area (which is orange in the demo) to represent the currently selected item.

I’ll arrange the controls in a vertical column with flexbox declared on the .scroll-container:

.scroll-container { display: flex; flex-direction: column; /* rest of styles */} With layout work done, we can focus on the scrolling part of this. If you haven’t worked with CSS Scroll Snapping before, it’s a convenient way to direct a container’s scrolling behavior. For example, we can tell the .scroll-container that we want to enable scrolling in the vertical direction. That way, it’s possible to scroll to the rest of the items that are not in view.

.scroll-container { overflow-y: scroll; /* rest of styles */} Next, we reach for the scroll-snap-style property that can be used to tell the .scroll-container that we want scrolling to stop on an item — not near an item, but directly on it.

.scroll-container { overflow-y: scroll; scroll-snap-type: y mandatory; /* rest of styles */} Now items “snap” onto an item instead of allowing a scroll to end wherever it wants. One more little detail I like to include is overscroll-behavior, specifically along the y-axis as far as this demo goes:

.scroll-container { overflow-y: scroll; scroll-snap-type: y mandatory; overscroll-behavior-y: none; /* rest of styles */} overscroll-behavior-y: none isn’t required to make this work, but when someone scrolls through the .scroll-container (along the y-axis), scrolling stops once the boundary is reached, and any further continued scrolling action will not trigger scrolling in any nearby scroll containers. Just a form of defensive CSS.

Time to move to the items inside the scroll container. But before we go there, here are some base styles for the items themselves that you can use as a starting point:

.scroll-item { /* SIZING & LAYOUT */ width: 90%; box-sizing: border-box; padding-inline: 20px; border-radius: inherit; /* PAINT & FONT */ background: linear-gradient(to right, rgb(242 194 66), rgb(235 122 51)); box-shadow: 0 0 4px rgb(235 122 51); font: 16pt/var(--itemHeight) system-ui; color: #fff; input { appearance: none; } abbr { float: right; } /* The airport code */} As I mentioned earlier, the --itemHeight variable is setting as the size of each item and we’re declaring it on the flex property — flex: 0 0 var(--itemHeight). Margin is added before and after the first and last items, respectively, so that every item can reach the middle of the container through scrolling.

The scroll-snap-align property is there to give the .scroll-container a snap point for the items. A center alignment, for instance, snaps an item’s center (vertical center, in this case) with the .scroll-container‘s center (vertical center as well). Since the items are meant to be selected through scrolling alone pointer-events: none is added to prevent selection from clicks.

One last little styling detail is to set a new background on an item when it is in a :checked state:

.scroll-item { /* Same styles as before */ /* If input="radio" is :checked */ &:has(:checked) { background: rgb(229 50 34); }} But wait! You’re probably wondering how in the world an item can be :checked when we’re removing pointer-events. Good question! We’re all finished with styling, so let’s move on to figuring some way to “select” an item purely through scrolling. In other words, whatever item scrolls into view and “snaps” into the container’s vertical center needs to behave like a typical form control selection. Yes, we’ll need JavaScript for that.

let observer = new IntersectionObserver(entries => { entries.forEach(entry => { with(entry) if(isIntersecting) target.children[1].checked = true; });}, { root: document.querySelector(`.scroll-container`), rootMargin: `-51% 0px -49% 0px`});document.querySelectorAll(`.scroll-item`).forEach(item => observer.observe(item)); The IntersectionObserver object is used to monitor (or “observe”) if and when an element (called a target) crosses through (or “intersects”) another element. That other element could be the viewport itself, but in this case, we’re observing the .scroll-container for when a .scroll-item intersects it. We’ve established the observed boundary with rootMargin:"-51% 0px -49% 0px".

A callback function is executed when that happens, and we can use that to apply changes to the target element, which is the currently selected .scroll-item. In our case, we want to select a .scroll-item that is at the halfway mark in the .scroll-container: target.children[1].checked = true.

That completes the code. Now, as we scroll through the items, whichever one snaps into the center position is the selected item. Here’s a look at the final demo again:

CodePen Embed FallbackLet’s say that, instead of selecting an item that snaps into the .scroll-container‘s vertical center, the selection point we need to watch is the top of the container. No worries! All we do is update the scroll-snap-align property value from center to start in the CSS and remove the :first-of-type‘s top margin. From there, it’s only a matter of updating the scroll container’s background gradient so that the color stops highlight the top instead of the center. Like this:

CodePen Embed FallbackAnd if one of the items has to be pre-selected when the page loads, we can get its position in JavaScript (getBoundingClientRect()) and use the scrollTo() method to scroll the container to where that specific item’s position is at the point of selection (which we’ll say is the center in keeping with our original demo). We’ll append a .selected class on that .scroll-item.

```

`` Let’s select the.selected` class, get its dimensions, and automatically scroll to it on page load:

let selected_item = (document.querySelector(".selected")).getBoundingClientRect();let scroll_container = document.querySelector(".scroll-container");scroll_container.scrollTo(0, selected_item.top - scroll_container.offsetHeight - selected_item.height); It’s a little tough to demo this in a typical CodePen embed, so here’s a live demo in a GitHub Page (source code). I’ll drop a video in as well:

That’s it! You can build up this control or use it as a starting point to experiment with different layouts, styles, animations, and such. It’s important the UX clearly conveys to the users how the selection is done and which item is currently selected. And if I was doing this in a production environment, I’d want to make sure there’s a good fallback experience for when JavaScript might be unavailable and that my markup performs well on a screen reader.

References and further reading* A Few Functional Uses for Intersection Observer to Know When an Element is in View (Preethi Sam) * An Explanation of How the Intersection Observer Watches (Travis Almand) * Practical CSS Scroll Snapping (Max Kohler) * The Current State of Styling Selects in 2019 (Chris Coyier) * CSS Flexbox Layout Guide (CSS-Tricks) * CSS flex property (CSS-Tricks) * CSS Scroll Snap Properties (MDN) * scrollTo() (MDN)


How to Make a “Scroll to Select” Form Control originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Mixing colors in CSS is pretty much a solved deal, thanks to the more recent color-mix() function as it gains support. Pass in two color values — any two color values at all — and optionally set the proportions.

background-color: color-mix(#000 30%, #fff 70%); We also have the relative color syntax that can manipulate colors from one color space to another and modify them from there. The preeminent use case being a way to add opacity to color values that don’t support it, such as named colors.

background-color: hsl(from black h s l); /* hsl(0 0% 0%) */background-color: hsl(from black h s l / 50%); /* hsl(0 0% 0% / 50%) */ We can get hacky and overlay one opaque element with another, I suppose.

CodePen Embed FallbackSame general idea maybe, but with mix-blend-mode?

CodePen Embed FallbackAnother roundabout way of getting there is something I saw this morning when looking over the updates that Ryan added to the animation property in the Almanac. Now, we all know that animation is shorthand for about a gajillion other properties (the order of which always eludes me). One of those is animation-composition and it’s used to… well, Ryan nails the explanation:

Defining a property in CSS also sets what is considered the underlying value of the property. By default, keyframe animations will ignore the underlying value, as they only consider the effect values defined within the animation. Keyframes create a stack of effect values, which determines the order in which the animation renders to the browser. Composite operations are how CSS handles the underlying effect combined with the keyframe effect value.

Manuel Matuzović and Robin Rendle also have excellent ways of explaining the property, the former of which sparked us to update the Almanac.

OK! We have three values supported by animation-composition to replace the underlying property value in favor of the effect value defined in keyframes, add to them, or accumulate for combining multiple values. The add value is what’s interesting to us because… oh gosh, let’s just let Ryan take it:

[I]nstead of replacing an underlying background-color property value with the keyframe’s effect value, the color type values are combined, creating new colors.

A-ha! The example goes like this:

CodePen Embed FallbackSee that? The add value blends the two colors as one transitions to the other. Notice, too, how much smoother that transition is than the replace value, although we wind up with a completely new color at the 100% mark rather than the color we declared in the keyframes. What if we pause the animation at some arbitrary point? Can we extract a new color value from it?

Ryan made this so that hovering on the elements pauses the animation. If we crack open DevTools and force the :hover pseudo on the element, maybe we can head over to the Computed tab to get the new color value.

Interestingly, we get some RGB conversions in there. Probably because updating color channels is easier than converting one hex to another? Browsers do smart stuff.

Now I want to go update my old color interpolation demo…

CodePen Embed FallbackHmm, not any different to my untrained eye. Maybe that’s only because we’re changing the HSL’s hue channel and it’s super subtle. Whatever the case, animation-composition can produce new computed color values. What you need those for and what you’d do with them? I dunno, but go wild.


Color Mixing With Animation Composition originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I was looking over an older article Patrick Brosset penned for us introducing <selectmenu>, a new proposal at the time for a more style-able cousin to <select>. From there, I clicked the linked-up <selectmenu> explainer and got… this:

OK, link rot is a thing and happens all the time. Perhaps the site needs a little URL designing? But no, it’s not that at all. I searched a bit and found Jared White’s post saying that <selectmenu> is no more, which came by way of Una’s post over at the Chrome Developer Blog seeking feedback on a “customizable select”. And Adam Argyle’s got a wonderful demo dedicated to it, no surprise there.

I’m only sharing the links for now but plan to spend some time with it and jot down notes on Open UI’s new page for the Customizable <select>. I enjoyed looking at the boilerplate from Adam’s demo as a first glance:

select { &, &::picker(select) { appearance: base-select; } &::picker(select) { transition: display allow-discrete 1s, opacity 1s, overlay 1s allow-discrete ; } &:not(:open)::picker(select) { opacity: 0; } &:open::picker(select) { opacity: 1; @starting-style { opacity: 0; } }} I see the ::picker(select) there that’s driving all of it. If I sneak a peek at Una’s post, I see that there are more ways to select different <select> parts, including:

  • <selectedoption> (the current selection)
  • <option> (which now accepts HTML in between the tags!)
    • option::before
    • option:checked (a little confusion here with the selected option)
  • <button> (the little chevron arrow marker thingy)

So, perhaps Chrome is more of a fan of extending the native <select> with additional CSS features for selecting the existing parts rather than moving forward with a completely new element. That’s cool, as one of Una’s demos shows how we still get the default <select> behavior even if a browser does not support the new selectors.

CodePen Embed Fallback


The selectmenu Element is No More…Long Live select! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

For the past two months, all my livelihood has gone towards reading, researching, understanding, writing, and editing about Anchor Positioning, and with many Almanac entries published and a full Guide guide on the way, I thought I was ready to tie a bow on it all and call it done. I know that Anchor Positioning is still new and settling in. The speed at which it’s moved, though, is amazing. And there’s more and more coming from the CSSWG!

That all said, I was perusing the last CSSWG minutes telecon and knew I was in for more Anchor Positioning when I came to the following resolution:

Whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree) (Issue #10526: When does anchor-scope “match” a name?)

Resolutions aren’t part of the specification or anything, but the strongest of indications about where they’re headed. So, I thought this was a good opportunity not only to take a peek at what we might get in anchor-scope and touch on other interesting bits from the telecon.

Remember that you can subscribe and read the full minutes on W3C.org. :)

Full TranscriptionWhat’s anchor-scope?To register an anchor, we can give it a distinctive anchor-name and then absolutely positioned elements with a matching position-anchor are attached to it. Even though it may look like it, anchor-name doesn’t have to be unique — we may reuse an anchor element inside a component with the same anchor-name.

```

  • Anchor 1
    Target 1
  • Anchor 2
    Target 2
  • Anchor 3
    Target 3

``` However, if we try to connect them with CSS,

.anchor { anchor-name: --my-anchor;}.target { position: absolute; position-anchor: --my-anchor; position-area: top right;} We get an unpleasant surprise where instead of each .anchor having their .target positioned at its top-right edge, they all pile up on the last .anchor instance. We can see it better by rotating each target a little. You’ll want to check out the next demo in Chrome 125+ to see the behavior:

CodePen Embed FallbackThe anchor-scope property should make an anchor element only discoverable by targets in their individual subtree. So, the prior example would be fixed in the future like this:

.anchor { anchor-name: --my-anchor; anchor-scope: --my-anchor;} This is fairly straightforward — anchor-scope makes the anchor element available only in that specific subtree. But then we have to ask another question: What should the anchor-scope own scope be? We can’t have an anchor-scope-scope property and then an anchor-scope-scope-scope and so on… so which behavior should it be?

This is what started the conversation, initially from a GitHub issue:

When an anchor-scope is specified with a <dashed-ident>, it scopes the name to that subtree when the anchor name is “matching”. The problem is that this matching can be interpreted in at least three ways: (Assuming that anchor-scope is a tree-scoped reference, which is also not clear in the spec):

  1. It matches by the ident part of the name only, ignoring any tree-scope that would be associated with the name, or
  2. It matches by exact match of the ident part and the associated tree-scope, or
  3. It matches by some mechanism similar to dereferencing of tree-scoped references, where it’s a match when the tree-scope of the anchor-scope-name is an inclusive ancestor of the tree-scope of the anchor query.

And then onto the CSSWG Minutes:

TabAtkins: In anchor positioning, anchor names and references are tree scoped. The anchor-scope property that scopes, does not say whether the names are tree scoped or not. Question to decide: should they be?

TabAtkins: I think the answer should be yes. If you have an anchor in a shadow tree with a part involved, then problems result if anchor scopes are not tree scoped. This is bad, so I think it should be tree scoped

sounds pretty reasonable

makes sense to me as far as I can understand it :)

This solution of the scope of scoping properties expanded towards View Transitions, which also rely on tree scoping to work:

khush: Thinking about this in the context of view transitions: in that API you give names and the tree scope has to be the same for them to match. There is another view transitions feature where I’m not sure if the spec says it’s tree scoped

khush: Want to make sure that feature is covered by the more general resolution

TabAtkins: Proposed more general resolution: whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree)

So the scope of anchor-scope is tree-scoped. Say that five times fast!

RESOLVED: whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree)

The next resolution was pretty straightforward. Besides allowing a <dashed-ident> that says that specific anchor is three-scoped, the anchor-scope property can take an all keyword, which means that all anchors are tree-scoped. So, the question was if all is also a tree-scoped value.

TabAtkins: anchor-scope, in addition to idents, can take the keyword ‘all‘, which scopes all names. Should this be a tree-scoped ‘all‘? (i.e. only applies to the current tree scope)

TabAtkins: Proposed resolution: the ‘all‘ keyword is also tree-scoped in the same way sgtm +1, again same pattern with view-transition-group

RESOLVED: the ‘all‘ keyword is tree-scoped

The conversation switched gears toward new properties coming in the CSS Scroll Snap Module Level 2 draft, which is all about changing the user’s initial scroll with CSS. Taking an example directly from the spec, say we have an image carousel:

```

`` We could set the initial scroll to show another image by setting it’sscroll-start-targettoauto`:

.carousel { overflow-inline: auto;}.carousel .origin { scroll-start-target: auto;} As of right now, the only way to achieve this is using JavaScript to scroll an element into view:

document.querySelector(".origin").scrollIntoView({ behavior: "auto", block: "center", inline: "center"}); The last example is probably a carousel that is only scrollable in the inline direction. Still, there are doubts as far when the container is scrollable in both the inline and block directions. As seen in the initial GitHub issue:

The scroll snap 2 spec says that when there are multiple elements that could be scroll-start-targets for a scroll container “user-agents should select the one which comes first in tree order“.

Selecting the first element in tree-order seems like a natural way to resolve competition between multiple targets which would be scrolled to in one particular axis but is perhaps not as flexible as might be needed for the 2d case where an author wants to scroll to one item in one axis and another item in the other axis.

And back to the CSSWG minutes:

DavidA: We have a property we’re adding called scroll-start-target that indicates if an element within a scroll container, then the scroll should start with that element onscreen. Question is what happens if there are multiple targets?
DavidA: Propose to do it in reverse-DOM order, this would result in the first one applied last and then be on screen. Also, should only change the scroll position if you have to.

After discussing why we have to define scroll-start-target when we have scroll-snap-align, the discussion went on the reverse-DOM order:

fantasai: There was a bunch of discussion about regular vs reverse-DOM order. Where did we end up and why?
flackr: Currently, we expect that it scrolls to the first item in DOM order. We probably want that to still happen. That is why the proposal is to scroll to each item in sequence in reverse-DOM order.

So we are coming in reverse to scroll the element, but only as required so the following elements are showing as much as possible:

flackr: There is also the issue of nearest…
fantasai: Can you explain nearest?
flackr: Same as scroll into view
fantasai: ?
flackr: This is needed with you scroll multiple things into view and want to find a good position (?)
fantasai: You scroll in reverse-DOM order…when you add the spec can you make it really clear that this is the end result of the algorithm?
flackr: Yes absolutely
fantasai: Otherwise it seems to make sense

And so it was resolved:

Proposed resolution 2: When scroll-start-target targets multiple elements, scroll to each in reverse DOM order with text to specify priority is the first item

Lastly, there was the debate about the text-underline-position, that when set to auto says, “The user agent may use any algorithm to determine the underline’s position; however it must be placed at or under the alphabetic baseline.” The discussion was about whether the auto value should automatically adjust the underlined position to match specific language rules, for example, at the right of the text for vertical writing modes, like Japanese and Mongolian.

fantasai: The initial value of text-underline-position is auto, which is defined as “find a good place to put the underline”.
Three options there: (1) under alphabetical baseline, (2) fully below text (good for lots-of-descenders cases), (3) for vertical text on the RHS
fantasai: auto value is defined in the spec about ‘how far down below the text’, but doesn’t say things about flipping. The current spec says “at or below”. In order to handle language-specific aspects, there is a default UA style sheet that for Chinese and Japanese and Korean there are differences for those languages. A couple of implementations do this
fantasai: Should we change the spec to mention these things?
fantasai: Or should we stick with the UA stylesheet approach?

The thing is that Chrome and Firefox already place the underline on the right in vertical Japanese when text-underline-position is auto.

CodePen Embed FallbackThe group was left with three options:

A) Keep spec as-is, update Gecko + Blink to match (using UA stylesheet for language switch)
B) Introduce auto to text-emphasis-position and use it in both text-emphasis-position and text-underline-position to effect language switches
C) Adopt inconsistent behavior: text-underline-position uses ‘auto‘ and text-emphasis-position uses UA stylesheet

Many CSSWG members like Emilio Cobos, TabAtkins, Miriam Suzanne, Rachel Andrew and fantasai casted their votes, resulting in the following resolution:

RESOLVED: add auto value for text-emphasis-position, and change the meaning of text-underline-position: auto to care about left vs right in vertical text

I definitely encourage you to read at the full minutes! Or if you don’t have the time, you can there’s a list just of resolutions.


CSSWG Minutes Telecon (2024-09-18) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Getting right to it: the CSS-Tricks Almanac got a big refresh this week!

I’m guessing you’re already familiar with this giant ol’ section of CSS-Tricks called the Almanac. This is where we publish references for CSS selectors and properties. That’s actually all we’ve published in there since the beginning of time… or at least since 2009 when most of the original work on it took place. That might as well be the beginning of time in web years. We might even call it Year 1 BR, or one year before responsive.

You don’t need me telling you how different writing CSS is today in the Year 14 AR. Quite simply, the Almanac hasn’t kept pace with CSS which is much, much more than properties and selectors. The truth is that we never really wanted to touch the Almanac because of how it’s configured in the back end and I’m pretty sure I spotted a ghost or two in there when I poked at it.

Visiting the Alamanc now, you’ll find a wider range of CSS information, including dedicated sections for pseudo class selectors, functions, and at-rules in addition to the existing properties and selectors sections. We’ve still got plenty of work to do filling those in (you should help!) but the architecture is there and there’s room to scale things up a little more if needed.

The work was non-trivial and as scary as I thought it would be. Let me walk you around some of what I did.

The situationWe’re proudly running WordPress and have since day one. There’s a lot of benefit to that, especially as templating goes. It may not be everyone’s favorite jam, but I’m more than cool with it and jumped in — damn the torpedoes!

If you’re familiar with WordPress, then you know that content is largely sliced up into two types: pages and posts. The difference between the two is fairly minimal — and nearly indistinguishable as they both employ the same editing interface. There are nuances, of course, but pages are largely different in that they are hierarchal, meaning they’re best for establishing parent-child page relationships for a nicely structured sitemap. Posts, meanwhile, are more meta-driven in the sense that we get to organize things by slapping tags on them or dropping them into a category group or whatever custom taxonomy we might have in reach.

The Almanac is built on pages, not posts. Pages are boss at hierarchy, and the Alamanc is a highly structured area that has a typical sitemap-like flow, and it just so happens to follow alphabetical order. For example, an entry for a CSS property, let’s say aspect-ratio, goes: Almanac → Properties → A → Aspect Ratio.

That doesn’t sound like a bad thing, right? It’s not, but pages are tougher to query in a template than posts, which have a lot more meta we can use for filtering and whatnot. Pages, on the other hand, not so much. (Well, not obviously so much.) They’re usually returned as structured objects because, you know, hierarchy. But it also means we have to manually create all of those pages, unlike tags and categories that automatically generqte archives. It feels so dang silly creating an empty page for the letter “A” that’s a child of the “Properties” page — which is a child of the Almanac itself — just so there’s a logical place to insert properties that begin with the letter A. And that has to happen for both properties and selectors.

The real problem is that the Almanac simply tapped out. We want to publish other CSS-y things in there, like functions and at-rules, but the Almanac was only ever built to show two groups. That’s why we never published anything else. It’s also why general selectors and pseudo-selectors were in the same bucket.

Expanding the place to hold more content was the scope I worked with, knowing that I’d have some chances to style things along the way.

One template to rule them allThat’s how things were done. The original deal was a single template used for the Almanac index and the alphabetical pages that list selectors and properties. It was neat, really. The page first checked if the current page is the Almamac page that sits atop the page hierarchy. If it is that page, then the template spits out the results for selectors and properties on the same page, in two different columns.

The query for that is quite impressive.

```

'page', 'post_status' => 'publish', 'post_parent' => $selectorID, 'posts_per_page' => -1, 'orderby' => 'title', 'order' => "ASC" )); $html = '
'; $html .= ''; $html .= '
'; while ($selector_query->have_posts()) : $selector_query->the_post(); $html .= '
'; $html .= '

'; $html .= get_the_title(); $html .= '

'; $html .= '
'; $html .= get_the_excerpt(); $html .= '
';    $html .= get_post_meta(get_the_id(), 'almanac_example_code', true);    $html .= '
'; $html .= 'Continue Reading'; $html .= '
';endwhile; $html .= "
"; $html .= "
"; return $html;} ``` That’s actually half the snippet. Notice it’s only marked up for a `$selector_query`. It loops through this thing again for a `$property_query`. From there, the function needs to be called 26 times: one for each letter of the alphabet. It takes three parameters, namely the letter (e.g. `A`) and the page IDs for the “A” pages (e.g. `14146, 13712`) that are children of the selectors and properties. ```

``` And if we’re not currently on the index page? The template spits out just the alphabetical list of child pages for that particular section, e.g. properties. One template is enough for all that.

Querying child pagesI could have altered the letterOutput() function to take more page IDs to show the letter pages for other sections. But honestly, I just didn’t want to go there. I chose instead to reduce the function to one page ID argument instead of two, then split the template up: one for the main index and one for the “sub-sections” if you will. Yes, that means I wound up with more templates in my WordPress theme directory, but this is mostly for me and I don’t mind. I can check which sub-page I’m on (whether it’s a property index, selector index, at-rules index, etc.) and get just the child pages for those individually.

The other trouble with the function? All the generated markup is sandwiched inside a while()statement. Even if I wanted to parse the query by section to preserve a single template architecture, it’s not like I can drop an if() statement anywhere I want in there without causing a PHP fatal error or notice. Again, I had no interest in re-jiggering the function wholesale.

Letter archivesPublishing all those empty subpages for the letters of each section and then attaching them to the correct parent page is a lot of manual work. I know because I did it. There’s certainly a better, even programmatic, way but converting things from pages to posts and working from that angle didn’t appeal to me and I was working on the clock. We don’t always get to figure out an “ideal” way of doing things.

It’s a misnomer calling any of these letter pages “archives” according to WordPress parlance, but that’s how I’m looking at the child pages for the different sections — and that’s how it would have been if things were structured as posts instead of pages. If I have a section for Pseudo-Selectors, then I’m going to need individual pages for letters A through Z that, in turn, act as the parent pages for the individual pseudos. Three new sections with 26 letters each means I made 78 new pages. Yay.

You get to a letter page either through the breadcrumbs of an Almanac page (like this one for the aspect-ratio property) or by clicking the large letter in any of the sections (like this one for properties).

We’ve never taken those pages seriously. They’re there for structure, but it’s not like many folks ever land on them. They’re essentially placeholders. Helpful, yes, but placeholders nonetheless. We have been so unserious about these pages that we never formally styled them. It’s a model of CSS inheritance, tell you what.

Yup, you can stop gushing now. 😍This is where I took an opportunity to touch things up visually. I’ve been working with big, beefy things in the design since coming back to this job a few months ago. Things like the oversized headings and thick-as-heck shadows you see.

It’s not my natural aesthetic, but I think it works well with CSS-Tricks… and maybe, just maybe, there’s a tear of joy running down Chris Coyier’s face because of it. Maybe.

NavigatingAnother enhancement was added to the navigation displayed on the main index page. I replaced the alphabetical navigation at the top with a nav that takes you to each section and now we can edit the page directly in WordPress without having to dev around.

Before (top) and after (bottom)The only thing that bothers me is that I hardcoded the dang thing instead of making it a proper WordPress menu that I can manage from the admin. [Adds a TODO to his list.]

Since I freed up the Alamanc index from displaying the selector and property lists in full, I could truly use it as an index for the larger number of sections we’re adding.

There may be a time when we’ll want to make the main page content less redundant with the navigation but I’ll take this as a good start that we can build up from. Plus, it’s now more consistent with the rest of the “top-level” pages linked up in the site’s main menu as far as headers go and that can’t be bad.

Oh yeah, and while we’re talking about navigating around, the new sections have been added to the existing left sidebar on individual Almanac pages to help jump to other entries in any section without having to return to the index.

Yes, that’s really how little content we have in there right now!Quickly reference thingsThe last enhancement I’ll call out is minor but I think it makes a positive difference. If you head over to any subpage of the index — i.e., Selectors, Properties, Pseudos, Functions, At-Rules — a snippet and high-level definition is available for each item at the ready without having to jump to the full page.

We’ve always been big on “get to the examples quickly” and I think this helps that cause quite a bit.

“You could’ve also done [x]…”Yeah, lots more opportunities to tighten things up. The only goal I had in mind was to change things up just enough for the Almanac to cover more than selectors and properties, and maybe take some styling liberties here and there. There’s plenty more I wanna do and maybe we’ll get there, albeit incrementally.

What sort of things? Well, that hardcoded index navigation for one. But more than that, I’d like to keep pushing on the main page. It was serving a great purpose before and I pretty much wiped that out. It’d be good to find a way to list all of the entries — for all sections — the way we did when it was only twe sections. That’s something I plan to poke at.

And, yes, we want to cover even more CSS-y items in there, like general terminology, media and user preference queries, possibly specifications… you get the idea. The Almanac is a resource for us here on the team as much as it is for you, and we refer to it on the daily. We want it flush with useful information.

That’s all.You can stop reading now and just head on over to the Almanac for a virtual stroll.


Re-Working the CSS Almanac originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I’m working on a refresh of my personal website, what I’m calling the HD remaster. Well, I wouldn’t call it a “full” redesign. I’m just cleaning things up, and Polypane is coming in clutch. I wrote about how much I enjoy developing with Polypane on my personal blog back in March 2023. In there, I say that I discover new things every time I open the browser up and I’m here to say that is still happening as of August 2024.

Polypane, in case you’re unfamiliar with it, is a web browser specifically created to help developers in all sorts of different ways. The most obvious feature is the multiple panes displaying your project in various viewport sizes:

I’m not about to try to list every feature available in Polypane; I’ll leave that to friend and creator, Kilian Valkhof. Instead, I want to talk about a neat feature that I discovered recently.

Outline tabInside Polypane’s sidebar, you will find various tabs that provide different bits of information about your site. For example, if you are wondering how your social media previews will look for your latest blog post, Polypane has you covered in the Meta tab.

The tab I want to focus on though, is the Outline tab. On the surface, it seems rather straightforward, Polypane scans the page and provides you outlines for headings, landmarks, links, images, focus order, and even the full page accessibility tree.

Seeing your page this way helps you spot some pretty obvious mistakes, but Polypane doesn’t stop there. Checking the Show issues option will point out some of the not-so-obvious problems.

In the Landmarks view, there is an option to Show potentials as well, which displays elements that could potentially be page landmarks.

In these outline views, you also can show an overlay on the page and highlight where things are located.

Now, the reason I even stumbled upon these features within the Outline tab is due to a bug I was tracking down, one specifically related to focus order. So, I swapped over to the “Focus order” outline to inspect things further.

That’s when I noticed the option to see an overlay for the focus order.

This provides a literal map of the focus order of your page. I found this to be incredibly useful while troubleshooting the bug, as well as a great way to visualize how someone might navigate your website using a keyboard.

These types of seemingly small, but useful features are abundant throughout Polypane.

Amazing toolWhen I reached out to Kilian, mentioning my discovery, his response was “Everything’s there when you need it!”

I can vouch for that.


Clever Polypane Debugging Features I’m Loving originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Only Chris, right? You’ll want to view this in a Chromium browser:

CodePen Embed FallbackThis is exactly the sort of thing I love, not for its practicality (cuz it ain’t), but for how it illustrates a concept. Generally, tutorials and demos try to follow the “rules” — whatever those may be — yet breaking them helps you understand how a certain thing works. This is one of those.

The concept is pretty straightforward: one target element can be attached to multiple anchors on the page.

```

`` We’ve gotta register the anchors and attach the.target` to them:

.anchor-1 { anchor-name: --anchor-1;}.anchor-2 { anchor-name: --anchor-2;}.target { } Wait, wait! I didn’t attach the .target to the anchors. That’s because we have two ways to do it. One is using the position-anchor property.

.target { position-anchor: --anchor-1;} That establishes a target-anchor relationship between the two elements. But it only accepts a single anchor value. Hmm. We need more than that. That’s what the anchor() function can do. Well, it doesn’t take multiple values, but we can declare it multiple times on different inset properties, each referencing a different anchor.

.target { top: anchor(--anchor-1, bottom);} The second piece of anchor()‘s function is the anchor edge we’re positioned to and it’s gotta be some sort of physical or logical inset — top, bottom, start, end, inside, outside, etc. — or percentage. We’re bascially saying, “Take that .target and slap it’s top edge against --anchor-1‘s bottom edge.

That also works for other inset properties:

.target { top: anchor(--anchor-1 bottom); left: anchor(--anchor-1 right); bottom: anchor(--anchor-2 top); right: anchor(--anchor-2 left);} Notice how both anchors are declared on different properties by way of anchor(). That’s rad. But we aren’t actually anchored yet because the .target is just like any other element that participates in the normal document flow. We have to yank it out with absolute positioning for the inset properties to take hold.

.target { position: absolute; top: anchor(--anchor-1 bottom); left: anchor(--anchor-1 right); bottom: anchor(--anchor-2 top); right: anchor(--anchor-2 left);} In his demo, Chris cleverly attaches the .target to two <textarea> elements. What makes it clever is that <textarea> allows you to click and drag it to change its dimensions. The two of them are absolutely positioned, one pinned to the viewport’s top-left edge and one pinned to the bottom-right.

If we attach the .target's top and left edges to --anchor-1‘s bottom and right edges, then attach the target's bottom and right edges to --anchor-2‘s top and left edges, we’re effectively anchored to the two <textarea> elements. This is what allows the .target element to stretch with the <textarea> elements when they are resized.

But there’s a small catch: a <textarea> is resized from its bottom-right corner. The second <textarea> is positioned in a way where the resizer isn’t directly attached to the .target. If we rotate(180deg), though, it’s all good.

CodePen Embed FallbackAgain, you’ll want to view that in a Chromium browser at the time I’m writing this. Here’s a clip instead if you prefer.

That’s just a background-color on the .target element. We can put a little character in there instead as a background-image like Chris did to polish this off.

CodePen Embed FallbackFun, right?! It still blows my mind this is all happening in CSS. It wasn’t many days ago that something like this would’ve been a job for JavaScript.


Multiple Anchors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I’m a big Lynn Fisher fan. You probably are, too, if you’re reading this. Or maybe you’re reading her name for the first time, in which case you’re in for a treat.

That’s because I had a chance to sit down with Lynn for a whopping hour to do nothing more than gab, gab, and gab some more. I love these little Smashing Hours because they’re informal like that and I feel like I really get to know the person I’m talking with. And this was my very first time talking with Lynn, we had a ton to talk about — her CSS art, her annual site refreshes, where she finds inspiration for her work, when the web started to “click” for her… and so much more.

Don’t miss the bit where Lynn discusses her current site design (~24 min.) because it’s a masterclass in animation and creativity.


Smashing Hour With Lynn Fisher originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The **text-box-trim** and **text-box-edge** properties in CSS enable developers to trim specifiable amounts of the whitespace that appear above the first formatted line of text and below the last formatted line of text in a text box, making the text box vertically larger than the content within.

This whitespace is called leading, and it appears above and below (so it’s two half-leadings, actually) all lines of text to make the text more readable. However, we only want it to appear in between lines of text, right? We don’t want it to appear along the over or under edges of our text boxes, because then it interferes with our margins, paddings, gaps, and other spacings.

As an example, if we implement a 50px margin but then the leading adds another 37px, we’d end up with a grand total of 87px of space. Then we’d need to adjust the margin to 13px in order to make the space 50px in practice.

As a design systems person, I try to maintain as much consistency as possible and use very little markup whenever possible, which enables me to use the adjacent-sibling combinator (+) to create blanket rules like this:

/* Whenever <element> is followed by <h1> */<element> + h1 { margin-bottom: 13px; /* instead of margin-bottom: 50px; */} This approach is still a headache since you still have to do the math (albeit less of it). But with the text-box-trim and text-box-edge properties, 50px as defined by CSS will mean 50px visually:

Disclaimer: text-box-trim and text-box-edge are only accessible via a feature flag in Chrome 128+ and Safari 16.4+, as well as Safari Technology Preview without a feature flag. See Caniuse for the latest browser support.

Start with text-box-trimtext-box-trim is the CSS property that basically activates text box trimming. It doesn’t really have a use beyond that, but it does provide us with the option to trim from just the start, just the end, both the start and end, or none:

text-box-trim: trim-start;text-box-trim: trim-end;text-box-trim: trim-both;text-box-trim: none; Note: In older web browsers, you might need to use the older start/end/both values in place of the newer trim-start/trim-end/trim-both values, respectively. In even older web browsers, you might need to use top/bottom/both. There’s no reference for this, unfortunately, so you’ll just have to see what works.

Now, where do you want to trim from?You’re probably wondering what I mean by that. Well, consider that a typographic letter has multiple peaks.

There’s the x-height, which marks the top of the letter “x” and other lowercase characters (not including ascenders or overshoots), the cap height, which marks the top of uppercase characters (again, not including ascenders or overshoots), and the alphabetic baseline, which marks the bottom of most letters (not including descenders or overshoots). Then of course there’s the ascender height and descender height too.

You can trim the whitespace between the x-height, cap height, or ascender height and the “over” edge of the text box (this is where overlines begin), and also the white space between the alphabetic baseline or descender height and the “under” edge (where underlines begin if text-underline-position is set to under).

Don’t trim anythingtext-box-edge: leading means to include all of the leading; simply don’t trim anything. This has the same effect as text-box-trim: none or forgoing text-box-trim and text-box-edge entirely. You could also restrict under-edge trimming with text-box-trim: trim-start or over edge trimming with text-box-trim: trim-end. Yep, there are quite a few ways to not even do this thing at all!

Newer web browsers have deviated from the CSSWG specification working drafts by removing the leading value and replacing it with auto, despite the “Do not ship (yet)” warning (shrug).

Naturally, text-box-edge accepts two values (an instruction regarding the over edge, then an instruction regarding the under edge). However, auto must be used solo.

text-box-edge: auto; /* Works */text-box-edge: ex auto; /* Doesn't work */text-box-edge: auto alphabetic; /* Doesn't work */ I could explain all the scenarios in which auto would work, but none of them are useful. I think all we want from auto is to be able to set the over or under edge to auto and the other edge to something else, but this is the only thing that it doesn’t do. This is a problem, but we’ll dive into that shortly.

Trim above the ascenders and/or below the descendersThe text value will trim above the ascenders if used as the first value and below the descenders if used as the second value and is also the default value if you fail to declare the second value. (I think you’d want it to be auto, but it won’t be.)

text-box-edge: ex text; /* Valid */text-box-edge: ex; /* Computed as `text-box-edge: ex text;` */text-box-edge: text alphabetic; /* Valid */text-box-edge: text text; /* Valid */text-box-edge: text; /* Computed as `text-box-edge: text text;` */ It’s worth noting that ascender and descender height metrics come from the fonts themselves (or not!), so text can be quite finicky. For example, with the Arial font, the ascender height includes diacritics and the descender height includes descenders, whereas with the Fraunces font, the descender height includes diacritics and I don’t know what the ascender height includes. For this reason, there’s talk about renaming text to from-font.

Trim above the cap height onlyTo trim above the cap height:

text-box-edge: cap; /* Computed as text-box-edge: cap text; */ Remember, undeclared values default to text, not auto (as demonstrated above). Therefore, to opt out of trimming the under edge, you’d need to use trim-start instead of trim-both:

text-box-trim: trim-start; /* Not text-box-trim: trim-both; */text-box-edge: cap; /* Not computed as text-box-edge: cap text; */ Trim above the cap height and below the alphabetic baselineTo trim above the cap height and below the alphabetic baseline:

text-box-trim: trim-both;text-box-edge: cap alphabetic; By the way, the “Cap height to baseline” option of Figma’s “Vertical trim” setting does exactly this. However, its Dev Mode produces CSS code with outdated property names (leading-trim and text-edge) and outdated values (top and bottom).

Trim above the x-height onlyTo trim above the x-height only:

text-box-trim: trim-start;text-box-edge: ex; Trim above the x-height and below the alphabetic baselineTo trim above the x-height and below the alphabetic baseline:

text-box-trim: trim-both;text-box-edge: ex alphabetic; Trim below the alphabetic baseline onlyTo trim below the alphabetic baseline only, the following won’t work (things were going so well for a moment, weren’t they?):

text-box-trim: trim-end;text-box-edge: alphabetic; This is because the first value is always the mandatory over-edge value whereas the second value is an optional under-edge value. This means that alphabetic isn’t a valid over-edge value, even though the inclusion of trim-end suggests that we won’t be providing one. Complaints about verbosity aside, the correct syntax would have you declare any over-edge value even though you’d effectively cancel it out with trim-end:

text-box-trim: trim-end;text-box-edge: [any over edge value] alphabetic; What about ideographic glyphs?It’s difficult to know how web browsers will trim ideographic glyphs until they do, but you can read all about it in the spec. In theory, you’d want to use the ideographic-ink value for trimming and the ideographic value for no trimming, both of which aren’t unsupported yet:

text-box-edge: ideographic; /* No trim */text-box-edge: ideographic-ink; /* Trim */text-box-edge: ideographic-ink ideographic; /* Top trim */text-box-edge: ideographic ideographic-ink; /* Bottom trim */ text-box, the shorthand propertyIf you’re not keen on the verbosity of text box trimming, there’s a shorthand text-box property that makes it somewhat inconsequential. All the same rules apply.

/* Syntax */text-box: [text-box-trim] [text-box-edge (over)] [text-box-edge (under)]?/* Example */text-box: trim-both cap alphabetic; Final thoughtsAt first glance, text-box-trim and text-box-edge might not seem all that interesting, but they do make spacing elements a heck of a lot simpler.

Is the current proposal the best way to handle text box trimming though? Personally, I don’t think so. I think text-box-trim-start and text-box-trim-end would make a lot more sense, with text-box-trim being used as the shorthand property and text-box-edge not being used at all, but I’d settle for some simplification and/or consistent practices. What do you think?

There are some other concerns too. For example, should there be an option to include underlines, overlines, hanging punctuation marks, or diacritics? I’m going to say yes, especially if you’re using text-underline-position: under or a particularly thick text-decoration-thickness, as they can make the spacing between elements appear smaller.


Two CSS Properties for Trimming Text Box Whitespace originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I collect a bunch of links in a bookmarks folder. These are things I fully intend to read, and I do — eventually. It’s a good thing bookmarks are digital, otherwise, I’d need a bigger coffee table to separate them from the ever-growing pile of magazines.

The benefit of accumulating links is that the virtual pile starts revealing recurring themes. Two seemingly unrelated posts published a couple months apart may congeal and become more of a dialogue around a common topic.

I spent time pouring through a pile of links I’d accumulated over the past few weeks and noticed a couple of trending topics. No, that’s not me you’re smelling — there’s an aroma of nostalgia in the air., namely a newfound focus on learning web fundamentals and some love for manual deployments.

Web Developers, AI, and Development FundamentalsAlvaro Montero:

Ultimately, it is not about AI replacing developers, but about developers adapting and evolving with the tools. The ability to learn, understand, and apply the fundamentals is essential because tools will only take you so far without the proper foundation.

ShopTalk 629: The Great Divide, Global Design + Web Components, and Job TitlesChris and Dave sound off on The Great Divide in this episode and the rising value of shifting back towards fundamentals:

Dave: But I think what is maybe missing from that is there was a very big feeling of disenfranchisement from people who are good and awesome at CSS and JavaScript and HTML. But then were being… The market was shifting hard to these all-in JavaScript frameworks. And a lot of people were like, “I don’t… This is not what I signed up for.”

[…]

Dave: Yeah. I’m sure you can be like, “Eat shit. That’s how it is, kid.” But that’s also devaluing somebody’s skillset. And I think what the market is proving now is if you know JavaScript or know HTML, CSS, and regular JavaScript (non-framework JavaScript), you are once again more valuable because you understand how a line of CSS can replace 10,000 lines of JavaScript – or whatever it is.

Chris: Yeah. Maybe it’s coming back just a smidge–

Dave: A smidge.

Chris: –that kind of respecting the fundamental stuff because there’s been churn since then, since five years ago. Now it’s like these exclusively React developers we hired, how useful are they anymore? Were they a little too limited and fundamental people are knowing more? I don’t know. It’s hard to say that the job industry is back when it doesn’t quite feel that way to me.

Dave: Yeah, yeah. Yeah, who knows. I just think the value in knowing CSS and HTML, good HTML, are up more than they maybe were five years ago.

Just a Spec: HTML Finally Gets the Respect It DeservesJared and Ayush riffin’ on the first ever State of HTML survey, why we need it, and whether “State of…” surveys are representative of people who work with HTML.

[…] once you’ve learned about divs and H’s 1 through 6, what else is there to know? Quite a lot, as it turns out. Once again, we drafted Lea Verou to put her in-depth knowledge of the web platform to work and help us craft a survey that ended up reaching far beyond pure HTML to cover accessibility, web components, and much more.

[…]

You know, it’s perfectly fine to be an expert at HTML and CSS and know very little JavaScript. So, yeah, I think it’s important to note that as we talk about the survey, because the survey is a snapshot of just the people who know about the survey and answer the questions, right? It’s not necessarily representative of the broad swath of people around the world who have used HTML at all.

[…]

So yeah, a lot of interest in HTML. I’m talking about HTML. And yeah, in the conclusion, Lea Verou talks about we really do have this big need for more extensibility of HTML.

In a more recent episode:

I’m not surprised. I mean, when someone who’s only ever used React can see what HTML does, I think it’s usually a huge revelation to them.

[…]

It just blows their minds. And it’s kind of like you just don’t know what you’re missing out on up to a point. And there is a better world out there that a lot of folks just don’t know about.

[…]

I remember a while back seeing a post come through on social media somewhere, somebody’s saying, oh, I just tried working with HTML forms, just standard HTML forms the first time and getting it to submit stuff. And wait, it’s that easy?

Yeah, last year when I was mentoring a junior developer with the Railsworld conference website, she had come through Bootcamp and only ever done React, and I was showing her what a web component does, and she’s like, oh, man, this is so cool. Yeah, it’s the web platform.

Reckoning: Part 4 — The Way OutAlex Russell in the last installment of an epic four-part series well worth your time to fully grasp the timeline, impact, and costs of modern JavsaScript frameworks to today’s development practices:

Never, ever hire for JavaScript framework skills. Instead, interview and hire only for fundamentals like web standards, accessibility, modern CSS, semantic HTML, and Web Components. This is doubly important if your system uses a framework.

Semi-Annual Reminder to Learn and Hire for Web StandardsAdrian Roselli:

This is a common cycle. Web developers tire of a particular technology — often considered the HTML killer when released — and come out of it calling for a focus on the native web platform. Then they decide to reinvent it yet again, but poorly.

There are many reasons companies won’t make deep HTML / CSS / ARIA / SVG knowledge core requirements. The simplest is the commoditization of the skills, partly because framework and library developers have looked down on the basics.

The anchor elementHeydon Pickering in a series dedicated to HTML elements, starting alphabetically with the good ol’ anchor <a>:

Sometimes, the <a> is referred to as a hyperlink, or simply a link. But it is not one of these and people who say it is one are technically wrong (the worst kind of wrong).

[…]

Web developers and content editors, the world over, make the mistake of not making text that describes a link actually go inside that link. This is collosally unfortunate, given it’s the main thing to get right when writing hypertext.

AI Myth: It lets me write code I can’t on my ownChris Ferndandi:

At the risk of being old and out-of-touch: if you don’t know how to write some code, you probably shouldn’t use code that Chat GPT et al write for you.

[…]

It’s not bulletproof, but StackOverflow provides opportunities to learn and understand the code in a way that AI-generated code does not.

What Skills Should You Focus on as Junior Web Developer in 2024?Frontend Masters:

Let’s not be old-man-shakes-fist-at-kids.gif about this, but learning the fundamentals of tech is demonstrateably useful. It’s true in basketball, it’s true for the piano, and it’s true in making websites. If you’re aiming at a long career in websites, the fundamentals are what powers it.

[…]

The point of the fundamentals is how long-lasting and transferrable the knowledge is. It will serve you well no matter what other technologies a job might have you using, or when the abstractions over them change, as they are want to do.

As long as we’re talking about learning the fundamentals…

The BasicsOh yeah, and of course there’s this little online course I released this summer for learning HTML and CSS fundamentals that I describe like this:

The Basics is more for your clients who do not know how to update the website they paid you to make. Or the friend who’s learning but still keeps bugging you with questions about the things they’re reading. Or your mom, who still has no idea what it is you do for a living. It’s for those whom the entry points are vanishing. It’s for those who could simply sign up for a Squarespace account but want to understand the code it spits out so they have more control to make a site that uniquely reflects them.

Not all this nostalgia is reserved only for HTML and CSS, but for deploying code, too. A few recent posts riff on what it might look like to ship code with “buildless” or near “buildless” workflows.

Raw-Dogging WebsitesBrad Frost:

It is extraordinarily liberating. Yes, there are some ergonomic inefficiencies, but at the end of the day it comes out in the wash. You might have to copy-and-paste some HTML, but in my experience I’d spend that much time or more debugging a broken build or dependency hell.

Going BuildlessMax Böck in a follow-up to Brad:

So, can we all ditch our build tools soon?

Probably not. I’d say for production-grade development, we’re not quite there yet. Performance tradeoffs are a big part of it, but there are lots of other small problems that you’d likely run into pretty soon once you hit a certain level of complexity.

For smaller sites or side projects though, I can imagine going the buildless route – just to see how far I can take it.

Manual ’till it hurtsJeremy Keith in a follow-up to Max:

If you’re thinking that your next project couldn’t possibly be made without a build step, let me tell you about a phrase I first heard in the indie web community: “Manual ‘till it hurts”. It’s basically a two-step process:

  1. Start doing what you need to do by hand.
  2. When that becomes unworkable, introduce some kind of automation.

It’s remarkable how often you never reach step two.

I’m not saying premature optimisation is the root of all evil. I’m just saying it’s premature.


That’s it for this pile of links and good gosh my laptop feels lighter for it. Have you read other recent posts that tread similar ground? Share ’em in the comments.


What’s Old is New originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

HTML forms come with built-in ways to validate form inputs and other controls against predefined rules such as making an input required, setting min and max constraints on range sliders, or establishing a pattern on an email input to check for proper formatting. Native HTML and browsers give us a lot of “free” features that don’t require fancy scripts to validate form submissions.

And if something doesn’t properly validate? We get “free” error messaging to display to the person using the form.

These are usually good enough to get the job done, but we may need to override these messages if we need more specific error content — especially if we need to handle translated content across browsers. Here’s how that works.

The Constraints APIThe Constraints API is used to override the default HTML form validation messages and allows us to define our own error messages. Chris Ferdinandi even covered it here on CSS-Tricks in great detail.

In short, the Constraints API is designed to provide control over input elements. The API can be called at individual input elements or directly from the form element.

For example, let’s say this simple form input is what we’re working with:

```

`` We can set our own error message by grabbing theelement and calling thesetCustomValidity()` method on it before passing it a custom message:

const fullNameInput = document.getElementById("fullName");fullNameInput.setCustomValidity("This is a custom error message"); When the submit button is clicked, the specified message will show up in place of the default one.

Translating custom form validation messagesOne major use case for customizing error messages is to better handle internationalization. There are two main ways we can approach this. There are other ways to accomplish this, but what I’m covering here is what I believe to be the most straightforward of the bunch.

Method 1: Leverage the browser’s language settingThe first method is using the browser language setting. We can get the language setting from the browser and then check whether or not we support that language. If we support the language, then we can return the translated message. And if we do not support that specific language, we provide a fallback response.

Continuing with the HTML from before, we’ll create a translation object to hold your preferred languages (within the script tags). In this case, the object supports English, Swahili, and Arabic.

const translations = { en: { required: "Please fill this", email: "Please enter a valid email address", }, sw: { required: "Sehemu hii inahitajika", email: "Tafadhali ingiza anwani sahihi ya barua pepe", }, ar: { required: "هذه الخانة مطلوبه", email: "يرجى إدخال عنوان بريد إلكتروني صالح", }}; Next, we need to extract the object’s labels and match them against the browser’s language.

// the translations objectconst supportedLangs = Object.keys(translations);const getUserLang = () => { // split to get the first part, browser is usually en-US const browserLang = navigator.language.split('-')[0]; return supportedLangs.includes(browserLang) ? browserLang :'en';};// translated error messagesconst errorMsgs = translations[getUserLang()];// form elementconst form = document.getElementById("myForm");// button elementconst btn = document.getElementById("btn");// name inputconst fullNameInput = document.getElementById("fullName");// wrapper for error messagingconst errorSpan = document.getElementById("error-span");// when the button is clicked…btn.addEventListener("click", function (event) { // if the name input is not there… if (!fullNameInput.value) { // …throw an error fullNameInput.setCustomValidity(errorMsgs.required); // set an .error class on the input for styling fullNameInput.classList.add("error"); }}); Here the getUserLang() function does the comparison and returns the supported browser language or a fallback in English. Run the example and the custom error message should display when the button is clicked.

Method 2: Setting a preferred language in local storageA second way to go about this is with user-defined language settings in localStorage. In other words, we ask the person to first select their preferred language from a <select> element containing selectable <option> tags. Once a selection is made, we save their preference to localStorage so we can reference it.

<label for="languageSelect">Choose Language:</label><select id="languageSelect"> <option value="en">English</option> <option value="sw">Swahili</option> <option value="ar">Arabic</option></select><form id="myForm"> <label for="fullName">Full Name</label> <input type="text" id="fullName" name="fullName" placeholder="Enter your full name" required> <span id="error-span"></span> <button id="btn" type="submit">Submit</button></form> With the <select> in place, we can create a script that checks localStorage and uses the saved preference to return a translated custom validation message:

// the <select> elementconst languageSelect = document.getElementById("languageSelect");// the <form> elementconst form = document.getElementById("myForm");// the button elementconst btn = document.getElementById("btn");// the name inputconst fullNameInput = document.getElementById("fullName");const errorSpan = document.getElementById("error-span");// translated custom messagesconst translations = { en: { required: "Please fill this", email: "Please enter a valid email address", }, sw: { required: "Sehemu hii inahitajika", email: "Tafadhali ingiza anwani sahihi ya barua pepe", }, ar: { required: "هذه الخانة مطلوبه", email: "يرجى إدخال عنوان بريد إلكتروني صالح", }};// the supported translations objectconst supportedLangs = Object.keys(translations);// get the language preferences from localStorageconst getUserLang = () => { const savedLang = localStorage.getItem("preferredLanguage"); if (savedLang) return savedLang; // provide a fallback message const browserLang = navigator.language.split('-')[0]; return supportedLangs.includes(browserLang) ? browserLang : 'en';};// set initial languagelanguageSelect.value = getUserLang();// update local storage when user selects a new languagelanguageSelect.addEventListener("change", () => { localStorage.setItem("preferredLanguage", languageSelect.value);});// on button clickbtn.addEventListener("click", function (event) { // take the translations const errorMsgs = translations[languageSelect.value]; // ...and if there is no value in the name input if (!fullNameInput.value) { // ...trigger the translated custom validation message fullNameInput.setCustomValidity(errorMsgs.required); // set an .error class on the input for styling fullNameInput.classList.add("error"); }}); The script sets the initial value to the currently selected option, saves that value to localStorage, and then retrieves it from localStorage as needed. Meanwhile, the script updates the selected option on every change event fired by the <select> element, all the while maintaining the original fallback to ensure a good user experience.

If we open up DevTools, we’ll see that the person’s preferred value is available in localStorage when a language preference is selected.

Wrapping upAnd with that, we’re done! I hope this quick little tip helps out. I know I wish I had it a while back when I was figuring out how to use the Constraints API. It’s one of those things on the web you know is possible, but exactly how can be tough to find.

References* Form Validation series (Chris Ferdinandi) * Meet the Pseudo Class Selectors (Chris Coyier) * Constraint validation (MDN) * Client-side form validation (MDN)


Two Ways to Create Custom Translated Messaging for HTML Forms originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I am thrilled to say, that from this week onwards, the CSS-tricks Almanac has an entry for each property, function, and at-rule related to the new Anchor Positioning API! For the last month, I have tried to fully understand this new module and explain it to the best of my ability. However, anchor positioning is still a new feature that brings even newer dynamics on how to position absolute elements, so it’s bound to have some weird quirks and maybe even a few bugs lurking around.

To celebrate the coverage, I wanted to discuss those head-scratchers I found while diving into this stuff and break them down so that hopefully, you won’t have to bang your head against the wall like I did at first.

The inset-modified containing blockA static element containing block is a fairly straightforward concept: it’s that element’s parent element’s content area. But things get tricky when talking about absolutely positioned elements. By default, an absolutely positioned element’s containing block is the viewport or the element’s closest ancestor with a position other than static, or certain values in properties like contain or filter.

All in all, the rules around an absolute element’s containing block aren’t so hard to remember. While anchor positioning and the containing block have their quirks (for example, the anchor element must be painted before the positioned element), I wanted to focus on the inset-modified containing block (which I’ll abbreviate as IMCB from here on out).

There isn’t a lot of information regarding the inset-modified containing block, and what information exists comes directly from the anchor positioning specification module. This tells me that, while it isn’t something new in CSS, it’s definitely something that has gained relevance thanks to anchor positioning.

The best explanation I could find comes directly from the spec:

For an absolutely positioned box, the inset properties effectively reduce the containing block into which it is sized and positioned by the specified amounts. The resulting rectangle is called the inset-modified containing block.

So if we inset an absolutely positioned element’s (with top, left, bottom, right, etc.), its containing block shrinks by the values on each property.

.absolute { position: absolute; top: 80px; right: 120px; bottom: 180px; left: 90px;} For this example, the element’s containing block is the full viewport, while its inset modified containing block is 80px away from the top, 120px away from the right, 180px away from the bottom, and 90px away from the left.

Knowing how the IMCB works isn’t a top priority for learning CSS, but if you want to understand anchor positioning to its fullest, it’s a must-know concept. For instance, the position-area and position-try-order heavily rely on this concept.

In the case of the position-area property, a target containing block can be broken down into a grid divided by four imaginary lines:

  1. The start of the target’s containing block.
  2. The start of the anchor element or anchor(start).
  3. The end of the anchor element or anchor(end).
  4. The end of the target’s containing block.

The position-area property uses this 3×3 imaginary grid surrounding the target to position itself inside the grid. So, if we have two elements…

```

Anchor
Target

``` …attached with anchor positioning:

.anchor { anchor-name: --my-anchor; height: 50px; width: 50px;}.target { position: absolute; position-anchor: --my-anchor; height: 50px; width: 50px;} …we can position the .target element using the position-area property:

.target { position: absolute; position-anchor: --my-anchor; position-area: top left; height: 50px; width: 50px;} The IMCB is shrunk to fit inside the region of the grid we selected, in this case, the top-left region.

You can see it by setting both target’s dimensions to 100%:

CodePen Embed FallbackThe position-try-order also uses the IMCB dimensions to decide how to order the fallbacks declared in the position-try-fallbacks property. It checks which one of the fallbacks provides the IMCB with the largest available height or width, depending on whether you set the property with either the most-height or most-width values.

I had a hard time understanding this concept, but I think it’s perfectly shown in a visual tool by Una Kravets on https://chrome.dev/anchor-tool/.

Specification vs. implementationThe spec was my best friend while I researched anchor positioning. However, theory can only take you so far, and playing with a new feature is the fun part of understanding how it works. In the case of anchor positioning, some things were written in the spec but didn’t actually work in browsers (Chromium-based browsers at the time). After staring mindlessly at my screen, I found the issue was due to something so simple I didn’t even consider it: the browser and the spec didn’t match.

Anchor positioning is different from many other features in how fast it shipped to browsers. The first draft was published on June 2023 and, just a year later, it was released on Chrome 125. To put itinto perspective, the first draft for custom properties was published in 2012 and we waited four years to see it implemented (although, Firefox shipped it years before other browsers).

I am excited to see browsers shipping new CSS features at a fast pace. While it’s awesome to get new stuff faster, it leaves less space between browsers and the CSSWG to remake features and polish existing drafts. Remember, once something is available in browsers, it’s hard to change or remove it. In the case of anchor positioning, browsers shipped certain properties and functions early on that were ultimately changed before the spec had fully settled into a Candidate Recommendation.

It’s a bit confusing, but as of Chrome 129+, this is the stuff that Chrome shipped that required changes:

position-areaThe inset-area property was renamed to position-area (#10209), but it will be supported until Chrome 131.

.target { /* from */ inset-area: top right; /* to */ position-area: top right;} position-try-fallbacksThe position-try-options was renamed to position-try-fallbacks (#10395).

.target { /* from */ position-try-options: flip-block, --smaller-target; /* to */ position-try-fallbacks: flip-block, --smaller-target;} inset-area()The inset-area() wrapper function doesn’t exist anymore for the position-try-fallbacks (#10320), you can just write the values without the wrapper.

.target { /* from */ position-try-options: inset-area(top left); /* to */ position-try-fallbacks: top left;} anchor(center)In the beginning, if we wanted to center a target from the center, we would have to write this convoluted syntax:

.target { --center: anchor(--x 50%); --half-distance: min(abs(0% - var(--center)), abs(100% - var(--center))); left: calc(var(--center) - var(--half-distance)); right: calc(var(--center) - var(--half-distance));} The CWSSG working group resolved (#8979) to add the anchor(center) argument for much-needed brevity.

.target { left: anchor(center);} Bugs!Some bugs snuck into browser implementations of anchor positioning. For example, the spec says that if an element doesn’t have a default anchor element, then the position-area property does nothing. This is a known issue (#10500) but it’s still possible to replicate, so please, just don’t do it.

The following code…

.container { position: relative;}.element { position: absolute; position-area: center; margin: auto;} …centers the .element inside its container as we can see in this demo from Temani Afif:

CodePen Embed FallbackAnother example comes from the position-visibility property. If your anchor element is off-screen, you typically want its target to be hidden as well. The spec says the default is anchors-visible, but browsers go with always instead.

Chrome currently isn’t reflecting the spec. It indeed is using always as the initial value. But the spec’s text is intentional — if your anchor is off-screen or otherwise scrolled off, you usually want it to hide (#10425).

Anchor positioning accessibilityWhile anchor positioning’s most straightforward use case is for stuff like tooltips, infoboxes, and popovers, it can be used for a lot of other stuff as well. Check this example by Silvestar Bistrović, for example, where he connects elements with lines. He’s tethered elements together for decorative purposes, so anchor positioning doesn’t mean there is a semantic relationship between the elements. As a consequence, non-visual agents, like screen readers, are left in the dark about how to interpret two seemingly unrelated elements.

If we’re aiming to link a tooltip to another element, we need to set up a relationship in the DOM and let anchor positioning handle the visuals. Happily, there are APIs (like the Popover API) that do this for us, even establishing an anchor relationship that we can take advantage of to create more compelling visuals.

In a general way, the spec describes an approach to create this relationship using ARIA attributes such as the aria-details or aria-describedby, along the role attribute on the target element.

So, while we could attach the following two elements…

```

anchor
toolip

``` …using anchor positioning:

.anchor { anchor-name: --my-anchor;}.toolip { position: absolute; position-anchor: --my-anchor; position-area: top;} …but screen readers only see two elements next to one another without any remarked relationship. That’s a bummer for accessibility, but we can easily fix it using the corresponding ARIA attribute:

```

anchor

``` And now they are both visually and semantically linked together! It would just be better if could pull it off without ARIA.

ConclusionBeing confused by a new feature just to finally understand it is one of the most satisfying experiences anyone in programming can feel. While there are still some things about anchor positioning that can be (and are) confusing, I’m pleased to say the CSS-Tricks Almanac now has a deluge of information to help clarify things.

The most exciting thing is that anchor positioning is still in an early stage. That means there are many more confusing things coming for us to discover and learn!


Anchor Positioning Quirks originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Inclusive Design 24 is in 8 short days — and it’s FREE, no sign-up required!


Quick Hit #14 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Seems like we’re always talking about clipping text around here. All it takes is a little browsing to spot a bunch of things we’ve already explored.

 **Article**  on Sep 19, 2012    Multi-line Text Overflow Ellipsis    accessibility content truncation       Chris Coyier     **Article**  on Mar 16, 2021    Better Line Breaks for Long URLs    accessibility content truncation       Reuben Lillie     **Article**  on Feb 8, 2017    Flexbox and Truncated Text    accessibility content truncation       Chris Coyier     **Article**  on Jun 20, 2017    Handling Long and Unexpected Content in CSS    accessibility content truncation       Ahmad Shadeed     **Article**  on Oct 1, 2021    Line Clampin’ (Truncating Multiple Line Text)    accessibility content truncation       Chris Coyier     **Article**  on Oct 23, 2021    Multi-Line Truncation with Pure CSS    accessibility content truncation       Chris Coyier     **Article**  on Jun 30, 2022    Text-overflow: ellipsis considered harmful    accessibility content truncation       Geoff Graham     **Article**  on Jul 21, 2020    Using Flexbox and text ellipsis together    accessibility content truncation       Chris Coyier    It’s harder than it looks! And there’s oodles of consideration that go into it! Last time I visited this, I recreated a cool-looking implementation on MDN.

CodePen Embed FallbackIn there, I noted that VoiceOver respects the full text and announces it.

I started wondering: What if I or someone else wanted to read the full text but didn’t have the assistive tech for it? It’s almost a selfish-sounding thing because of long-term muscle memory telling me I’m not the user. But I think that’s a valid request for a more inclusive experience. All folks should be able to get the full content, visually or announced.

I didn’t want to make the same demo, so I opted for something a little different that poses similar challenges, perhaps even more so. This is it:

CodePen Embed FallbackI know, not the loveliest thing ever. But it’s an interesting case study because:

  • It uses a newer (Chrome-only, at the moment) way to transition an element from zero to auto-height.
  • There’s an HTML <button> in there to trigger the auto-height transition.

This setup does what my MDN experiment doesn’t: give users without assistive tech a path to the full content. Right on, ship it! But wait…

Now VoiceOver (I’m sorry that’s all I’ve tested in) announces the button. I don’t want that. I don’t even need that because a screen reader already announces the full text. It’s not like someone who hears the text needs to expand the panel or anything. They should be able to skip it!

But should we really “hide” the button? So much conventional wisdom out there tells us that it’s terrible to hide and disable buttons. Any control that’s there for sighted readers should be present for hearing listeners as well.

If we were to simply drop disabled="true" on the button, that prevents the screen reader from pressing the button to activate something needlessly. But now we’ve created a situation where we’ve disabled something without so much as an explanation why. If I’m hearing that there’s a button on the page and it’s disabled (or dimmed), I want to know why because it sounds like I might be missing out on something even if I’m not. Plus, I don’t want to disable the button by default, especially for those who need it.

This is where “real world” Geoff would likely stop and question the pattern altogether. If something is getting this complicated, then there’s probably a straighter path I’m missing. But we’re all learners here, so I gave that other Geoff a shiny object and how he’s distracted for hours.

Let’s say we really do want to pursue this pattern and make it where the button remains in place but also gives assistive tech-ers some context. I know that the first rule of ARIA is “don’t use ARIA” but we’ve crossed that metaphorical line by deciding to use a <button>. We’re not jamming functionality into a <div> but are using a semantic element. Seems like the “right” place to reach for ARIA.

We could “hide” the button this way:

```

`` Buuuut, slappingaria-hidden="true"on a focusable element is not considered a best practice. There’s anaria-approach that’s equivalent to thedisabledattribute, only it doesn’t actuallydisable` the button when we’re not using a screen reader.

<button aria-disabled="true">Show Full Text</button> Cool, now it’s hidden! Ship it. Oh, wait again. Yes, we’ve aria-disabled the button as far as assistive tech is concerned, but it still doesn’t say why.

Still some work to do. I recently learned a bunch about ARIA after watching Sara Soueidan’s “The Other C in CSS” presentation. I’m not saying I “get” it all now, but I wanted to practice and saw this demo as a good learning exercise. I learned a couple different ways we can “describe” the button accessibly:

  • Using aria-label: If our element is interactive (which it is), we can use this to compose a custom description for the button, assuming the button’s accessible name is not enough (which it’s not).
  • Using aria-labelledby: It’s like aria-label but allows us to reference another element on the page that’s used for the button’s description.

Let’s focus on that second one for a moment. That might look something like this:

<button aria-disabled="true" aria-labelledby="notice">Show Full Text</button><span id="notice">This button is disabled since assistive tech already announces the article content.</span> The element we’re referencing has to have an id. And since an ID can only be used once a page we’ve gotta make sure this is the only instance of it, so make it unique — more unique than my lazy example. Once it’s there, though, we want to hide it because sighted folks don’t need the additional context — it’s for certain people. We can use some sort of .visually-hidden utility class to hide the text inclusively so that screen readers still see and announce it:

.visually-hidden:not(:focus):not(:active) { width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); /* for IE only */ clip-path: inset(50%); position: absolute; white-space: nowrap;} Let’s make sure we’re referencing that in the CSS:

<button aria-disabled="true" aria-labelledby="notice">Show Full Text</button><span id="notice" class="visually-hidden">This button is disabled since assistive tech already announces the article content.</span> This certainly does the trick! VoiceOver recognizes the button, calls it out, and reads the .visually-hidden notice as the button’s description.

I’m pretty sure I would ship this. That said, the markup feels heavy with hidden span. We had to intentionally create that span purely to describe the button. It’s not like that element was already on the page and we decided to recycle it. We can get away with aria-label instead without the extra baggage:

```

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quidem asperiores reprehenderit, dicta illum culpa facere qui ab dolorem suscipit praesentium nostrum delectus repellendus quas unde error numquam maxime cupiditate quaerat? ``` VoiceOver seemed to read things a little smoother this time around. For example, it read the `aria-label` content right away and announced the button and its state only once. Left to my own devices, I’d call this “baked” and, yes, finally *ship it*. But not left to my own devices, this probably isn’t up to snuff. I inspected the button again in DevTools to see how the accessibility API translates it. This looks *off* to me. I know that the button’s accessible name should come from the `aria-label` if one is available, but I also know two other ARIA attributes are designed specifically for describing elements: * **`aria-description`:** This is likely what we want! MDN describes it as “a string value that describes or annotates the current element.” Perfect for our purposes. But! MDN notes that this is still in the Editor’s Draft of the ARIA 1.3 specification. It shows up widely supported in Caniuse at the same time. It’s probably safe to use but I’m strangely conservative with features that haven’t been formally recommended and would be hesitant to ship this. I’m sure an actual accessibility practitioner would have a more informed opinion based on testing. * **`aria-describedby`:** We can leverage another element’s accessible description to describe the button just like we did with `aria-labelledby`. Cool for sure, but not what we need right here since I wouldn’t want to introduce another element only to hide it for its description. But don’t just take it from me! I’m feeling my way through this — and only partially as far as testing. This also might be a completely dumb use case — hiding text is usually a bad thing, including little excerpts like this. I’m expecting we’ll get some tips from smarter folks in the comments. Here’s that final demo once again. It’s editable, so feel free to poke around. CodePen Embed Fallback --- Another Stab at Truncated Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

View Transitions are one of the most awesome features CSS has shipped in recent times. Its title is self-explanatory: transitions between views are possible with just CSS, even across pages of the same origin! What’s more interesting is its subtext, since there is no need to create complex SPA with routing just to get those eye-catching transitions between pages.

What also makes View Transitions amazing is how quickly it has gone from its first public draft back in October 2022 to shipping in browsers and even in some production contexts like Airbnb — something that doesn’t happen to every feature coming to CSS, so it shows how rightfully hyped it is.

That said, the API is still new, so it’s bound to have some edge cases or bugs being solved as they come. An interesting way to keep up with the latest developments about CSS features like View Transitions is directly from the CSS Telecom Minutes (you can subscribe to them at W3C.org).

Full Transcription


View Transitions were the primary focus at the August 21 meeting, which had a long agenda to address. It started with a light bug in Chrome regarding the navigation descriptor, used in every cross-document view transition to opt-in to a view transition.

@view-transition { navigation: auto | none;} Currently, the specs define navigation as an enum type (a set of predefined types), but Blink takes it as a CSSOMString (any string). While this initially was passed as a bug, it’s interesting to see the conversation it sparked on the GitHub Issue:

Actually I think this is debatable, we don’t currently have at rules that use enums in that way, and usually CSSOM doesn’t try to be fully type-safe in this way. e.g. if we add new navigation types and some browsers don’t support them, this would interpret them as invalid rules rather than rules with empty navigation.

The last statement may not look exciting, but it opens the possibility of new navigation types beyond auto and none, so think about what a different type of view transition could do.

And then onto the CSSWG Minutes:

emilio: Is it useful to differentiate between missing auto or none?

noamr: Yes, very important for forward compat. If one browser adds another type that others don’t have yet, then we want to see that there’s a difference between none or invalid

emilio: But then you get auto behavior?

noamr: No, the unknown value is not read for purpose of nav. It’s a vt role without navigation descriptor and no initial value Similar to having invalid rule

So in future implementations, an invalid navigation descriptor will be ignored, but exactly how is still under debate:

ntim: How is it different from navigation none?

noamr: Auto vs invalid and then auto vs none. None would supersede auto; it has a meaning to not do a nav while invalid is a no-op.

ntim: So none cancels the nav from the prev doc?

noamr: Yes

The none has the intent to cancel any view transitions from a previous document, while an invalid or empty string will be ignored. In the end, it resolved to return an empty string if it’s missing or invalid.

RESOLVED: navigation is a CSSOMString, it returns an empty string when navigation descriptor is missing or invalid

Onto the next item on the agenda. The discussion went into the view-transition-group property and whether it should have an order of precedence. Not to confuse with the pseudo-element of the same name (::view-transition-group) the view-transition-group property was resolved to be added somewhere in the future. As of right now, the tree of pseudo-elements created by view transitions is flattened:

::view-transition├─ ::view-transition-group(name-1)│ └─ ::view-transition-image-pair(name-1)│ ├─ ::view-transition-old(name-1)│ └─ ::view-transition-new(name-1)├─ ::view-transition-group(name-2)│ └─ ::view-transition-image-pair(name-2)│ ├─ ::view-transition-old(name-2)│ └─ ::view-transition-new(name-2)│ /* and so one... */ However, we may want to nest transition groups into each other for more complex transitions, resulting in a tree with ::view-transition-group inside others ::view-transition-group, like the following:

::view-transition├─ ::view-transition-group(container-a)│ ├─ ::view-transition-group(name-1)│ └─ ::view-transition-group(name-2)└─ ::view-transition-group(container-b) ├─ ::view-transition-group(name-1) └─ ::view-transition-group(name-2) So the view-transition-group property was born, or to be precise, it will be at some point in timer. It might look something close to the following syntax if I’m following along correctly:

view-transition-group: normal | <ident> | nearest | contain; * **normal** is contained by the root ::view-transition (current behavior). * **<ident>** will be contained by an element with a matching view-transition-name * **nearest** will be contained by its nearest ancestor with view-transition-name. * **contain** will contain all its descendants without changing the element’s position in the tree

The values seem simple, but they can conflict with each other. Imagine the following nested structure:

A /* view-transition-name: foo */└─ B /* view-transition-group: contain */ └─ C /* view-transition-group: foo */ Here, B wants to contain C, but C explicitly says it wants to be contained by A. So, which wins?

vmpstr: Regarding nesting with view-transition-group, it takes keywords or ident. Contain says that all of the view-transition descendants are nested. Ident says same thing but also element itself will nest on the thing with that ident. Question is what happens if an element has a view-transition-group with a custom ident and also has an ancestor set to contain – where do we nest this? the contain one or the one with the ident? noam and I agree that ident should probably win, seems more specific.

: +1

The conversations continued if there should be a contain keyword that wins over <ident>

emilio: Agree that this seems desirable. Is there any use case for actually enforcing the containment? Do we need a strong contain? I don’t think so?

astearns: Somewhere along the line of adding a new keyword such as contain-idents?

: “contain-all”

emilio: Yeah, like sth to contain everything but needs a use case

But for now, it was set for <ident> to have more specificity than contain

PROPOSED RESOLUTION: idents take precedence over contain in view-transition-group

astearns: objections or concerns or questions?

: just as they do for <ident> values. (which also apply containment, but only to ‘normal’ elements)

RESOLVED: idents take precedence over contain in view-transition-group

Lastly, the main course of the discussion: whether or not some properties should be captured as styles instead of as a snapshot. Right now, view transitions work by taking a snapshot of the “old” view and transitioning to the “new” page. However, not everything is baked into the snapshot; some relevant properties are saved so they can be animated more carefully.

From the spec:

However, properties like mix-blend-mode which define how the element draws when it is embedded can’t be applied to its image. Such properties are applied to the element’s corresponding ::view-transition-group() pseudo-element, which is meant to generate a box equivalent to the element.

In short, some properties that depend on the element’s container are applied to the ::view-transition-group rather than ::view-transition-image-pair(). Since, in the future, we could nest groups inside groups, how we capture those properties has a lot more nuance.

noamr: Biggest issue we want to discuss today, how we capture and display nested components but also applies to non-nested view transition elements derived from the nested conversation. When we nest groups, some CSS properties that were previously not that important to capture are now very important because otherwise it looks broken. Two groups: tree effects like opacity, mask, clip-path, filters, perspective, these apply to entire tree; borders and border-radius because once you have a hierarchy of groups, and you have overflow then the overflow affects the origin where you draw the borders and shadows these also paint after backgrounds

noamr: We see three options.

  1. Change everything by default and don’t just capture snapshot but add more things that get captured as ?? instead of a flat snapshot (opacity, filter, transform, bg borders). Will change things because these styles are part of the group but have changed things before (but this is different as it changes observable computed style)
  2. Add new property view-transition-style or view-transition-capture-mode. Fan of the first as it reminds me of transform-style.
  3. To have this new property but give it auto value. If group contains other groups when you get the new mode so users using nesting get the new mode but can have a property to change the behavior If people want the old crossfade behavior they can always do so by regular DOM nesting

Regarding the first option about changing how all view transitions capture properties by default:

bramus: Yes, this would be breaking, but it would break in a good way. Regarding the name of the property, one of the values proposed is cross-fade, which is a value I wouldn’t recommend because authors can change the animation, e.g. to scale-up/ scale-down, etc. I would suggest a different name for the property, view-transition-capture-mode: flat | layered

Of course, changing how view transitions work is a dilemma to really think about:

noamr: There is some sentiment to 1 but I feel people need to think about this more?

astearns: Could resolve on option 1 and have blink try it out to see how much breakage there is and if its manageable then we’re good and come back to this. Would be resolving one 1 unless it’s not possible. I’d rather not define a new capture mode without a switch

…so the best course of action was to gather more data and decide:

khush: When we prototype we’ll find edge cases. We will take those back to the WG in that case. Want to get this right

noamr: It involves a lot of CSS props. Some of them are captured and not painted, while others are painted. The ones specifically would all be specified

After some more discussion, it was resolved to come back with compat data from browsers, you can read the full minutes at W3C.org. I bet there are a lot of interesting things I missed, so I encourage you to read it.

RESOLVED: Change the capture mode for all view-transitions and specify how each property is affected by this capture mode change

RESOLVED: Describe categorization of properties in the Module Interactions sections of each spec

RESOLVED: Blink will experiment and come back with changes needed if there are compat concerns


CSSWG Minutes Telecon (2024-08-21) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I sure do love little reminders about HTML semantics, particularly semantics that are tougher to commit to memory. Scott has a great one, beginning with this markup:

``` I am a paragraph.

I am also a paragraph.You might hate it, but I'm a paragraph too.* Even I am a paragraph. * Though I'm a list item as well.

I might trick you

Guess who? A paragraph! ```

You may look at that markup and say “Hey! You can’t fool me, only the

elements are “real” paragraphs!

You might even call out such elements as divs or spans being used as “paragraphs” a WCAG failure.

But, if you’re thinking those sorts of things, then maybe you’re not aware that those are actually all “paragraphs”.

It’s easy to forget this since many of those non-paragraph elements are not allowed in between paragraph tags and it usually gets all sorted out anyway when HTML is parsed.

The accessibility bits are what I always come to Scott’s writing for:

Those examples I provided at the start of this post? macOS VoiceOver, NVDA and JAWS treat them all as paragraphs ([asterisks] for NVDA, read on…). […] The point being that screen readers are in step with HTML, and understand that “paragraphs” are more than just the p element.


Paragraphs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Didya see that Tumblr is getting a WordPress makeover? And it’s not a trivial move:

This won’t be easy. Tumblr hosts over half a billion blogs. We’re talking about one of the largest technical migrations in internet history. Some people think it’s impossible. But we say, “challenge accepted.”

Half a billion blogs. Considering that WordPress already powers somewhere around 40% of all websites (which is much, much higher than 500m) this’ll certainly push that figure even further.

I’m sure there’s at least one suspicious nose out there catching whiffs of marketing smoke though I’m amicable to the possibility that this is a genuine move to enhance a beloved platform that’s largely seen as a past relic of the Flickr era. I loved Tumblr back then. It really embraced the whole idea that a blog can help facilitate better writing with a variety of post formats. (Post formats, fwiw, are something I always wished would be a WordPress first-class citizen but they never made it out of being an opt-in theme feature). Tumblr was the first time I was able to see blogging as more than a linear chain of content organized in reverse chronological order. Blog posts are more about what you write and how you write it than they are when they’re written.

Anyway, I know jobs are a scarce commodity in tech these days and Auttomatic is looking for folks to help with the migration.

I was about to say this “could” be a neat opportunity, but nay, it’s a super interesting and exciting opportunity, one where your work is touching two of the most influential blogging platforms on the planet. I remember interviewing Alex Hollender and Jon Robson after they shipped a design update to Wikipedia and thinking how much fun and learning would come out of a project like that. This has that same vibe to me. Buuuut, make no illusions about it: it’ll be tough.


Shipping Tumblr and WordPress originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I think it’s worth listening to anything Sara Soueidan has to say. That’s especially true if she’s speaking at an event for the first time in four years, which was the case when she took the stage at CSS Day 2024 in Amsterdam. What I enjoy most about Sara is how she not only explains the why behind everything she presents but offers it in a way that makes me go “a-ha!” instead of “oh crap, I’m doing everything wrong.”

(Oh, and you should take her course on Practical Accessibility.)

Sara’s presentation, “The Other ‘C’ in CSS”, was published on YouTube just last week. It’s roughly 55 minutes of must-see points on the various ways CSS can, and does, impact accessibility. I began watching the presentation casually but quickly fired up a place where I could take thorough notes once I found myself ooo-ing and ahhh-ing along.

So, these are the things I took away from Sara’s presentation. Let me know if you’ve also taken notes so we can compare! Here we go, there’s a lot to take in.

Here’s the videoYes, CSS affects accessibilityCSS changes more than the visual appearance of elements, whether we like it or not. More than that, its effects cascade down to HTML and the accessibility tree (accTree). And when we’re talking about the accTree, we’re referring to a list of objects that describes and defines accessible information about elements.

There are typically four main bits of info about an accTree object:

  • Role: what kind of thing is this? Most HTML elements map to ARIA roles, but not all of them.
  • Name: identifies the element in the user interface.
  • Description: how do we further describe the thing?
  • State: what is its current state? Announce it!

The browser provides interactive features — like checking a checkbox that updates and exposes the element’s information — so the user knows what happens following an interaction.

Accessibility tree objects may also contain properties and relationships, such as whether it is part of a group or labeled by another element.

Example: List semanticsCSS can affect an object’s accessible role, name, description, or even whether it is exposed in the accTree at all. As such, it can directly impact the screen reader announcement. We shared a while back how removing list-style affects list semantics, particularly in the case of Safari, and Sara explains its nuances.

/* Removes list role semantics in Safari *//* Need to add aria-role=list */ul { list-style: none;}/* Does not remove role semantics in Safari */nav ul { list-style: none:}/* Removed unless specifically re-added in the markup */ul:where([role="list"]) { list-style: none;}/* Preserves list semantics */ul { list-style: "";} display: contentsCSS can completely remove the presence of an element from the accessibility tree. I took a screenshot from one of Sara’s slides but it’s just so darn helpful that I figured putting the info in a table would be more useful:

| Exposed to a11y APIs? | Keyboard accessible? | Visually accessible (rendered)? | Children exposed to a11y APIs? | | --- | --- | --- | --- | | display: none | ❌ | ❌ | ❌ | ❌ | | visibility: hidden | ❌ | ❌ | ❌ | ❌ | | opactity: 0 and filter: opacity(0) | ✅ | ✅ | ❌ | ✅ | | clip-path: inset(100%) | ✅ | ✅ | ❌ | ✅ | | position(off-canvas) | ✅ | ✅ | ❌ | ✅ | | .visually-hidden | ✅ | ✅ | ❌ | ✅ | | display: contents | ✅ | ✅ | ❌ | ✅ |

The display: contents method does more than it’s supposed to. In short, we know that display controls the type of box an element generates. A value of none, for example, generates no box.

The contents value is sort of like none in that not box is generated. The difference is that it has no impact on the element’s children. In other words, declaring contents does not remove the element or its child elements from the accTree. More than that, there’s a current bug report saying that declaring contents in Firefox breaks the anchoring effect of an ID attribute attached to an element.

Eric Bailey says that using display: contents is considered harmful. If using it, the recommendation is to set it on a generic <div> instead of a semantically meaningful element. If we were to use it on a meaningful interactive element, it would be removed from the accTree, and its children would be bumped up to the next level in the DOM.

Visually hiding stuffMany, many of us use some sort of .visibility-hidden class as a utility for hiding elements while allowing screenreaders to pick them up and announce the contents. TPGi has a great breakdown of the technique.

.visually-hidden:not(:focus):not(:active) { width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); /* for IE only */ clip-path: inset(50%); position: absolute; white-space: nowrap;} This is super close to what I personally use in my work, but the two :not() statements were new to me and threw me for a loop. What they do is make sure that the selector only applies when the element is neither focused nor activated.

It’s easy to slap this class on things we want to hide and call it a day. But we have to be careful and use it intentionally when the situation allows for us to hide but still announce an element. For example, we would not want to use this on interactive elements because those should be displayed at all times. If you’re interacting with something, we have to be able to see it. But for generic text stuff, all good. Skip to content links, too.

There’s an exception! We may want an animated checkbox and have to hide the native control’s appearance so that it remains hidden, even though CSS is styling it in a way that it is visible. We still have to account for the form control’s different states and how it is announced to assistive tech. For example, if we hide the native checkbox for a custom one by positioning it way off the screen, the assistive tech will not announce it on focus or activation. Better to absolutely position the checkbox over the custom one to get the interactive accessibility benefits.

Bottom line: Ask yourself whether an interactive element will become visible when it receives focus when deciding whether or not to use a .visually-hidden utility.

CSS and accessible namesThe browser follows a specific process when it determines an element’s accessible name (accName):

  • First, it checks for aria-labelledby. If present, and if the ID in the attribute is a valid reference to an element on the page, it uses the reference’s element’s computed text as the element’s accessible name.
  • Otherwise, it checks for aria-label.
  • Otherwise, unless the element is marked with role="presentation" or role="none" (i.e., the element does not accept an accName anymore), the browser checks if the element can get its own name, which could happen in a few ways, including:
    • from an HTML elemnenty, such as alt or title (which is best on an <iframe>; otherwise, avoid),
    • from another element, like <label> or <legend>, or
    • from its contents.

At this point, Sara went into a brief (but wonderful) tangent on <button> semantics. Buttons are labelable elements and can get their accName by using an aria-label attribute, an aria-labelledby attribute, its contents, or even a <label> element.

ARIA takes precedence over HTML which is why we want to avoid it only where we have to. We can see the priorities and overrides for accessible names in DevTools under the Accessibility tab when inspecting elements.

But note: the order of priority defined in the accName computation algorithm does not define the order of priority that you should follow when providing an accName to elements. The steps should like be reversed if anything. Prioritize native HTML!

CSS generated contentAvoid using CSS to create meaningful content. Here’s why:

<a href="#" class="info">CSS generated content</a>

.info::before { content: "ⓘ" / "Info: "; /* or */ content: url('path-to-icon.svg') / "Info: ";}/* Contents: : Info: CSS generated content. */ But it’s more nuanced than that. For one, we’re unable to translate content generated by CSS into different languages, at least via automated tools. Another one: that content is gone if CSS is unavailable for whatever reason. I didn’t think this would ever be too big a concern until Sara reminded me that some contexts completely strip out CSS, like Safari’s Reader Mode (something I rely on practically every day, but wish I didn’t have to).

There are also edge cases where CSS generated content might be inaccessible, including in Forced Colors environments (read: color conflicts), or if a broken image is passed to the url() function (read: alt text of the image is not shown in place of the broken image, at least in most browsers, yet it still contributes to the accName, violating SC 2.5.3 Label in Name). Adrian Roselli’s article on the topic includes comprehensive test results of the new feature, showing different results.

Inline SVG is probably better! But we can also do this to help with icons that are meant to be decorative to not repeat redundant information. But it is inconsistent as far as browser implementation (but Sara says Safari gets it right).

/* like: <img src="icon.svg" alt=""> */.icon { content: url('path/to/icon.svg') / "";} So, what can we do to help prevent awkward and inaccessible situations that use CSS generated content?

  • Avoid using CSS pseudo-elements for meaningful content — use HTML!
  • Hide decorative and redundant CSS content by giving it an empty alt text (when support is there and behavior is consistent).

CSS can completely strip an element of its accName……if the source of the name is hidden in a way that removes it from the accessibility tree.

For example, an <input> can get its accName from a <label>, but that label is hidden by CSS in a way that doesn’t expose it to a11y APIs. In other words, the <label> is no longer rendered and neither are its contents, so the input winds up with no accName.

BUT! Per spec:

By default assistive technologies do not relay hidden information, but an author can explicitly override that and include hidden text as part of the accessible name or accessible description by using aria-labelledby or aria-describedby.

So, in this case, we can reuse the label even if it is hidden by tacking on aria-labelledby. We could use the .visually-hidden utility, but the label is still accessible and will continue to be announced.

CSS does not affect the state of an element in the accTreeIf we use a <button> to show/hide another element, for example, the <button> element state needs to expose that state. Content on hover or focus violates SC 1.4.13 which requires a way to dismiss the content. And users must be able to move their cursor away from the text and have it persist.

CSS-only modals using the checkbox hack are terrible because they don’t trap focus, don’t make the page content inert, and don’t manage keyboard focus (without JavaScript).

Popovers created with the Popover API are always non-modal. If you want to create a modal popover, a <dialog> is the right way to go. I’m enamored with Jhey Tompkins’s demo using the popover for a flyout navigation component, so much so that I used it in another article. But, using popover for modal-type stuff — including for something like a flyout nav — we still need to update the accessible states.

There’s much more to consider, from focus traps to inert content. But we can also consider removing the popover’s ::backdrop for fewer restrictions, like making background content inert or trapping focus. Then again, something like a popover-based flyout navigation violates SC 2.4.12 Focus Not Obscured if it covers or obscures another element with focus. So, yes, visibility is important for usability but we should shoot for better usability that goes beyond WCAG conformance. (Sara elaborates on this in a comment down below.)

So… close the popover when focus leaves it. Sara mentioned an article that Amit Sheen wrote for Smashing Magazine where it’d be wise to pay close attention to how a change is communicated to the user when a <select> menu <option> is selected to update colors on the page. That poses issues about SC 3.2.2 where something changes on input. When the user interacts with it, the user should know what’s going to happen.

Final thoughtsYeah, let all that sink in. It feels good, right? Again, what I love most about Sara’s presentation (or any of them, for that matter) is that she isn’t pointing any condemning fingers at anyone. I care about oodles accessible experiences but know just how much I don’t know, and it’s practical stuff like this where I see clear connections to my work that can make me better.

I took one more note from Sara’s talk and didn’t quite know where to put it, but I think the conclusion makes sense because it’s a solid reminder that HTML, CSS, and, yes JavaScript, all have seats at the table and can each contribute positively to accessible experience:

  • Hacking around JavaScript with CSS can introduce accessible barriers. JavasScript is still useful and required for these things. Use the right tool for the job.

The “Other” C in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I created a little library at work to make those “skeleton screens” that I’m not sure anyone likes. […] We named it skellyCSS because… skeletons and CSS, I guess. We still aren’t even really using it very much, but it was fun to do and it was the first node package I made myself (for the most part).

Regardless of whether or not anyone “likes” skeleton screens, they do come up and have their use cases. And they’re probably not something you want to rebuild time and again. Great use for a web component, I’d say! Maybe Ryan can get Uncle Dave to add it to his Awesome Standalones list. 😉

The other reason I’m sharing this link is that Ryan draws attention to the Web Components De-Mystified course that Scott Jehl recently published, something worth checking out of course, but that I needed a reminder for myself.


Introducing originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

AVIF (AV1 Image File Format) is a modern image file format specification for storing images that offer a much more significant file reduction when compared to other formats like JPG, JPEG, PNG, and WebP. Version 1.0.0 of the AVIF specification was finalized in February 2019 and released by Alliance for Open Media to the public.

You save 50% of your file size when compared to JPG and 20% compared to WebP while still maintaining the image quality.

In this article, you will learn about some browser-based tools and command-line tools for creating AVIF images.

Why use AVIF over JPGs, PNGS, WebP, and GIF?* Lossless compression and lossy compression * JPEG suffers from awful banding * WebP is much better, but there’s still noticeable blockiness compared to the AVIF * Multiple color space * 8, 10, 12-bit color depth

CaveatsJake Archibald, wrote an article a few years back on this new image format and also helped us to identify some disadvantages to compressing images, normally you should look out for these two when compressing to AVIF:

  1. If a user looks at the image in the context of the page, and it strikes them as ugly due to compression, then that level of compression is not acceptable. But, one tiny notch above that boundary is fine.
  2. It’s okay for the image to lose noticeable detail compared to the original unless that detail is significant to the context of the image.

See also: Addy Osmani at Smashing Magazine goes in-depth on using AVIF and WebP.

Browser SolutionsSquooshScreenshot of Squoosh.Squoosh is a popular image compression web app that allows you to convert images in numerous formats to other widely used compressed formats, including AVIF.

Features* File-size limit: 4MB * Image optimization settings (located on the right side) * Download controls – this includes seeing the size of the resulting file and the percentage reduction from the original image * Free to use

CloudinaryCloudinary’s free image-to-AVIF converter is another image tool that doesn’t require any form of code. All you need to do is upload your selected images (PNG, JPG, GIF, etc.) and it returns compressed versions of them. Its API even has more features besides creating AVIF images like its image enhancement and artificially generating filling for images.

I’m pretty sure you’re here because you’re looking for a free and fast converter. So, the browser solution should do.

Features

  • No stated file size limit
  • Free to use

You can find answers to common questions in the Cloudinary AVIF converter FAQ.

Command Line Solutionsavif-cli``avif-cli by lovell lets you take your images (PNG, JPEG, etc.) stored in a folder and converts them to AVIF images of your specified reduction size.

Here are the requirements and what you need to do:

  • Node.js 12.13.0+

Install the package:

npm install avif Run the command in your terminal:

npx avif --input="./imgs/*" --output="./output/" --verbose * ./imgs/* – represents the location of all your image files * ./output/ – represents the location of your output folder

CodePen Embed FallbackFeatures* Free to use * Speed of conversion can be set

You can find out about more commands via the avif-cli GitHub page.

sharpsharp is another useful tool for converting large images in common formats to smaller, web-friendly AVIF images.

Here are the requirements and what you need to do:

  • Node.js 12.13.0+

Install the package:

npm install sharp Create a JavaScript file named sharp-example.js and copy this code:

const sharp = require('sharp')const convertToAVIF = () => { sharp('path_to_image') .toFormat('avif', {palette: true}) .toFile(__dirname + 'path_to_output_image')}convertToAVIF() Where path_to_image represents the path to your image with its name and extension, i.e.:

./imgs/example.jpg And path_to_output_image represents the path you want your image to be stored with its name and new extension, i.e.:

/sharp-compressed/compressed-example.avif Run the command in your terminal:

node sharp-example.js And there! You should have a compressed AVIF file in your output location!

Features* Free to use * Images can be rotated, blurred, resized, cropped, scaled, and more using sharp

See also: Stanley Ulili’s article on How To Process Images in Node.js With Sharp.

ConclusionAVIF is a technology that front-end developers should consider for their projects. These tools allow you to convert your existing JPEG and PNG images to AVIF format. But as with adopting any new tool in your workflow, the benefits and downsides will need to be properly evaluated in accordance with your particular use case.

I hope you enjoyed reading this article as much as I enjoyed writing it. Thank you so much for your time and I hope you have a great day ahead!


Useful Tools for Creating AVIF Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Developers suffer in the great multitudes whom their sacred block-based websites cannot reach.

Johannes Gutenberg (probably)

Long time WordPresser, first time Gutenberger here. I’m a fan even though I’m still anchored to a classic/block hybrid setup. I believe Johanes himself would be, too, trading feather pens for blocks. He was a forward-thinking 15th-century inventor, after all.

My enthusiasm for Gutenberg-ness is curbed at the theming level. I’ll sling blocks all day long in the Block Editor, but please, oh please, let me keep my classic PHP templates and the Template Hierarchy that comes with it. The separation between theming and editing is one I cherish. It’s not that the Site Editor and its full-site editing capabilities scare me. It’s more that I fail to see the architectural connection between the Site and Block Editors. There’s a connection for sure, so the failure of not understanding it is more on me than WordPress.

The WP Minute published a guide that clearly — and succinctly — describes the relationships between WordPress blocks, patterns, and templates. There are plenty of other places that do the same, but this guide is organized nicely in that it starts with the blocks as the lowest-level common denominator, then builds on top of it to show how patterns are comprised of blocks used for content layout, synced patterns are the same but are one of many that are edited together, and templates are full page layouts cobbled from different patterns and a sprinkle of other “theme blocks” that are the equivalent of global components in a design system, say a main nav or a post loop.

The guide outlines it much better, of course:

  1. Gutenberg Blocks: The smallest unit of content
  2. Patterns: Collections of blocks for reuse across your site
  3. Synced Patterns: Creating “master patterns” for site-wide updates
  4. Synced Pattern Overrides: Locking patterns while allowing specific edits
  5. Templates: The structural framework of your WordPress site

That “overrides” enhancement to the synced patterns feature is new to me. I’m familiar with synced patterns (with a giant nod to Ganesh Dahal) but must’ve missed that in the WordPress 6.6 release earlier this summer.

I’m not sure when or if I’ll ever go with a truly modern WordPress full-site editing setup wholesale, out-of-the-box. I don’t feel pressured to, and I believe WordPress doesn’t care one way or another. WordPress’s ultimate selling point has always been its flexibility (driven, of course, by the massive and supportive open-source community behind it). It’s still the “right” tool for many types of projects and likely will remain so as long as it maintains its support for classic, block, and hybrid architectures.


Understanding Gutenberg Blocks, Patterns, and Templates originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Happy birthday, Chris Coyier — and thank you for CSS-Tricks as well as everything you do at CodePen, ShopTalk, Boost, and even your personal blog!


Quick Hit #13 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Giant kudos to Scott Jehl on releasing his new Web Components De-Mystified online course! Eight full hours of training from one of the best in the business.


Quick Hit #12 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Eric gifting us with his research on all the various things that anchors (not links) do when they are in :focus.

Turns out, there’s a lot!

That’s an understatement! This is an incredible amount of work, even if Eric calls it “dry as a toast sandwich.” Boring ain’t always a bad thing. Let me simply drop in a pen that Dave put together pulling all of Eric’s findings into a table organized to compare the different behaviors between operating systems — and additional tables for each specific platform — because I think it helps frame Eric’s points.

CodePen Embed FallbackThat really is a lot! But why on Earth go through the trouble of documenting all of this?

All of the previously documented behavior needs to be built in JavaScript, since we need to go the synthetic link route. It also means that it is code we need to set aside time and resources to maintain.

That also assumes that is even possible to recreate every expected feature in JavaScript, which is not true. It also leaves out the mental gymnastics required to make a business case for prioritizing engineering efforts to re-make each feature.

There’s the rub! These are the behaviors you’re gonna need to mimic and maintain if veering away from semantic, native web elements. So what Eric is generously providing is perhaps an ultimate argument against adopting frameworks — or rolling some custom system — that purposely abstract the accessible parts of the web, often in favor of DX.

As with anything, there’s more than meets the eye to all this. Eric’s got an exhaustive list at the end there that calls out all the various limitations of his research. Most of those notes sound to me like there are many, many other platforms, edge cases, user agent variations, assistive technologies, and considerations that could also be taken into account, meaning we could be responsible for a much longer list of behaviors than what’s already there.

And yes, this sweatshirt is incredible. Indeed.


Basic keyboard shortcut support for focused links originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A gem from Chris Ferdinandi that details how to use custom events to hook into Web Components. More importantly, Chris dutifully explains why custom events are a better fit than, say, callback functions.

With a typical JavaScript library, you pass callbacks in as part of the instantiate process. […] Because Web Components self-instantiate, though, there’s no easy way to do that.

There’s a way to use callback functions, just not an “easy” way to go about it.

JavaScript provides developers with a way to emit custom events that developers can listen for with the Element.addEventListener() method.

We can use custom events to let developers hook into the code that we write and run more code in response to when things happen. They provide a really flexible way to extend the functionality of a library or code base.

Don’t miss the nugget about canceling custom events!


Callbacks on Web Components? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Hey look at that, the State of CSS Survey for 2024 is open and taking submissions.


Quick Hit #11 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

You ever find yourself in bumper-to-bumper traffic? I did this morning on the way to work (read: whatever cafe I fancy). There’s a pattern to it, right? Stop, go, stop, go, stop… it’s almost rhythmic and harmonious in the most annoying of ways. Everyone in line follows the dance, led by some car upfront, each subsequent vehicle pressed right up to the rear of the next for the luxury of moving a few feet further before the next step.

Photo by Jakob JinHave you tried breaking the pattern? Instead of playing shadow to the car in front of me this morning, I allowed space between us. I’d gradually raise my right foot off the brake pedal and depress the gas pedal only once my neighboring car gained a little momentum. At that point, my car begins to crawl. And continue crawling. I rarely had to tap the brakes at all once I got going. In effect, I had sacrificed proximity for a smoother ride. I may not be traveling the “fastest” in line, but I was certainly gliding along with a lot less friction.

I find that many things in life are like that. Getting closest to anything comes with a cost, be it financial or consequence. Want the VIP ticket to a concert you’re stoked as heck about? Pony up some extra cash. Want the full story rather than a headline? Just enter your email address. Want up-to-the-second information in your stock ticker? Hand over some account information. Want access to all of today’s televised baseball games? Pick up an ESPN+ subscription.

Proximity and speed are the commodities, the products so to speak. Closer and faster are what’s being sold.

You may have run into the “law of diminishing returns” in some intro-level economics class you took in high school or college. It’s the basis for a large swath of economic theory but in essence, is the “too much of a good thing” principle. It’s what AMPM commercials have been preaching this whole time.

I’m embedding the clip instead of linking it up because it clearly illustrates the “problem” of having too many of what you want (or need). Dude resorted to asking two teens to reach into his front pocket for his wallet because his hands were full, creeper. But buy on, the commercial says, because the implication is that there’s never too much of a good thing, even if it ends in a not-so-great situation chockfull of friction.

The only and only thing I took away from physics in college — besides gravity force being 9.8 m/s2 — is that there’s no way to have bigger, cheaper, and faster at the same time. You can take two, but all three cannot play together. For example, you can have a spaceship that’s faster and cheaper, but chances are that it ain’t gonna be bigger than a typical spaceship. If you were to aim for bigger, it’d be a lot less cheap, not only for the extra size but also to make the dang heavy thing go as fast as possible. It’s a good rule in life. I don’t have proof of it, but I’d wager Mick Jagger lives by it, or at least did at one time.

Speed. Proximity. Faster and slower. Closer and further. I’m not going to draw any parallels to web development, UX design, or any other front-end thing. They’re already there.


The Intersection of Speed and Proximity originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A client asked if we could mimic the “rubber band” scrolling behavior on many mobile devices. I’m sure you know what I’m talking about. It’s a behavior that already exists and happens automatically in most browsers. In iOS Safari, for example, you’re allowed to scroll beyond the top or bottom edge of the viewport by a few hundred pixels, and letting go snaps the page back in place.

I had heard of some instances where someone might want to prevent the bounce from happening but no one had asked me to implement it, especially in a way that supports devices without a touch interface. I was actually a bit surprised there isn’t an existing CSS property for this. There’s the non-standard -webkit-overflow-scrolling property but that’s for a different type of “momentum” scrolling. Nor would I want to rely on a non-standard property that’s not on track to become part of the specifications.

OK, so what if we want to force this sort of rubber banding in our work? For starters, we’d need some sort of element acting as a container for content that requires scrolling. From there, we could reach for JavaScript, of course, but that involves adding scroll listeners or a combination of pointerDown, pointerUp, and pointerMove events, not to mention keeping track of positions, inertial movement, etc.

A CSS-only solution would be much more ideal.

Here is a container with a few child elements:

```

``` Let’s get some baseline styles in place, specifically to create a situation where we’re guaranteed to overflow a parent container.

/* Parent container with fixed dimensions for overflow */.carousel { width: 200px; height: 400px; overflow-x: hidden; overflow-y: auto;}/* Wrapper for slides, stacked in a column */.slides { display: flex; flex-direction: column; flex-wrap: wrap; width: 100%; height: fit-content;}/* Each slide is the full width of the carousel */.slide { width: 100%; aspect-ratio: 1;} Let’s start by adding some vertical margins. If your container has only one long item, add it to the top and bottom of the child element. If the container has multiple children, you’ll want to add margin to the top of the first child element and the bottom of the last child element.

.carousel > .slides > .slide:first-child { margin-top: 100px;}.carousel > .slides > .slide:last-child { margin-bottom: 100px;} Great! We can now scroll past the edges, but we need something to snap it back after the user lifts their finger or pointer. For this, we’ll need the scroll-snap-type and scroll-snap-align properties

.carousel { scroll-snap-type: y mandatory;}.carousel > .slides > .slide { scroll-snap-align: start;}.carousel > .slides > .slide:first-child { margin-top: 100px;}.carousel > .slides > .slide:last-child { scroll-snap-align: end; margin-bottom: 100px;} Note that the same applies to a horizontally scrollingelement. For that, you’d change things up so that margin is applied to the element’s left and right edges instead of its top and bottom edges. You’ll also want to change the scroll-snap-type property’s value from y mandatory to x mandatory while you’re at it.

That’s really it! Here’s the final demo:

CodePen Embed FallbackI know, I know. This isn’t some Earth-shattering or mind-blowing effect, but it does solve a very specific situation. And if you find yourself in that situation, now you have something in your back pocket to use.

Additional resources

  • “The inside story of the iconic ‘rubber band’ effect that launched the iPhone” (Cult of Mac)
  • “Six things I learnt about iOS Safari’s rubber band scrolling” (Special Agent Squeaky)
  • “Scroll Bouncing On Your Websites” (Smashing Magazine)

Elastic Overflow Scrolling originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A couple of weeks ago I was super excited about publishing my first CSS-Tricks post: “Letter Spacing is Broken. Forget about that though, what’s important is the post’s topic: letter spacing is broken and doesn’t work as the CSS Specification says it should. In a nutshell, instead of spacing the characters evenly, it leaves an unpleasant space at the end of the element.

While this inconsistency between the web and the spec is just a quirk for a Spanish/English speaker like me, for speakers of right-to-left (RTL) languages like Arabic or Hebrew, an annoying space is left at the start or end of a word. Firefox (Gecko) kinda fixes it and rearranges the unnecessary space at the end (in the reading order), but Google and Safari (Blink and Webkit) leave it at the start.

Of course, I wanted to demo this major pain point, but styling RTL content was beyond my CSS power. That’s when I found this life-saver guide by Ahmad Shadeed that covers every major aspect of styling RTL content on the web and best practices to easily internationalize an LTR webpage. A resource that, I think, is a must-read if you are interested in i18n and accessibility in the web.

I may have discovered warm water since this guide goes back to 2018, but I hope those like me who didn’t know about it have fun learning something new!


RTL Styling 101 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I can’t say I would have ever expected to see Jeremy Keith performing the Yeah Yeah Yeahs song “Maps”, but then again, I don’t know what I expected to happen at Frostapalooza.

The EventBrad Frost, web designer, author of Atomic Design, and an absolute maniac on the bass, celebrated his birthday by putting together a one-night-only benefit concert featuring musical performances by himself and his talented family and friends.

Frostapalooza, held at Mr. Smalls Theatre in Pittsburgh, PA, was an all-ages event where 100% of the proceeds are headed towards two great causes:

  • NextStep Pittsburgh: Helping provide accessible rehabilitation for folks with spinal cord injuries and paralysis in Pittsburgh.
  • Project Healthy Minds: Providing research and resources to help tackle mental health.

PerformancesThe variation of musical performances sprawled across the night, covering tracks by Fleetwood Mac, Radiohead, David Bowie and so much more, check out this setlist of all 31 tracks on Spotify.

I loved the performance of Pink Floyd’s classic song, “Money.” As a Floyd fan who will never get to see them live, this was easily the best rendition I could ask for, which included the full lineup of instrumental sections.

Brad was joined on stage by none other than CSS-Tricks founder, Chris Coyier. Chris picked banjo on a few songs, such as Johnny Cash’s “Folsom Prison Blues” and The Band’s “The Weight,” both fantastic.

The stage background prominently displayed visuals out of CodePen demos made by CodePen community members during the set. Check out the Frostapalooza tag on CodePen to see everything that was projected.

Another favorite moment was Brad’s version of “Wake Up” by Arcade Fire, which felt like a perfectly matched song for the evening.

MusiciansIf you haven’t caught on yet, many of the folks lending their musical talents to Frostapalooza also happen to be web designers and developers Brad has met and worked with during his career. At times it felt like the Wu-Tang Clan of CSS on stage.

Brad’s family and musicians from his other bands pitched in, such as Elby Brass. Ridiculously impressive! I had never seen a tuba-playing lead vocalist until this night.

You can see the full lineup on the event’s website. But I’ll drop a screenshot in here just for posterity.

Photos! Videos!Mike Aparicio captured a great video of a group jam on Queen’s “Bohemian Rhapsody” that you’ve got to watch on YouTube. Brian Kardell nabbed this gem of Chris pickin’ on “The Weight”:

The endPlain and simple, this was a super fun night celebrating music and friends. Happy birthday, Brad, and thanks for putting on an awesome show!


On the Ground at Frostapalooza originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Every programming language has loops. Loops perform an operation (i.e., a chunk of work) a number of times, usually once for every item in an array or list, or to simply repeat an operation until a certain condition is met.

JavaScript in particular has quite a few different types of loops. I haven’t even used all of them, so for my own curiosity, I thought I’d do a high-level overview of them. And as it turns out, there are pretty good reasons I haven’t used at least a couple of the different types.

So, for now let’s spend a while exploring the different types of loops, what we can do with each of one, and why you might use one over another. (You’ll think that little play on words is absolutely hilarious by the end.)

The while and do...while loopsFirst up is the while loop. It’s the most basic type of loop and has the potential to be the easiest to read and the fastest in many cases. It’s usually used for doing something until a certain condition is met. It’s also the easiest way to make an infinite loop or a loop that never stops. There is also the do...while statement. Really, the only difference is that the condition is checked at the end versus the beginning of each iteration.

// remove the first item from an array and log it until the array is emptylet queue1 = ["a", "b", "c"];while (queue1.length) { let item = queue1.shift(); console.log(item);}// same as above but also log when the array is emptylet queue2 = [];do { let item = queue2.shift() ?? "empty"; console.log(item);} while (queue2.length); The for loopNext is the for loop. It should be the go to way to do something a certain number of times. If you need to repeat an operation, say, 10 times, then use a for loop instead. This particular loop may be intimidating to those new to programming, but rewriting the same loop in the while-style loop can help illustrate the syntax make it easier to stick in your mind.

// log the numbers 1 to 5for (let i = 1; i <= 5; i++) { console.log(i);}// same thing but as a while looplet i = 1; // the first part of a for loop// the secondwhile (i <= 5) { console.log(i); i++; // the third}("end"); The for...of and for await...of loopsA for...of loop is the easiest way to loop through an array.

let myList = ["a", "b", "c"];for (let item of myList) { console.log(item);} They aren’t limited to arrays though. Technically they can iterate through anything that implements what is called an iterable protocol. There are a few built-in types that implement the protocol: arrays, maps, set, and string, to mention the most common ones, but you can implement the protocol in your own code. What you’d do is add a [Symbol.iterator] method to any object and that method should return an iterator. It’s a bit confusing, but the gist is that iterables are things with a special method that returns iterators; a factory method for iterators if you will. A special type of function called a generator is a function that returns both a iterable and iterator.

let myList = { *[Symbol.iterator]() { yield "a"; yield "b"; yield "c"; },};for (let item of myList) { console.log(item);} There is the async version of all the things I just mentioned: async iterables, async iterators, and async generators. You’d use an async iterable with for await...of.

async function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); });}// this time we're not making an iterable, but a generatorasync function* aNumberAMinute() { let i = 0; while (true) { // an infinite loop yield i++; // pause a minute await delay(60_000); }}// it's a generator, so we need to call it ourselvesfor await (let i of aNumberAMinute()) { console.log(i); // stop after one hour if (i >= 59) { break; }} One unobvious thing about for await...of statement is that you can use it with non-async iterables and it will work just fine. The reverse, however, is not true; you can’t use async iterables with the for...of statement.

The forEach and map loopsWhile these are not technically loops per se, you can use them to iterate over a list.

Here is the thing about the forEach method. Historically it was much slower than using a for loop. I think in some cases that may not be true anymore, but if performance is a concern, then I would avoid using it. And now that we have for...of I’m not sure there is much reason to use it. I guess the only reason that it still may come up is if you have a function ready to use as the callback, but you could easily just call that same function from inside the body of for...of.

forEach also receives the index for each item though, so that may be a thing you need too. Ultimately, the decision to use it will probably come down to whether any other code you’re working with uses it, but I personally would avoid using it if I’m writing something new.

let myList = ["a", "b", "c"];for (let item of myList) {console.log(item);}// but maybe if I need the index use forEach["a", "b", "c"].forEach((item, index) => { console.log(`${index}: ${item}`);}); Meanwhile, map essentially converts one array into another. It still has the same performance impact that forEach has, but it is a bit nicer to read than the alternative. It’s certainly subjective though, and just like with forEach you’ll want to do what the rest of your other code is doing. You see it a ton in React and React-inspired libraries as the primary way to loop through an array and output a list of items within JSX.

function MyList({items}) { return ( <ul> {items.map((item) => { return <li>{item}</li>; })} </ul> );} The for...in loopThis list of loops in JavaScript wouldn’t be complete without mentioning the for...in statement because it can loop through the fields of an object. It visits fields that are inherited through the object’s prototype chain too, though, and I’ve honestly always avoided it for that reason.

That said, if you have an object literal, then for...in might be a viable way to iterate through the keys of that object. Also it’s worth noting that if you’ve been programming JavaScript for a long time, you may remember that the order of keys use to be inconsistent between browsers, but now the order is consistent. Any key that could be an array index (i.e., positive integers) will be first in ascending order, and then everything else in the order as authored.

let myObject = { a: 1, b: 2, c: 3,};for (let k in myObject) { console.log(myObject[k]);} Wrapping upLoops are something that many programmers use every day, though we may take them for granted and not think about them too much.

But when you step back and look at all of the ways we have to loop through things in JavaScript, it turns out there are several ways to do it. Not only that, but there are significant — if not nuanced — differences between them that can and will influence your approach to scripts.


All About JavaScript Loops originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I was just going over the latest CSSWG minutes (you can subscribe to them at W3C.org) and came across a few interesting nuggets I wanted to jot down for another time. The group discussed the CSS Values, CSS Easing, and Selectors modules, but what really caught my eye was adding triggered delays to CSS for things like hover, long taps, and focus states.

The idea stems from an OpenUI proposal, the same group we can thank for raising things like the Popover API and customizable select element. The concept, if I understand it right, is that anytime someone hovers, taps, or focuses on, say, a <button> for a certain amount of time, we can invoke some sort of thing. A tooltip is the perfect illustration. Hovering over the trigger element, the reasoning goes, is an expression of interest and as web authors, we can do something with that interest, like displaying a tooltip.

Whoa, right?! There’s long been chatter about CSS encroaching on JavaScript territory (isn’t it ironic, don’t you think?). Firing events in response to interaction is quite literally the only thing I use JavaScript for. There’s no mistake about that in the CSSWG, as documented in the minutes:

So. Does this belong in CSS? Or should it be elsewhere? Does the approach make sense? Are there better ideas? Most interested in the last.

[…]

Other question; does this belong in CSS or HTML… maybe this is just a javascript feature? In JS you can determine MQ state and change things so it wouldn’t necessarily be in CSS.

And shortly later:

As you were talking; one thing that I kept thinking of; should developers be customizing the delay at all? Original use case for delay is that hover shouldn’t be instant. But if we don’t allow for customizing we can align to platform delay lengths.

But there’s an excellent point to be made about the way many of us are already doing this with CSS animations (animation-delay) and transitions (transition-delay). Sometimes even applying those globally with the Universal Selector or a prefers-* query.

Things get even hairier when considering how values are defined for this. Are they explicit delays (800ms), generic keywords (none/short/medium/long), a custom property, a pseudo-class… something else? I’m glad there’re incredibly smart folks noodling on this stuff.

I think here it would be good to go with time values. CSS is a good place to put it. We have all the ergonomics. The right declarative place to put it.

Whatever the eventual case may be:

I think this sounds reasonable and I’d like to explore it. Unsure if this is the exact shape, but this space seems useful to me.


CSSWG Minutes Telecon (2024-08-14) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Yes, yes. Functionally, they are different. But heck if I didn’t know about the wacky thresholds until Jens Oliver Meiert tooted a pair of quick polls.

According to the HTML Standard:

  1. If the current cell has a colspan attribute, then parse that attribute’s value, and let colspan be the result.
    If parsing that value failed, or returned zero, or if the attribute is absent, then let colspan be 1, instead.
    If colspan is greater than 1000, let it be 1000 instead.
  2. If the current cell has a rowspan attribute, then parse that attribute’s value, and let rowspan be the result.
    If parsing that value failed or if the attribute is absent, then let rowspan be 1, instead.
    If rowspan is greater than 65534, let it be 65534 instead.

I saw the answers in advance and know I’d have flubbed rowspan. Apparently, 1000 table columns are plenty of columns to span at once, while 65534 is the magic number for clamping how many rows we can span at a time. Why is the sweet spot for rowspan 6,4543 spans greater than colspan? There are usually good reasons for these things.

What that reason is, darned if I know, but now I have a little nugget for cocktail chatter in my back pocket.


How are the colspan and rowspan attributes different? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Free e-book from Jens Oliver Meiert that’ll bore you to death in the best way: Rote Learning HTML & CSS


Quick Hit #11 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Killed by Google is called a “graveyard” but I also see it as a resume in experimentation.


Quick Hit #10 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Modern CSS keeps giving us a lot of new, easier ways to solve old problems, but often the new features we’re getting don’t only solve old problems, they open up new possibilities as well.

Container queries are one of those things that open up new possibilities, but because they look a lot like the old way of doing things with media queries, our first instinct is to use them in the same way, or at least a very similar way.

When we do that, though, we aren’t taking advantage of how “smart” container queries are when compared to media queries!

Because of how important media queries were for ushering in the era of responsive web design I don’t want to say anything mean about them… but media queries are dumb. Not dumb in terms of the concept, but dumb in that they don’t know very much. In fact, most people assume that they know more than they do.

Let’s use this simple example to illustrate what I mean:

html { font-size: 32px;}body { background: lightsalmon;}@media (min-width: 35rem) { body { background: lightseagreen; }} What would the viewport size be for the background color to change? If you said 1120px wide — which is the product of multiplying 35 by 32 for those who didn’t bother doing the math — you aren’t alone in that guess, but you’d also be wrong.

Remember when I said that media queries don’t know very much? There are only two things they do know:

  • the size of the viewport, and
  • the browser’s font size.

And when I say the browser’s font size, I don’t mean the root font size in your document, which is why 1120px in the above example was wrong.

The font size they look at is the initial font size coming from the browser before any values, including the user agent styles, are applied. By default, that’s 16px, though users can change that in their browser settings.

And yes, this is on purpose. The media query specification says:

Relative length units in media queries are based on the initial value, which means that units are never based on results of declarations.

This might seem like a strange decision, but if it didn’t work that way, what would happen if we did this:

html { font-size: 16px;}@media (min-width: 30rem) { html { font-size: 32px; }} If the media query looked at the root font-size (like most assume it does), you’d run into a loop when the viewport would get to 480px wide, where the font-size would go up in size, then back down over and over again.

Container queries are a lot smarterWhile media queries have this limitation, and for good reason, container queries don’t have to worry about this type of problem and that opens up a lot of interesting possibilities!

For example, let’s say we have a grid that should be stacked at smaller sizes, but three columns at larger sizes. With media queries, we sort of have to magic number our way to the exact point where this should happen. Using a container query, we can determine the minimum size we want a column to be, and it’ll always work because we’re looking at the container size.

That means we don’t need a magic number for the breakpoint. If I want three columns with a minimum size of 300px, I know I can have three columns when the container is 900px wide. If I did that with a media query, it wouldn’t work, because when my viewport is 900px wide, my container is, more often than not, smaller than that.

But even better, we can use any unit we want as well, because container queries, unlike media queries, can look at the font size of the container itself.

To me, ch is perfect for this sort of thing. Using ch I can say “when I have enough room for each column to be a minimum of 30 characters wide, I want three columns.”

We can do the math ourselves here like this:

.grid-parent { container-type: inline-size; }.grid { display: grid; gap: 1rem; @container (width > 90ch) { grid-template-columns: repeat(3, 1fr); }} And this does work pretty well, as you can see in this example.

CodePen Embed FallbackAs another bonus, thanks to Miriam Suzanne, I recently learned that you can include calc() inside media and container queries, so instead of doing the math yourself, you can include it like this: @container (width > calc(30ch * 3)) as you can see in this example:

CodePen Embed FallbackA more practical use caseOne of the annoying things about using container queries is having to have a defined container. A container cannot query itself, so we need an extra wrapper above the element we want to select with a container query. You can see in the examples above that I needed a container on the outside of my grid for this to work.

Even more annoying is when you want grid or flex children to change their layout depending on how much space they have, only to realize that this doesn’t really work if the parent is the container. Instead of having that grid or flex container be the defined container, we end up having to wrap each grid or flex item in a container like this:

```

``` ``` .card-container { container-type: inline-size; } ``` It’s not *that* bad in the grand scheme of things, but it is kind of annoying. **Except there are ways around this!**For example, if you’re using `repeat(auto-fit, ...)` you *can* use the main grid as the container! ``` .grid-auto-fit { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(30ch, 100%)), 1fr); container-type: inline-size;} ``` Knowing that the minimum size of a column is `30ch`, we can leverage that info to restyle individual grid items depending on how many columns we have: ``` /* 2 columns + gap */@container (width > calc(30ch * 2 + 1rem)) { ... }/* 3 columns + gaps */@container (width > calc(30ch * 3 + 2rem)) { ... } ``` I’ve used this in this example to change the styles of the first child in my grid based on whether we have one, two, or three columns. CodePen Embed FallbackAnd while changing the background color of something is great for demos, we can, of course, do much more with this: CodePen Embed FallbackThe downside to this approachThe only downside I’ve found using this approach is that we can’t use custom properties for the breakpoints, which would really improve the DX of this. That should *eventually* change considering custom media queries are in the spec editor’s draft of the Media Queries Level 5 specifications, but its been in there for a while with no movement from any browsers, so it might be a long time before we can use them. And while my opinion is that having custom properties for these would both make them more readable and easier to update, it opens up enough possibilities that it’s still worth it without them. What about flexbox?With flexbox, the flex items are what define the layout, so it’s a little strange in that the sizes we apply on the items are what are important in the breakpoints. **It *can* still work, but** **there is a big issue that can arise if you do this with flexbox**. Before we look at the issue, here is a quick example of how we can get this working with flexbox: ``` .flex-container { display: flex; gap: 1rem; flex-wrap: wrap; container-type: inline-size;}.flex-container > * { /* full-width at small sizes */ flex-basis: 100%; flex-grow: 1; /* when there is room for 3 columns including gap */ @container (width > calc(200px * 3 + 2rem)) { flex-basis: calc(200px); }} ``` In this case, I used `px` to show it works as well, but you could use any unit there, as I did with the grid examples. This might look like something you can use a media query for as well — you can use the `calc()` in them too! — but this would only work in one if the parent has a width that matches the viewport width, which most of the time isn’t the case. CodePen Embed FallbackThis breaks if the flex items have paddingA lot of people don’t realize it, but the flexbox algorithm doesn’t take padding or borders into account, even if you change your `box-sizing`. If you have `padding` on your flex items, you’ll basically have to magic number your way to getting it to work. Here’s an example where I added some padding but I haven’t changed anything else, and you’ll notice one of those awkward two-columns with one stretched on the bottom layouts at one point: CodePen Embed FallbackBecause of this, I do generally find myself using this type of approach more often with grid than flexbox, but there are definitely situations where it can still work. Like before, because we’re aware of how many columns we have, we can leverage that to make more dynamic and interesting layouts depending on the space available for a given element, or elements. CodePen Embed FallbackOpening up some interesting possibilitiesI’ve only started playing around with this type of thing, and I’ve found that it’s opened up some new possibilities that we never had with media queries, and that makes me excited to see what else is possible! --- “Smart” Layouts With Container Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

You might recall that Alvaro suggests bumping up font-size to 1.25rem from the default user agent size of 16px. Sebastian Laube pokes at that:

I wouldn’t adopt Alvaro’s suggestion without further ado, as I would waste so much space on a smartphone, for example, and many users would probably be irritated by the large font.

I set a font size of 1.2rem from a certain viewport size. But this also has to be done carefully, because then grey areas arise in which media queries suddenly fall back into another area…

I personally agree with Alvaro that the default 16px size is too small. That’s just how I feel as someone who is uncomfortably close to wearing the bottoms of actual Coke bottles to see anything clearly on a screen.

On the flip side, I professionally agree with Sebastian, not that many users would probably be irritated by the large font, but to openly question an approach rather than adopting someone else’s approach wholesale based on a single blog post. It may very well be that a font-size bump is the right approach. Everything is relative, after all, and we ought to be listening to the people who use the thing we’re making for decisions like this.

The much bigger question is the one Sebastian poses right at the end there:

Should browsers perhaps use a larger font size on large screens from the outset if the user does not specify otherwise? Or do we need an information campaign to make them aware that they should check their system settings or set a different default font size in the browser?

Fantastic, right?! I’m honestly unsure where I’d draw the viewport breakpoint for 16px being either too large or small and where to start making adjustments. Is 16px the right default at any viewport size? Or perhaps user agents ought to consider a fluid type implementation that defines a default font scale and range of sizes instead? It’s great food for thought.


font-size Limbo originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Mental health is always tough to talk about, especially in an industry that, to me, often rewards ego over vulnerability. I still find it tough even after having written about my own chronic depression and exploring UX case studies about it.

But that’s exactly the sort of discussions that Schalk Venter and Schalk Neethling host on their Mental Health in Tech podcast. They invited me on the show and we got deep into what it’s like to do your best work when you’re not feeling your best. We got so deep into it that we didn’t realize two hours blew right by, and the interview was split into two parts, the second of which published today.

Vulnerability and Breaking the Facade as a Balancing Act – Geoff – Part 1 by Schalk Neethling

Read on SubstackThe Emotional Rollercoaster of Tech Layoffs, Reviving CSS-Tricks, and Recovery – Geoff – Part 2 by Schalk Neethling

Read on Substack


Mental Health in Tech Podcast Interview originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Heydon with a reminder that <address> isn’t for, you know, mailing addresses.


Quick Hit #9 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A new explainer for a new HTML attribute to handle handwritten inputs. Like this:

<input type="text" handwriting="true" ... ><input type="text" handwriting="false" ... ><textarea handwriting="" ... > <!-- evaluates to "true" --><div contenteditable handwriting="true">...</div> <!-- maybe? --> The primary use case is for those using one of those writing devices for touch screens, like Apple Pencil. The explainer comes from the Microsoft Edge team, so maybe the Surface Slim Pen is the impetus behind it. I suppose there are other use cases since the same devices that support pen-like input devices also tend to support using your finger. There are also those text box signatures in apps like DocuSign that are impossibly tough to sign with a mouse cursor, though it’s possible.

We’ll let the author, Adam Ettenberger, articulate it better than I can:

The developer authoring the drawing widget may not be aware that it may be on top of or near an input element, and it seems bad if they need to find such elements and disable handwriting on them.

⚠️Auto-playing mediaWhy an HTML attribute? The explainer outlines a few alternatives that were considered, including:

  • Extending the CSS pointer-events property (e.g. pointer-events: handwriting)
  • Extending the CSS touch-action property
  • Extending the HTML inputmode attribute

And why not a JavaScript API instead? Haha, we won’t go there.

Anyway, handwriting on the web… let’s see if we get a prototype!

A couple more pieces of context:

  • Feature: HTML handwriting attribute (Chrome Platform Status)
  • Intent to Prototype: HTML handwriting attribute (blink-dev Google Group)

Direct Link →


HTML Attribute to Allow/Disallow Handwriting Input originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Remember these? Chris would write a post now and then to chronicle things happening around the ol’ CSS-Tricks site. It’s only been 969 days since the last one, give or take. Just think: back then we were poking at writing CSS in JavaScript and juuuuuuust starting to get excited about a set of proposed new color features that are mostly implemented today. We’re nesting CSS rules now. Container queries became an actual thing.

CSS was going gosh-darned hog wild. Probably not the “best” time for a site about CSS to take a break, eh?

That’s why I thought I’d dust off the chronicles. It’s been a hot minute and a lot is happening around CSS-Tricks today.

I’m (sorta) backWe may as well begin here! Yeah, I was “let go” last year. There was no #HotDrama. A bunch of really good folks — all in the DigitalOcean community team — were let go at the same time. It was a business decision, love it or not.

Things changed at DigitalOcean after that. A new leadership team is on board and, with it, a re-dedicated focus on re-establishing the community side of things. That, and Chris published a meaty post about the CSS-Tricks situation from his perspective. Coincidentally or not, a new job opened that looked a lot like my old gig. I had feelings about that, of course.

This little flurry of activity led to a phone call. And a few more. And now I’m back to help get the ol’ CSS-Tricks engine purring, hopefully making it the rich resource we’ve loved for so long. I’m on contract at the moment and feeling things out.

So far? Man, it feels great to be back.

What I did during the “lull”I jumped over to Smashing Magazine. Gosh, that team is incredible. It tickles me that we still have Smashing Magazine. And here’s a piece of trivia for your next front-end cocktail party: Smashing Magazine was launched in September 2006, a mere 11 months before Chris published the very first article here on CSS-Tricks.

I also spent my time teaching front-end development at a couple of colleges that are local to me where I live in Colorado. I had already been teaching but bumped up the load. But not too much because I decided this was as good a time as any to work on a master’s degree. So, I enrolled and split my days as a part-time editor, part-time educator, and part-time student.

The degree went quicker than expected, so I used the rest of my time finishing up an online course I had started a couple years earlier and finally got around to publishing it! It’s probably not the sort of course for someone reading this post, but for complete beginners who are likely writing their very first line of HTML or CSS. You ever get asked how to build a website but don’t have the energy (or time) to explain everything? Yeah, me too. That’s who this course is for. And my mom.

I call it The Basics — and I’d love it if you shared it with anyone you think might use it as a starting point into web development.

What I want for CSS-Tricks, going forwardThis site’s always been great, even long before I was brought on board. Historically, it’s been more of a personal blog turned multi-author blog with a steady stream of content. Nothing wrong with that at all.

What’s lacking, though, is structure. Most everything we publish is treated like a blog post: write it, smash the Publish button, and let it sit on top of the stream until the next blog post comes out. We’re talking about a time-based approach in which posts become a timeline of activity in reverse chronological order. Where do you find that one post you came across last month? It’s probably buried by this point and you’ve gotta either hit the post archives or try your hand searching for it by keyword. That might work for a blog with a few hundred posts, but there are more than 7,000 here and searching has become more like finding the metaphorical needle in the equally metaphorical haystack.

So, you may have noticed that I’m shuffling things around. Everything is still a “post” but we’re now using a Category taxonomy more effectively than we had been in the past. Each category is a “type” of post. And the type of post is determined by what exactly we’re trying to get out of it. Let’s actually break this out into its own section because it’s a sizeable change with some explanation around it.

The “types” of things we’re publishingOK, so everything used to be an article or an Almanac entry. We still have “articles” and “entries” but there are better ways to classify and distinguish them, most notably with articles.

This is how it shakes out:

  • Articles: The tutorials that have been the CSS-Tricks bread and butter forever
  • Guides: Comprehensive deep dives into a specific CSS topic (like the Flexbox guide)
  • Almanac: Reference pieces for understanding CSS selectors and properties that can be cited in articles and guides.
  • Notes: A post for taking notes on things we’re learning. They’re meant to be loose and a little rough around the edges, just like taking notes you’d take from a class lecture — only we’re taking notes on the things that others in the community (like you!) are writing about.
  • Links: Things we’re reading that we find interesting and want to share with you. A link might evolve into a Note down the road, but they’re also useful resources that can be cited in the Almanac, a guide, or an article.
  • Quick Hits: I hate this name but the idea is to have a place to post little one-liners, like a thought, an idea, or perhaps some timely news. I’m openly accepting ideas for a better name for these. 😇

This is what we’re looking at right now, but there are obviously other ways we can slice-n-dice content. For example, we have an archive of “snippets” that we’ve buried for many years but could be useful. Same with videos. And more, if you can believe it. So, there’s plenty of housekeeping to do to keep us busy! This is still very much early days. You’ll likely experience some turbulence during your flight. And I’m okay with that because this is a learning place, and the people working it are learning, too.

Yes, I did just say, “people” as in more than one person because I’d to…

Welcome a couple of new faces!The thing that excites me most — even more than the ice cream truck excites my daughters — is bringing new people along for the ride. Running CSS-Tricks is a huge job (no matter how easy I make it look 😝). So, I’ve brought on a couple of folks to help share the load!

Juan Diego Rodriguez

Ryan Trimble

I got to know Juan Diego while editing for Smashing Magazine. He had written a couple of articles for Smashing before I joined and his latest work, the first part of a series of articles discussing the “headaches” of working with Gatsby, landed on my desk. It’s really, really good — you should check it out. What you should know about Juan Diego that I’ve come know is that the dude cares a lot about the web platform. Not only that, but pays close attention to it. I’m pretty sure he reads CSSWG specifications for pleasure over tea. His love and curiosity for all-things-front-end is infectious and I’ve already learned a bunch from him. I know you will, too.

Ryan, on the other hand, is a total nerd for design systems that advocates for accessible interfaces. He actually reached out to me on Mastodon when he caught wind that I needed help. It was perfect timing and I couldn’t be more grateful that he poked me when he did. As I’ve gotten to know him, I’m realizing how versatile his skillset is. Working with “design systems” can mean lots of different things. For Ryan, it means consistent, predictable user interfaces based on modular and reusable web components — specifically web components that are native to the platform. In fact, he’s currently working on a design system called Platform UI. I’ve also become a fan of his personal blog, especially his weekly roundups of articles he finds interesting.

You’ll be seeing a lot of Juan Diego and Ryan around here! They’re both hard at work on bringing the trusty Almanac up-to-date but will be posting articles as well. No one’s full time here, me included, so it’s truly a team effort.

Please give ’em both a hearty welcome!

This is all an ongoing work in progress…and probably always will be! I love that CSS-Tricks is a place where everyone learns together. It might be directly about CSS. Maybe it’s not. Perhaps it’s only tangentially related to web development. It may even be a rough idea that isn’t fully baked, but we put it out there and learn new things together with an open mind to the fact that the web is a massive place where everyone has something to contribute and a unique perspective we all benefit from — whether it’s from a specialization in CSS, semantics, performance, accessibility, design, typography, marketing, or what have you.

Do you wanna write for CSS-Tricks?You can and you should! You get paid, readers learn something, and that gets people coming to the site. Everybody wins!

I know writing isn’t everyone’s top skill. But that’s exactly what the team is here for. You don’t have to be a superior writer, but only be willing to write something. We’ll help polish it off and make it something you’re super proud of.

More than 200 web developers, designers, and specialists just like you have written for this site. You should apply to write an article and join the club!

So, yes: CSS-Tricks is back!In its own weird way! In my perfect world, there would be no doubt whether CSS-Tricks is publishing content on any given day. But that’s not entirely up to me. It not only has to be of at least some value to people like you who depend on sites like CSS-Tricks but also to DigitalOcean. It’s a delicate dance but I think everyone’s on the same page with a shared interest of keeping this site around and healthy.

I’m stoked I get to be a part of it. And that Juan Diego and Ryan do, too. And you, as well.

We’re all in it together. 🧡


CSS Chronicles XLII originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Ever search for CSS info and run into some article — perhaps even one or a dozen on this site — that looks promising until you realize it was published when dinosaurs roamed the planet? The information is good, but maybe isn’t the best reflection of modern best practices?

I’ve wondered that a bunch. And so has Chris. But so has Brecht De Ruyte and he’s actually doing something about it, along with other folks who make up the W3C CSS-Next community group. I worked with him on this article for Smashing Magazine and was stoked to see how much discussion, thought, and intention have gone into “versioning” CSS.

The idea? We’d “skip” CSS4, so to speak, slapping a CSS4 label on a lot of what’s released since CSS3:

CSS3 (~2009-2012):
Level 3 CSS specs as defined by the CSSWG. (immutable)

CSS4 (~2013-2018):
Essential features that were not part of CSS3 but are already a fundamental part of CSS..

From there?

CSS5 (~2019-2024):
Newer features whose adoption is steadily growing.

CSS6 (~2025+):
Early-stage features that are planned for future CSS

The most important part of the article, though, is that you (yes, you) can help the CSS-Next community group.

We also want you to participate. Anyone is welcome to join the CSS-Next group and we could certainly use help brainstorming ideas. There’s even an incubation group that conducts a biweekly hour-long session that takes place on Mondays at 8:00 a.m. Pacific Time (2:00 p.m. GMT).


It’s Time To Talk About “CSS5” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Christian Heilmann gave this talk at Typo3 Developer Days. I’m linking it up because it strikes an already stricken nerve in me. The increasing complexity of web development has an inverse relationship with the decreasing number of entry points for those getting into web development.

I love how Christian compares two hypothetical development stacks.

Thenindex.html

Now* Get the right editor with all the right extensions * Set up your terminal with the right font and all the cool dotfiles * Install framework flügelhorn.js with bundler wolperdinger.io * Go to the terminal and run packagestuff –g install * Look at all the fun warning messages and update dependencies * Doesn’t work? Go SUDO, all the cool kids are … * Don’t bother with the size of the modules folder * Learn the abstraction windfarm.css – it does make you so much more effective * Use the templating language funsocks – it is much smaller than HTML * Check out the amazing hello world example an hour later… He’s definitely a bit glib, but the point is solid. Things are more complex today than they were, say, ten years ago. I remember struggling with Grunt back then and thinking I’d never get it right. I did eventually, and my IDE was never the same after that.

It’s easy to get swept up in the complexity, even for those with experience in the field:

This world is unfortunately becoming lost or, at least, degraded — not because it is no longer possible to view the source of a webpage, but because that source is often inscrutable, even on simple webpages.

— Pixel Envy “A View Source Web”

Christian’s post reminds me that the essence of the web is not only still alive but getting better every day:

  • Browsers are constantly updated.
  • The web standardisation process is much faster than it used to be.
  • We don’t all need to build the next killer app. Many a framework promises scaling to infinity and only a few of us will ever need that.

He goes on to suggest many ways to remove complexity and abstractions from a project. My biggest takeaway is captured by a single headline:

The web is built on resilient technologies – we just don’t use them

Which recalls what Molly White said earlier this year that there’s always an opportunity to swing the pendulum back:

The thing is: none of this is gone. Nothing about the web has changed that prevents us from going back. If anything, it’s become a lot easier. We can return. Better, yet: we can restore the things we loved about the old web while incorporating the wonderful things that have emerged since, developing even better things as we go forward, and leaving behind some things from the early web days we all too often forget when we put on our rose-colored glasses.

We can return. We can restore all the things. So, tell me: do you take the red pill or the blue one?


Let’s make a simpler, more accessible web originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

It was a few years ago during the 2020 Olympics in Tokyo 2020 that I made a demo of animated 3D Olympic rings. I like it, it looks great, and I love the effect of the rings crossing each other.

CodePen Embed FallbackBut the code itself is kind of old. I wrote it in SCSS, and crookedly at that. I know it could be better, at least by modern standards.

So, I decided to build the demo again from scratch in honor of this year’s Olympics. I’m writing vanilla CSS this time, leveraging modern features like trigonometric functions for fewer magic numbers and the relative color syntax for better color management. The kicker, turns out, is that the new demo winds up being more efficient with fewer lines of code than the old SCSS version I wrote in 2020!

Look at the CSS tab in that first demo again because we’ll wind up with something vastly different — and better — with the approach we’re going to use together. So, let’s begin!

The markupWe’ll use layers to create the 3D effect. These layers are positioned one after the other (on the z-axis) to get the depth of the 3D object which, in our case, is a ring. The combination of the shape, size, and color of each layer — plus the way they vary from layer to layer — is what creates the full 3D object.

In this case, I’m using 16 layers where each layer is a different shade (with the darker layers stacked at the back) to get a simple lighting effect, and using the size and thickness of each layer to establish a round, circular shape.

As far as HTML goes, we need five <div> elements, one for each ring, where each <div> contains 16 elements that act as the layers, which I’m wrapping in <i> tags. Those five rings we’ll put in a parent container to hold things together. We’ll give the parent container a .rings class and each ring, creatively, a .ring class.

This is an abbreviated version of the HTML showing how that comes together:

```

`` Note the--icustom property I’ve dropped on thestyleattribute of each` element:

<i style="--i: 1;"></i><i style="--i: 2;"></i><i style="--i: 3;"></i><!-- etc. --> We’re going to use --i to calculate each layer’s position, size, and color. That’s why I’ve set their values as integers in ascending order — those will be multipliers for arranging and styling each layer individually.

Pro tip: You can avoid writing the HTML for each and every layer by hand if you’re working on an IDE that supports Emmet. But if not, no worries, because CodePen does! Enter the following into your HTML editor then press the Tab key on your keyboard to expand it into 16 layers: i*16[style="--i: $;"]

The (vanilla) CSSLet’s start with the parent .rings container for now will just get a relative position. Without relative positioning, the rings would be removed from the document flow and wind up off the page somewhere when setting absolute positioning on them.

.rings { position: relative;}.ring { position: absolute;} Let’s do the same with the <i> elements, but use CSS nesting to keep the code compact. We’ll throw in border-radius while we’re at it to clip the boxy edges to form perfect circles.

.rings { position: relative;}.ring { position: absolute; i { position: absolute; border-radius: 50%; }} The last piece of basic styling we’ll apply before moving on is a custom property for the --ringColor. This’ll make coloring the rings fairly straightforward because we can write it once, and then override it on a layer-by-layer basis. We’re declaring --ringColor on the border property because we only want coloration on the outer edges of each layer rather than filling them in completely with background-color:

.rings { position: relative;}.ring { position: absolute; --ringColor: #0085c7; i { position: absolute; inset: -100px; border: 16px var(--ringColor) solid; border-radius: 50%; }} Did you notice I snuck something else in there? That’s right, the inset property is also there and set to a negative value of 100px. That might look a little strange, so let’s talk about that first as we continue styling our work.

Negative insettingSetting a negative value on the inset property means that the layer’s position falls outside the .ring element. So, we might think of it more like an “outset” instead. In our case, the .ring has no size as there are no content or CSS properties to give it dimensions. That means the layer’s inset (or rather “outset”) is 100px in each direction, resulting in a .ring that is 200×200 pixels.

Let’s check in with what we have so far:

CodePen Embed FallbackPositioning for depthWe’re using the layers to create the impression of depth. We do that by positioning each of the 16 layers along the z-axis, which stacks elements from front to back. We’ll space each one a mere 2px apart — that’s all the space we need to create a slight visual separation between each layer, giving us the depth we’re after.

Remember the --i custom property we used in the HTML?

<i style="--i: 1;"></i><i style="--i: 2;"></i><i style="--i: 3;"></i><!-- etc. --> Again, those are multipliers to help us translate each layer along the z-axis. Let’s create a new custom property that defines the equation so we can apply it to each layer:

i { --translateZ: calc(var(--i) * 2px);} What do we apply it to? We can use the CSS transform property. This way, we can rotate the layers vertically (i.e., rotateY()) while translating them along the z-axis:

i { --translateZ: calc(var(--i) * 2px); transform: rotateY(-45deg) translateZ(var(--translateZ));} Color for shadingFor color shading, we’ll darken the layers according to their position so that the layers get darker as we move from the front of the z-axis to the back. There are a few ways to do it. One is dropping in another black layer with decreasing opacity. Another is modifying the “lightness” channel in a hsl() color function where the value is “lighter” up front and incrementally darker towards the back. A third option is playing with the layer’s opacity, but that gets messy.

Even though we have those three approaches, I think the modern CSS relative color syntax is the best way to go. We’ve already defined a default --ringColor custom property. We can put it through the relative color syntax to manipulate it into other colors for each ring <i> layer.

First, we need a new custom property we can use to calculate a “light” value:

.ring { --ringColor: #0085c7; i { --light: calc(var(--i) / 16); border: 16px var(--ringColor) solid; }} We’ll use the calc()-ulated result in another custom property that puts our default --ringColor through the relative color syntax where the --light custom property helps modify the resulting color’s lightness.

.ring { --ringColor: #0085c7; i { --light: calc(var(--i) / 16); --layerColor: rgb(from var(--ringColor) calc(r * var(--light)) calc(g * var(--light)) calc(b * var(--light))); border: 16px var(--ringColor) solid; }} That’s quite an equation! But it only looks complex because the relative color syntax needs arguments for each channel in the color (RGB) and we’re calculating each one.

rgb(from origin-color channelR channelG channelB) As far as the calculations go, we multiply each RGB channel by the --light custom property, which is a number between 0 and 1 divided by the number of layers, 16.

Time for another check to see where we’re at:

CodePen Embed FallbackCreating the shapeTo get the circular ring shape, we’ll set the layer’s size (i.e., thickness) with the border property. This is where we can start using trigonometry in our work!

We want the thickness of each ring to be a value between 0deg to 180deg — since we’re only actually making half of a circle — so we will divide 180deg by the number of layers, 16, which comes out to 11.25deg. Using the sin() trigonometric function (which is equivalent to the opposite and hypotenuse sides of a right angle), we get this expression for the layer’s --size:

--size: calc(sin(var(--i) * 11.25deg) * 16px); So, whatever --i is in the HTML, it acts as a multiplier for calculating the layer’s border thickness. We have been declaring the layer’s border like this:

i { border: 16px var(--ringColor) solid;) Now we can replace the hard-coded 16px value with --size calculation:

i { --size: calc(sin(var(--i) * 11.25deg) * 16px); border: var(--size) var(--layerColor) solid;) But! As you may have noticed, we aren’t changing the layer’s size when we change its border width. As a result, the round profile only appears on the layer’s inner side. The key thing here is understanding that setting the --size with the inset property which means it does not affect the element’s box-sizing. The result is a 3D ring for sure, but most of the shading is buried.

⚠️ Auto-playing mediaWe can bring the shading out by calculating a new inset for each layer. That’s kind of what I did in the 2020 version, but I think I’ve found an easier way: add an outline with the same border values to complete the arc on the outer side of the ring.

i { --size: calc(sin(var(--i) * 11.25deg) * 16px); border: var(--size) var(--layerColor) solid; outline: var(--size) var(--layerColor) solid;} We have a more natural-looking ring now that we’ve established an outline:

CodePen Embed FallbackAnimating the ringsI had to animate the ring in that last demo to compare the ring’s shading before and after. We’ll use that same animation in the final demo, so let’s break down how I did that before we add the other four rings to the HTML

I’m not trying to do anything fancy; I’m just setting the rotation on the y-axis from -45deg to 45deg (the translateZ value remains constant).

@keyframes ring { from { transform: rotateY(-45deg) translateZ(var(--translateZ, 0)); } to { transform: rotateY(45deg) translateZ(var(--translateZ, 0)); }} As for the animation property, I’ve given named it ring , and a hard-coded (at least for now) a duration of 3s, that loops infinitely. Setting the animation’s timing function with ease-in-out and alternate, respectively, gives us a smooth back-and-forth motion.

i { animation: ring 3s infinite ease-in-out alternate;} That’s how the animation works!

Adding more ringsNow we can add the remaining four rings to the HTML. Remember, we have five rings total and each ring contains 16 <i> layers. It could look as simple as this:

```

`` There’s something elegant about the simplicity of this markup. And we could use the CSSnth-child()pseudo-selector to select them individually. I like being a bit more declarative than that and am going to give each.ring` and additional class we can use to explicitly select a given ring.

```

``` Our task now is to adjust each ring individually. Right now, everything looks like the first ring we made together. We’ll use the unique classes we just set in the HTML to give them their own color, position, and animation duration.

The good news? We’ve been using custom properties this entire time! All we have to do is update the values in each ring’s unique class.

.ring { &.ring__1 { --ringColor: #0081c8; --duration: 3.2s; --translate: -240px, -40px; } &.ring__2 { --ringColor: #fcb131; --duration: 2.6s; --translate: -120px, 40px; } &.ring__3 { --ringColor: #444444; --duration: 3.0s; --translate: 0, -40px; } &.ring__4 { --ringColor: #00a651; --duration: 3.4s; --translate: 120px, 40px; } &.ring__5 { --ringColor: #ee334e; --duration: 2.8s; --translate: 240px, -40px; }} If you’re wondering where those --ringColor values came from, I based them on the International Olympic Committee’s documented colors. Each --duration is slightly offset from one another to stagger the movement between rings, and the rings are --translate‘d 120px apart and then staggered vertically by alternating their position 40px and -40px.

Let’s apply the translation stuff to the .ring elements:

.ring { transform: translate(var(--translate));} Earlier, we set the animation’s duration to a hard-coded three seconds:

i { animation: ring 3s infinite ease-in-out alternate;} This is the time to replace that with a custom property that calculates the duration for each ring separately.

i { animation: ring var(--duration) -10s infinite ease-in-out alternate;} Whoa, whoa! What’s the -10s value doing in there? Even though each ring layer is set to animate for a different duration, the starting angle of the animations is all the same. Adding a constant negative delay on changing durations will make sure that each ring’s animation starts at a different angle.

Now we have something that is almost finished:

CodePen Embed FallbackSome final touchesWe’re at the final stretch! The animation looks pretty great as-is, but I want to add two more things. The first one is a small-10deg “tilt” on the x-axis of the parent .rings container. This will make it look like we’re viewing things from a higher perspective.

.rings { rotate: x -10deg;} The second finishing touch has to do with shadows. We can really punctuate the 3D depth of our work and all it takes is selecting the .ring element’s ::after pseudo-element and styling it like a shadow.

First, we’ll set the width of the pseudos’ border and outline to a constant (24px) while setting the color to a semi-transparent black (#0003). Then we’ll translate them so they appear to be further away. We’ll also inset them so they line up with the actual rings. Basically, we’re shifting the pseudo-elements around relative to the actual element.

.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; }} The pseudos don’t look very shadow-y at the moment. But they will if we blur() them a bit:

.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; filter: blur(12px); }} The shadows are also pretty box-y. Let’s make sure they’re round like the rings:

.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; filter: blur(12px); border-radius: 50%; }} Oh, and we ought to set the same animation on the pseudo so that the shadows move in harmony with the rings:

.ring { /* etc. */ &::after { content: ''; position: absolute; inset: -100px; border: 24px #0003 solid; outline: 24px #0003 solid; translate: 0 -100px -400px; filter: blur(12px); border-radius: 50%; animation: ring var(--duration) -10s infinite ease-in-out alternate; }} Final demoLet’s stop and admire our completed work:

CodePen Embed FallbackAt the end of the day, I’m really happy with the 2024 version of the Olympic rings. The 2020 version got the job done and was probably the right approach for that time. But with all of the features we’re getting in modern CSS today, I had plenty of opportunities to improve the code so that it is not only more efficient but more reusable — for example, this could be used in another project and “themed” simply by updating the --ringColor custom property.

Ultimately, this exercise proved to me the power and flexibility of modern CSS. We took an existing idea with complexities and recreated it with simplicity and elegance.


CSS Olympic Rings originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Heydon on the virtues of hyperlinking hypertext in an anchor element:

Sometimes, the <a> is referred to as a hyperlink, or simply a link. But it is not one of these and people who say it is one are technically wrong (the worst kind of wrong).

[…]

An <a> is an interactive element (well, it is if it has an href). The text inside an interactive element is sometimes referred to as a label since it should tell you what the element does. Since anchors take you places on the web, the text should tell you where you would be going or what you can do there.

[…]

Web developers and content editors, the world over, make the mistake of not making text that describes a link actually go inside that link. This is collosally [sic] unfortunate, given it’s the main thing to get right when writing hypertext.

As far as where that anchor hyperlinks to, Jim Nielsen back in 2003 discussed a bunch of considerations that go into designing URLs. More recently, he’s mused on the the potential of well-designed URLs to change — or more accurately, the potential of humans to change things:

If a slug is going to be human-friendly, i.e. human-readable, then it’s going to contain information that is subject to change because humans make errors.

Swapping the contents of a URL is a breaking change. If we were to start with a wonderful URL like, say:

<a href=“css-tricks.com/almanac”> …but decide that we now like “Docs” instead of “Almanac” then we might do this:

<a href=“css-tricks.com/docs”> Naturally, we’d drop some sorta redirect on the server so that anyone attempting to hit /almanac is automatically directed to /docs instead. But now we’ve got a form of technical debt to maintain that may not be any more dangerous than walking and chewing gum at the same time, but could become a mouthful much later. We’ve got a gazillion redirects on CSS-Tricks for a gazillion different reasons, most often for totally human reasons like typos. Remember the CSS-Tricks Chronicles we used to write? Botching the Roman numeral numbering system on those was standard fare. Look at the very last edition from 2001, titled “CSS-Tricks Choronicles XLI” and its URL:

https://css-tricks.com/css-tricks-chronicle-xxxxi/ 🥸

I’ve been thinking about this a lot while attempting to organize the 7,000 some-odd articles on this site. For years, we’ve maintained a “flat” structure in the sense that the title of an article becomes the URL (after, perhaps, with some light editing):

<a href=“css-tricks.com/geoff-is-on-another-dumb-rant”> But I’m starting to think about the content on this site in terms of type rather than title alone. For example, we’ve always had “articles” on this site with a smattering of “links” sprinkled in alongside Almanac “entries” and “guides” among other categories of content. We’ve just never reflected that in our URLs because, well, the design is flat. Adding another layer for the type of content borks the original URL!

<a href=“css-tricks.com/soapbox/geoff-is-on-another-dumb-rant”> Jay Hoffman has been thinking about this, too.

A dead link may not seem like it means very much, even in the aggregate. But they are. One-way links, the way they exist on the web where anyone can link to anything, is what makes the web universal. In fact, the first name for URL’s was URI’s, or Universal Resource Identifier. It’s right there in the name. And as Berners-Lee once pointed out, “its universality is essential.”

[…]

Time and time again, when the web goes into crisis and part of it is lost, the Internet Archive and similar efforts come to the rescue. But even the Internet Archive is having a hard time protecting against a barrage of link rot we can’t seem to get away from.

All of this dovetails into recent reporting that Google has decided to sunset its URL shortener. All of those goo.gl URLs accumulated since the shortener was introduced in 2018?

Any developers using links built with the Google URL Shortener in the form https://goo.gl/* will be impacted, and these URLs will no longer return a response after August 25th, 2025. We recommend transitioning these links to another URL shortener provider.

There’s some minutiae of consolation for Google itself:

Note that goo.gl links generated via Google apps (such as Maps sharing) will continue to function.

To be clear, this move is less a form of link rot than it is a straight-up pruning to cut things off. If link rot is akin to allowing your hair to go gray, then deprecating Google’s URL shortener is a total head shave. Nick Heer believes there’s a good side to it, however:

In principle, I support this deprecation because it is confusing and dangerous for Google’s own shortened URLs to have the same domain as ones created by third-party users. But this is a Google-created problem because it designed its URLs poorly. It should have never been possible for anyone else to create links with the same URL shortener used by Google itself.

I tend to agree. The whole situation is a Rosemary’s Baby predicament presenting two terribly uncomfortable choices. The right uncomfortable decision was made, but we still have to deal with the repercussions of wiping out part of the web’s context.

Heydon’s post led me down this rabbit trail, so I’ll link it up here for you to take a hike with it.


(Hyper) Links About (Hyper) Links originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I have to thank Jeremy Keith and his wonderfully insightful article from late last year that introduced me to the concept of HTML Web Components. This was the “a-ha!” moment for me:

When you wrap some existing markup in a custom element and then apply some new behaviour with JavaScript, technically you’re not doing anything you couldn’t have done before with some DOM traversal and event handling. But it’s less fragile to do it with a web component. It’s portable. It obeys the single responsibility principle. It only does one thing but it does it well.

Until then, I’d been under the false assumption that all web components rely solely on the presence of JavaScript in conjunction with the rather scary-sounding Shadow DOM. While it is indeed possible to author web components this way, there is yet another way. A better way, perhaps? Especially if you, like me, advocate for progressive enhancement. HTML Web Components are, after all, just HTML.

While it’s outside the exact scope of what we’re discussing here, Andy Bell has a recent write-up that offers his (excellent) take on what progressive enhancement means.

Let’s look at three specific examples that show off what I think are the key features of HTML Web Components — CSS style encapsulation and opportunities for progressive enhancement — without being forced to depend on JavaScript to work out of the box. We will most definitely use JavaScript, but the components ought to work without it.

The examples can all be found in my Web UI Boilerplate component library (built using Storybook), along with the associated source code in GitHub.

Example 1: Live demoI really like how Chris Ferdinandi teaches building a web component from scratch, using a disclosure (show/hide) pattern as an example. This first example extends his demo.

Let’s start with the first-class citizen, HTML. Web components allow us to establish custom elements with our own naming, which is the case in this example with a tag we’re using to hold a designed to show/hide a block of text and a that holds the of text we want to show and hide.

Show / Hide

Content to be shown/hidden. If JavaScript is disabled or doesn’t execute (for any number of possible reasons), the button is hidden by default — thanks to the hidden attribute on it— and the content inside of the div is simply displayed by default.

Nice. That’s a really simple example of progressive enhancement at work. A visitor can view the content with or without the .I mentioned that this example extends Chris Ferdinandi’s initial demo. The key difference is that you can close the element either by clicking the keyboard’s ESC key or clicking anywhere outside the element. That’s what the two [data-attribute]s on the tag are for.

We start by defining the custom element so that the browser knows what to do with our made-up tag name:

`customElements.define('webui-disclosure', WebUIDisclosure);` Custom elements must be named with a dashed-ident, such as or whatever, but as Jim Neilsen notes, by way of Scott Jehl, that doesn’t exactly mean that the dash *has* to go between two words.

I typically prefer using TypeScript for writing JavaScript to help eliminate stupid errors and enforce some degree of “defensive” programming. But for the sake of simplicity, the structure of the web component’s ES Module looks like this in plain JavaScript:

`default class WebUIDisclosure extends HTMLElement { constructor() { super(); this.trigger = this.querySelector('[data-trigger]'); this.content = this.querySelector('[data-content]'); this.bindEscapeKey = this.hasAttribute('data-bind-escape-key'); this.bindClickOutside = this.hasAttribute('data-bind-click-outside'); if (!this.trigger || !this.content) return; this.setupA11y(); this.trigger?.addEventListener('click', this); } setupA11y() { // Add ARIA props/state to button. } // Handle constructor() event listeners. handleEvent(e) { // 1. Toggle visibility of content. // 2. Toggle ARIA expanded state on button. } // Handle event listeners which are not part of this Web Component. connectedCallback() { document.addEventListener('keyup', (e) => { // Handle ESC key. }); document.addEventListener('click', (e) => { // Handle clicking outside. }); } disconnectedCallback() { // Remove event listeners. }}` Are you wondering about those event listeners? The first one is defined in theconstructor()function, while the rest are in theconnectedCallback()function. Hawk Ticehurst explains the rationale much more eloquently than I can.

This JavaScript isn’t required for the web component to “work” but it does sprinkle in some nice functionality, not to mention accessibility considerations, to help with the progressive enhancement that allows the

to show and hide the content. For example, JavaScript injects the appropriatearia-expandedandaria-controls` attributes enabling those who rely on screen readers to understand the button’s purpose.That’s the progressive enhancement piece to this example.

For simplicity, I have not written any additional CSS for this component. The styling you see is simply inherited from existing global scope or component styles (e.g., typography and button).

However, the next example does have some extra scoped CSS.

Example 2: That first example lays out the progressive enhancement benefits of HTML Web Components. Another benefit we get is that CSS styles are encapsulated, which is a fancy way of saying the CSS doesn’t leak out of the component. The styles are scoped purely to the web component and those styles will not conflict with other styles applied to the current page.

Let’s turn to a second example, this time demonstrating the style encapsulating powers of web components and how they support progressive enhancement in user experiences. We’ll be using a tabbed component for organizing content in “panels” that are revealed when a panel’s corresponding tab is clicked — the same sort of thing you’ll find in many component libraries.

Live demoStarting with the HTML structure:

``` Tab 1 Tab 2 Tab 3 1 - Lorem ipsum dolor sit amet consectetur.

2 - Lorem ipsum dolor sit amet consectetur.

3 - Lorem ipsum dolor sit amet consectetur.

`` You get the idea: three links styled as tabs that, when clicked, open a tab panel holding content. Note that each[data-tab]in the tab list targets an anchor link matching a tab panel ID, e.g.,#tab1,#tab2`, etc.

We’ll look at the style encapsulation stuff first since we didn’t go there in the last example. Let’s say the CSS is organized like this:

webui-tabs { [data-tablist] { /* Default styles without JavaScript */ } [data-tab] { /* Default styles without JavaScript */ } [role='tablist'] { /* Style role added by JavaScript */ } [role='tab'] { /* Style role added by JavaScript */ } [role='tabpanel'] { /* Style role added by JavaScript */ }} See what’s happening here? We have two style rules — [data-tablist] and [data-tab] — that contain the web component’s default styles. In other words, these styles are there regardless of whether JavaScript loads or not. Meanwhile, the other three style rules are selectors that are injected into the component as long as JavaScript is enabled and supported. This way, the last three style rules are only applied if JavaScript plops the **role** attribute on those elements in the HTML. Right there, we’re already supplying a touch of progressive enhancement by setting styles only when JavasScript is needed.

All these styles are fully encapsulated, or scoped, to the web component. There is no “leakage” so to speak that would bleed into the styles of other web components, or even to anything else on the page within the global scope. We can even choose to forego classnames, complex selectors, and methodologies like BEM in favour of simple descendent selectors for the component’s children, allowing us to write styles more declaratively on semantic elements.

Quickly: “Light” DOM versus Shadow DOMFor most web projects, I generally prefer to bundle CSS (including the web component Sass partials) into a single CSS file so that the component’s default styles are available in the global scope, even if the JavaScript doesn’t execute.

However, it is possible to import a stylesheet via JavaScript that is only consumed by this web component if JavaScript is available:

import styles from './styles.css';class WebUITabs extends HTMLElement { constructor() { super(); this.adoptedStyleSheets = [styles]; }}customElements.define('webui-tabs', WebUITabs); Alternatively, we could inject a // etc.; }} customElements.define('webui-tabs', WebUITabs);`

Whichever method you choose, these styles are scoped directly to the web component, preventing component styles from leaking out, but allowing global styles to be inherited.

Now consider this simple example. Everything we write in between the component’s opening and closing tags is considered part of the “Light” DOM.

``` Some content... styles are inherited from the global scope

----------- Shadow DOM Boundary ------------- | | --------------------------------------------- ``` Dave Rupert has an excellent write-up that makes it really easy to see how external styles are able to “pierce” the Shadow DOM and select an element in the Light DOM. Notice how the

element that is written in between the custom element’s tags receives the button selector’s styles in the global CSS, while the injected via JavaScript is left untouched.If we want to style the Shadow DOM

we’d have to do that with internal styles like the examples above for importing a stylesheet or injecting an inline`

View Details

Most days, I’m writing vanilla CSS. Thanks to CSS variables and nesting, I have fewer reasons to reach for Sass or any other preprocessor. The times I reach for Sass tend to be when I need a @mixin to loop through a list of items or help keep common styles DRY.

That could change for me in the not-so-distant future since a new CSS Functions and Mixins Module draft was published in late June after the CSSWG resolved to adopt the proposal back in February.

Notice the module’s name: Functions and Mixins. There’s a distinction between the two.

This is all new and incredibly unbaked at the moment with plenty of TODO notes in the draft and points to consider in future drafts. The draft spec doesn’t even have a definition for mixins yet. It’ll likely be some time before we get something real to work and experiment with, but I like trying to wrap my mind around these sorts of things while they’re still in early days, knowing things are bound to change.

In addition to the early draft spec, Miriam Suzanne published a thorough explainer that helps plug some of the information gaps. Miriam’s an editor on the spec, so I find anything she writes about this to be useful context.

There’s a lot to read! Here are my key takeaways…

Custom functions are advanced custom propertiesWe’re not talking about the single-purpose, built-in functions we’ve come to love in recent years — e.g., calc(), min(), max(), etc. Instead, we’re talking about custom functions defined with an @function at-rule that contains logic for returning an expected value.

That makes custom functions a lot like a custom property. A custom property is merely a placeholder for some expected value that we usually define up front:

:root { --primary-color: hsl(25 100% 50%);} Custom functions look pretty similar, only they’re defined with @function and take parameters. This is the syntax currently in the draft spec:

@function <function-name> [( <parameter-list> )]? { <function-rules> result: <result>;} The result is what the ultimate value of the custom function evaluates to. It’s a little confusing to me at the moment, but how I’m processing this is that a custom function returns a custom property. Here’s an example straight from the spec draft (slightly modified) that calculates the area of a circle:

@function --circle-area(--r) { --r2: var(--r) * var(--r); result: calc(pi * var(--r2));} Calling the function is sort of like declaring a custom property, only without var() and with arguments for the defined parameters:

.element { inline-size: --circle-area(--r, 1.5rem); /* = ~7.065rem */} Seems like we could achieve the same thing as a custom property with current CSS features:

:root { --r: 1rem; --r2: var(--r) * var(--r); --circle-area: calc(pi * var(--r2));}.element { inline-size: var(--circle-area, 1.5rem);} That said, the reasons we’d reach for a custom function over a custom property are that (1) they can return one of multiple values in a single stroke, and (2) they support conditional rules, such as @supports and @media to determine which value to return. Check out Miriam’s example of a custom function that returns one of multiple values based on the inline size of the viewport.

/* Function name */@function --sizes( /* Array of possible values */ --s type(length), --m type(length), --l type(length), /* The returned value with a default */) returns type(length) { --min: 16px; /* Conditional rules */ @media (inline-size < 20em) { result: max(var(--min), var(--s, 1em)); } @media (20em < inline-size < 50em) { result: max(var(--min), var(--m, 1em + 0.5vw)); } @media (50em < inline-size) { result: max(var(--min), var(--l, 1.2em + 1vw)); }} Miriam goes on to explain how a comma-separated list of parameters like this requires additional CSSWG work because it could be mistaken as a compound selector.

Mixins help maintain DRY, reusable style blocksMixins feel more familiar to me than custom functions. Years of writing Sass mixins will do that to you, and indeed, is perhaps the primary reason I still reach for Sass every now and then.

Mixins sorta look like the new custom functions. Instead of @function we’re working with @mixin which is exactly how it works in Sass.

/* Custom function */@function <function-name> [( <parameter-list> )]? { <function-rules> result: <result>;}/* CSS/Sass mixin */@mixin <mixin-name> [( <parameter-list> )]? { <mixin-rules>} So, custom functions and mixins are fairly similar but they’re certainly different:

  • Functions are defined with @function; mixins are defined with @mixin but are both named with a dashed ident (e.g. --name).
  • Functions result in a value; mixins result in style rules.

This makes mixins ideal for abstracting styles that you might use as utility classes, say a class for hidden text that is read by screenreaders:

.sr-text { position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden;} In true utility fashion, we can sprinkle this class on elements in the HTML to hide the text.

<a class="sr-text">Skip to main content</a> Super handy! But as any Tailwind-hater will tell you, this can lead to ugly markup that’s difficult to interpret if we rely on many utility classes. Screereader text isn’t in too much danger of that, but a quick example from the Tailwind docs should illustrate that point:

```

``` It’s a matter of preference, really. But back to mixins! The deal is that we can use utility classes almost as little CSS snippets to build out other style rules and maintain a clearer separation between markup and styles. If we take the same `.sr-text` styles from before and mixin-erize them (yep, I’m coining this): ``` @mixin --sr-text { position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden;} ``` Instead of jumping into HTML to apply the styles, we can embed them in other CSS style rules with a new `@apply` at-rule: ``` header a:first-child { @apply --sr-text; /* Results in: */ position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden;} ``` Perhaps a better example is something every project seems to need: **centering something!** ``` @mixin --center-me { display: grid; place-items: center;} ``` This can now be part of a bigger ruleset: ``` header { @apply --center-me; /* display: grid; place-items: center; */ background-color: --c-blue-50; color: --c-white; /* etc. */} ``` That’s different from Sass which uses `@include` to call the mixin instead of `@apply`. We can even return larger blocks of styles, such as styles for an element’s `::before` and `::after` pseudos: ``` @mixin --center-me { display: grid; place-items: center; position: relative; &::after { background-color: hsl(25 100% 50% / .25); content: ""; height: 100%; position: absolute; width: 100%; }} ``` And, of course, we saw that mixins accept argument parameters just like custom functions. You might use arguments if you want to loosen up the styles for variations, such as defining consistent gradients with different colors: ``` @mixin --gradient-linear(--color-1, --color-2, --angle) { /* etc. */} ``` We’re able to specify the syntax for each parameter as a form of type checking: ``` @mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle),) { /* etc. */} ``` We can abstract those variables further and set default values on them: ``` @mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle),) { --from: var(--color-1, orangered); --to: var(--from-color, goldenrod); --angle: var(--at-angle, to bottom right); /* etc. */} ``` …then we write the mixin’s style rules with the parameters as variable placeholders. ``` @mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle),) { --from: var(--color-1, orangered); --to: var(--from-color, goldenrod); --angle: var(--at-angle, to bottom right); background: linear-gradient(var(--angle), var(--from), var(--to));} ``` Sprinkle conditional logic in there if you’d like: ``` @mixin --gradient-linear( --color-1 type(color), --color-2 type(color), --angle type(angle),) { --from: var(--color-1, orangered); --to: var(--from-color, goldenrod); --angle: var(--at-angle, to bottom right); background: linear-gradient(var(--angle), var(--from), var(--to)); @media (prefers-contrast: more) { background: color-mix(var(--from), black); color: white; }} ``` This is all set to `@apply` the mixin in any rulesets we want: ``` header { @apply --gradient-linear; /* etc. */}.some-class { @apply --gradient-linear; /* etc. */} ``` …and combine them with other mixins: ``` header { @apply --gradient-linear; @apply --center-me; /* etc. */} ``` This is all very high level. Miriam gets into the nuances of things like: * Applying mixins at the root level (i.e., not in a selector) * Working with Container Queries with the limitation of having to set global custom properties on another element than the one that is queried. * The possibility of conditionally setting mixin parameters with something like `@when`/`@else` in the mixin. (Which makes me wonder about the newly-proposed `if()` function and whether it would be used in place of `@when`.) * Why we might draw a line at supporting loops the same way Sass does. (CSS is a declarative language and loops are imperative flows.) * Scoping mixins (`@layer`? `scope`? Something else?) Miriam has an excellent outline of the open questions and discussions happening around mixins. That’s, um, it… at least for now.Gah, this is a lot for my blonde brain! Anytime I’m neck-deep in CSS specification drafts, I have to remind myself that the dust is still settling. The spec authors and editors are wrestling with a lot of the same questions we have — and more! — so it’s not like a cursory read of the drafts is going to make experts out of anyone. And that’s before we get to the fact that things can, and likely will, change by the time it all becomes a recommended feature for browsers to implement. This will be an interesting space to watch, which is something you can do with the following resources: * Proposal: Custom CSS Functions & Mixins (GitHub Issue #9350) * CSS Mixins *&* Functions Explainer (Miriam Suzanne) * Layered Toggles: Optional CSS Mixins (Roman Komarov) * All GitHub issues tagged `css-mixins` --- CSS Functions and Mixins Module Notes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

It’s been a few months out since A Book Apart closed shop. I’m sad about it, of course. You probably are, too, if you have one of their many brightly-colored paperbacks sitting on a bookshelf strategically placed as a backdrop for your video calls.

It looked for a bit like the books would still be available for purchase through third-party distributors who could print them on demand or whatever. And then a redaction on A Book Apart’s original announcement:

UPDATE: Ownership and publishing rights for all books have been given back to their respective authors. Many authors are continuing to offer their work for free or in new editions. Our hope is that these books will continue to live on forever. A Book Apart no longer sells or distributes books, please reach out to authors for information about availability.

Oh, snap. The books are on the loose and several authors are making sure they’re still available. Eric Meyer, for example, says he and co-author Sara Wachter-Boettcher still figuring out what’s next for their Design for Real Life title:

One of the things Sara and I have decided to do is to eventually put the entire text online for free, as a booksite. That isn’t ready yet, but it should be coming somewhere down the road.

In the meantime, we’ve decided to cut the price of print and e-book copies available through Ingram. [Design for Real Life] was the eighteenth book [A Book Apart] put out, so we’ve decided to make the price of both the print and e-book $18, regardless of whether those dollars are American, Canadian, or Australian.

Ethan Marcotte has followed suit by listing his three titles on his personal website and linking up where they can be purchased at a generous discount off the original price tag, including his latest, You Deserve a Tech Union.

Others have quickly responded with free online versions of their books. Mat Marquis has offered JavaScript for Web Designers free online for a long time. He helped Chris Coyier do the same with Practical SVG this past week. Jeremy Keith put out one of my personal ABA faves (and the first ever ABA-published book) for free, HTML5 for Web Designers.

What about all the other titles? I dunno. A Book Apart simply doesn’t sell or distribute them anymore. Rachel McConnell sells Leading Content Design directly. Every other book I checked seems to be a link back to A Book Apart. We’ll have to see where the proverbial dust settles. The authors now hold all the rights to their works and may or may not decide to re-offer them. Meanwhile, many of the titles are listed in places like Goodreads, Amazon, Barnes & Noble, etc.

A couple of folks have even started tracking the books on their personal sites, like Ryan Trimble and Alan Dalton. (Thanks for the tip, Chris!)

Thanks for all the great reads and years, A Book Apart! You’ve helped man, many people become better web citizens, present company included.


Where You Can Still Get A Book Apart Titles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Smashing Magazine invited me to sit down for a one-on-one with “Uncle” Dave Rupert to discuss web components, yes, but also check in on Dave’s new Microsoft gig and what the ShopTalk co-host is working on these days.

I first met Dave in 2015 when CSS Dev Conf took place in my backyard, Long Beach. It’s not like we’ve been in super close touch between then and now — we may have only chatted one-on-one like that a couple other times — but talking with Dave each time feels like hanging with a close friend ands this time was no different. Good, good vibes and web nerdery.

To Shared Link — Permalink on CSS-Tricks


Smashing Hour With Dave Rupert originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

This post came up following a conversation I had with Emilio Cobos — a senior developer at Mozilla and member of the CSSWG — about the last CSSWG group meeting. I wanted to know what he thought were the most exciting and interesting topics discussed at their last meeting, and with 2024 packed with so many new or coming flashy things like masonry layout, if() conditionals, anchor positioning, view transitions, and whatnot, I thought his answers had to be among them.

He admitted that my list of highlights was accurate on what is mainstream in the community, especially from an author’s point of view. However, and to my surprise, his favorite discussion was on something completely different: an inaccuracy on how the letter-spacing property is rendered across browsers. It’s a flaw so ingrained on the web that browsers have been ignoring the CSS specification for years and that can’t be easily solved by a lack of better options and compatibility issues.

Emilios’s answer makes sense — he works on Gecko and rendering fonts is an art in itself. Still, I didn’t get what the problem is exactly, why he finds it so interesting, and even why it exists in the first place since letter-spacing is a property as old as CSS. It wasn’t until I went into the letter-spacing rabbit hole that I understood how amazingly complex the issue gets and I hope to get you as interested as I did in this (not so) simple property.

What’s letter spacing?The question seems simple: letter spacing is the space between letters. Hooray! That was easy, for humans. For a computer, the question of how to render the space between letters has a lot more nuance. A human just writes the next letter without putting in much thought. Computers, on the other hand, need a strategy on how to render that space: should they add the full space at the beginning of the letter, at the end, or halve it and add it on both sides of the letter? Should it work differently from left-to-right (LTR) languages, like English, to right-to-left (RTL) like Hebrew? These questions are crucial since choosing one as a standard shapes how text measurement and line breaks work across the web.

Which of the three strategies is used on the web? Depends on who you ask. The implementation in the CSS specifications completely differs from what the browsers do, and there is even incompatibility between browsers rendering engines, like Gecko (Firefox), Blink (Chrome, Brave, Opera, etc.), and WebKit (Safari).

What the CSS spec saysLet’s backpedal a bit and first know how the spec says letter spacing should work. At the time of writing, letter-spacing:

Specifies additional spacing between typographic character units. Values may be negative, but there may be implementation-dependent limits.

The formal specification has more juice to it, but this one gives us enough to understand how the CSS spec wants letter-spacing to behave. The keyword is between, meaning that the letter spacing should only affect the space between characters. I know, sounds pretty obvious.

So, as the example given on the spec, the following HTML:

```

abbc

``` …with this CSS:

p { letter-spacing: 1em;}span { letter-spacing: 2em;} …should give an equal space between the two “b” letters:

However, if we run the same code on any browser (e.g., Chrome, Firefox, or Safari), we’ll see the spacing isn’t contained between the “b” letters, but also at the end of the complete word.

What browsers doI thought it was normal for letter-spacing to attach spacing at the end of a character and didn’t know the spec said otherwise. However, if you think about it, the current behavior does seem off… it’s just that we’re simply used to it.

Why would browsers not follow the spec on this one?

As we saw before, letter spacing isn’t straightforward for computers since they must stick to a strategy for where spacing is applied. In the case of browsers, the standard has been to apply an individual space at the end of each character, ignoring if that space goes beyond the full word. It may have not been the best choice, but it’s what the web has leaned into, and changing it now would result in all kinds of text and layout shifts across the web.

This leaves a space at the end of elements with bigger letter spacing, which is somewhat acceptable for LTR text, but it leaves a hole at the beginning of the text in an RTL writing mode.

CodePen Embed FallbackThe issue is more obvious with centered text, where the ending space pushes the text away from the element’s dead center. You’ve probably had to add padding on the opposite side of an element to make up for any letter-spacing you’ve applied to the text at least one time, like on a button.

CodePen Embed FallbackAs you can see, the blue highlight creates a symmetrical pyramid which our text sadly doesn’t follow.

What’s worse, the “end of each character” means something different to browsers, particularly when working in an RTL writing mode. Chrome and Safari (Blink/WebKit) say the end of a character is always on the right-hand side. Firefox (Gecko), on the other hand, adds space to the “reading” end — which in Hebrew and Arabic is the left-hand side. See the difference yourself:

Can this be fixed?The first thought that comes to mind is to simply follow what the spec says and trim the unnecessary space at the ending character, but this (anti) solution brings compatibility risks that are simply too big to even consider; text measurement and line breaks would change, possibly causing breakage on lots of websites. Pages that have removed that extra space with workarounds probably did it by offsetting the element’s padding/margin, which means changing the behavior as it currently stands makes those offsets obsolete or breaking.

There are two real options for how letter-spacing can be fixed: reworking how the space is distributed around the character or allowing developers an option to choose where we want the ending space.

Option 1: Reworking the space distributionThe first option would be to change the current letter-spacing definition so it says something like this:

Specifies additional spacing applied to each typographic character unit except those with zero advance. The additional spacing is divided equally between the inline-start and -end sides of the typographic character unit. Values may be negative, but there may be implementation-dependent limits.

Simply put, instead of browsers applying the additional space at the end of the character, they would divide it equally at the start and end, and the result is symmetrical text. This would also change text measurements and line breaks, albeit to a lesser degree.

Now text that is center-aligned text is correctly aligned to the center:

Option 2: Allowing developers an option to chooseEven if the offset is halved, it could still bring breaking layout shifts to pages which to some is still (rightfully) unacceptable. It’s a dilemma: most pages need, or at least would benefit, from leaving letter-spacing as-is, while new pages would enjoy symmetrical letter spacing. Luckily, we could do both by giving developers the option to choose how the space is applied to characters. The syntax is anybody’s guess, but we could have a new property to choose where to place the spacing:

letter-spacing-justify: [ before | after | left | right | between | around]; Each value represents where the space should be added, taking into account the text direction:

  • before: the spacing is added at the beginning of the letter, following the direction of the language.
  • after: the spacing is added at the end of the letter, following the direction of the language.
  • left: the spacing is added at the left of the letter, ignoring the direction of the language.
  • right: the spacing is added at the right of the letter, ignoring the direction of the language.
  • between: the spacing is added between characters, following the spec.
  • around: the spacing is divided around the letter.

Logically, the current behavior would be the default to not break anything and letter-spacing would become a shorthand for both properties (length and placing).

letter-spacing: 1px before;letter-spacing: 1px right;letter-spacing: 1px around;letter-spacing: 1px;/* same as: */letter-spacing: 1px before; What about a third option?And, of course, the third option is to leave things as they are. I’d say this is unlikely since the CSSWG resolved to take action on the issue, and they’ll probably choose the second option if I had to bet the nickel in my pocket on it.

Now you know letter-spacing is broken… and we have to live with it, at least for the time being. But there are options that may help correct the problem down the road.


Letter Spacing is Broken and There’s Nothing We Can Do About It… Maybe originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I’ve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.

You may have seen other tutorials out there showing off what the Popover API can do, but this is more of a beating-it-mercilessly-into-submission kind of article. We’ll add a little more pop ~~music~~ to the mix, like with balloons… some literal “pop” if you will.

What I’ve done is make a game — using only HTML and CSS, of course — leaning on the Popover API. You’re tasked with popping as many balloons as possible in under a minute. But be careful! Some balloons are (as Gollum would say) “tricksy” and trigger more balloons.

I have cleverly called it Pop(over) the Balloons and we’re going to make it together, step by step. When we’re done it’ll look something like (OK, exactly like) this:

CodePen Embed FallbackHandling the popover attributeAny element can be a popover as long as we fashion it with the popover attribute:

```

...

`` We don’t even have to supplypopoverwith a value. By default,popover‘s initial value isauto` and uses what the spec calls “light dismiss.” That means the popover can be closed by clicking anywhere outside of it. And when the popover opens, unless they are nested, any other popovers on the page close. Auto popovers are interdependent like that.

The other option is to set popover to a manual value:

```

...

`` …which means that the element is manually opened and closed — we literally have to click a specific button to open and close it. In other words,manual` creates an ornery popup that only closes when you hit the correct button and is completely independent of other popovers on the page.

CodePen Embed FallbackUsing the <details> element as a starterOne of the challenges of building a game with the Popover API is that you can’t load a page with a popover already open… and there’s no getting around that with JavaScript if our goal is to build the game with only HTML and CSS.

Enter the <details> element. Unlike a popover, the <details> element can be open by default:

```

`` If we pursue this route, we’re able to show a bunch of buttons (balloons) and “pop” all of them down to the very last balloon by closing the

. In other words, we can plop our starting balloons in an open
` element so they are displayed on the page on load.

This is the basic structure I’m talking about:

```

🎈

`` In this way, we can click on the balloon in

to close the
and “pop” all of the button balloons, leaving us with one balloon (the` at the end (which we’ll solve how to remove a little later).

You might think that <dialog> would be a more semantic direction for our game, and you’d be right. But there are two downsides with <dialog> that won’t let us use it here:

  1. The only way to close a <dialog> that’s open on page load is with JavaScript. As far as I know, there isn’t a close <button> we can drop in the game that will close a <dialog> that’s open on load.
  2. <dialog>s are modal and prevent clicking on other things while they’re open. We need to allow gamers to pop balloons outside of the <dialog> in order to beat the timer.

Thus we will be using a <details open> element as the game’s top-level container and using a plain ol’ <div> for the popups themselves, i.e. <div popover>.

All we need to do for the time being is make sure all of these popovers and buttons are wired together so that clicking a button opens a popover. You’ve probably learned this already from other tutorials, but we need to tell the popover element that there is a button it needs to respond to, and then tell the button that there is a popup it needs to open. For that, we give the popover element a unique ID (as all IDs should be) and then reference it on the <button> with a popovertarget attribute:

```

🎈

Level 1 Popup

``` This is the idea when everything is wired together:

CodePen Embed FallbackOpening and closing popoversThere’s a little more work to do in that last demo. One of the downsides to the game thus far is that clicking the <button> of a popup opens more popups; click that same <button> again and they disappear. This makes the game too easy.

We can separate the opening and closing behavior by setting the popovertargetaction attribute (no, the HTML spec authors were not concerned with brevity) on the <button>. If we set the attribute value to either show or hide, the <button> will only perform that one action for that specific popover.

```

🎈

Level 1 Popup

`` Note, that I’ve added a new

`` And, of course, we’ll need to add a form action for our/listpage. Actions can only go in.serverpages, so we’ll add a+page.server.jsin our/listfolder. (Yes, a+page.server.jsfile 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 updatesWhat 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:

```

``` 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 deeperWe 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 functionLet’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: ```
``` 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: ```
``` ``` 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 thoughtsThis 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.

View Details

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 conceptAs 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 GroupThat’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 yetBut 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 furtherThe 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. 😉

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.

View Details

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* Table of contents * Example 1: Expanding sidebar * Example 2: Expanding Panels * Example 3: Adding Rows and Columns * A few more examples Example 1: Expanding sidebarFirst of all, this is what we’re talking about:

CodePen Embed FallbackA 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.

```

`` Putting the cosmetic CSS aside, you’ll first need to setdisplay: gridon 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 FallbackExample 2: Expanding PanelsCodePen Embed FallbackThis 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 ColumnsCodePen Embed FallbackThis 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 FallbackA few more examplesBecause 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 FallbackSame concept, but with more of that Michelle Barker pizzazz. Could make a nice loading spinner?

CodePen Embed FallbackWrapping 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.

View Details

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 projectSpinning 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 routingNotice 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.

LayoutsWe’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:

```

`` Some rudimentary navigation with some basic styles. Of particular importance is the` 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 layoutsWhat 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:

```

This is an admin page

`` We add a red banner indicating this is an admin page and then, like before, a` 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 dataOK, 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 dataHow 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.

```

{#each todos as t} {/each}
Task Tags Assigned
{t.title} {t.tags.map((id) => tags[id].name).join(', ')} {t.assigned}

``` And this should render our to-do items!

Layout groupsBefore 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 locationsLet’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 pageWe’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.

```

Edit

`` 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.jsin/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.

```

{#each currentTags as tag} {tag.name} {/each}

`` 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 actualtag` 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 upWe’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:

  • Announcing SvelteKit 1.0 (Svelte Blog)
  • Beginner SvelteKit Course (Vercel)
  • SvelteKit Documentation

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

View Details

The :has() pseudo-class is, hands-down, my favorite new CSS feature. I know it is for many of you as well, at least those of you who took the State of CSS survey. The ability to write selectors upside down gives us more superpowers I’d never thought possible.

I say “more superpowers” because there have already been a ton of really amazing clever ideas published by a bunch of super smart people, like:

  • Using :has() as a CSS Parent Selector and much more by Jen Simmons
  • Quantity Queries for “islands of elements” with the same class, thanks to CSS :has() by Bramus
  • Style a parent element based on its number of children by Bramus
  • Using combinators in :has() by Manuel Matuzović
  • 4 ways CSS :has() can make your HTML forms even better by Austin Gil
  • Video: Practical Use Cases for :has() Pseudo-Class by Zoran Jambor
  • :has(): the family selector by Jhey Tompkins

This article is not a definitive guide to :has(). It’s also not here to regurgitate what’s already been said. It’s just me (hi 👋) jumping on the bandwagon for a moment to share some of the ways I’m most likely to use :has() in my day-to-day work… that is, once it is officially supported by Firefox which is imminent.

When that does happen, you can bet I’ll start using :has() all over the place. Here are some real-world examples of things I’ve built recently and thought to myself, “Gee, this’ll be so much nicer once :has() is fully supported.”

Avoid having to reach outside your JavaScript componentHave you ever built an interactive component that sometimes needs to affect styles somewhere else on the page? Take the following example, where <nav> is a mega menu, and opening it changes the colors of the <header> content above it.

I feel like I need to do this kind of thing all the time.

This particular example is a React component I made for a site. I had to “reach outside” the React part of the page with document.querySelector(...) and toggle a class on the <body>, <header>, or another component. That’s not the end of the world, but it sure feels a bit yuck. Even in a fully React site (a Next.js site, say), I’d have to choose between managing a menuIsOpen state way higher up the component tree, or do the same DOM element selection — which isn’t very React-y.

With :has(), the problem goes away:

header:has(.megamenu--open) { /* style the header differently if it contains an element with the class ".megamenu--open" */} No more fiddling with other parts of the DOM in my JavaScript components!

Better table striping UXAdding alternate row “stripes” to your tables can be a nice UX improvement. They help your eyes keep track of which row you’re on as you scan the table.

But in my experience, this doesn’t work great on tables with just two or three rows. If you have, for example, a table with three rows in the <tbody> and you’re “striping” every “even” row, you could end up with just one stripe. That’s not really worth a pattern and might have users wondering what’s so special about that one highlighted row.

Using this technique where Bramus uses :has() to apply styles based on the number of children, we can apply tble stripes when there are more than, say, three rows:

CodePen Embed FallbackWhat to get fancier? You could also decide to only do this if the table has at least a certain number of columns, too:

table:has(:is(td, th):nth-child(3)) { /* only do stuff if there are three or more columns */} Remove conditional class logic from templatesI often need to change a page layout depending on what’s on the page. Take the following Grid layout, where the placement of the main content changes grid areas depending on whether there’s a sidebar present.

That’s something that might depend on whether there are sibling pages set in the CMS. I’d normally do this with template logic to conditionally add BEM modifier classes to the layout wrapper to account for both layouts. That CSS might look something like this (responsive rules and other stuff omitted for brevity):

/* m = main content *//* s = sidebar */.standard-page--with-sidebar { grid-template-areas: 's s s m m m m m m m m m';}.standard-page--without-sidebar { grid-template-areas: '. m m m m m m m m m . .';} CSS-wise, this is totally fine, of course. But it does make the template code a little messy. Depending on your templating language it can get pretty ugly to conditionally add a bunch of classes, especially if you have to do this with lots of child elements too.

Contrast that with a :has()-based approach:

/* m = main content *//* s = sidebar */.standard-page:has(.sidebar) { grid-template-areas: 's s s m m m m m m m m m';}.standard-page:not(:has(.sidebar)) { grid-template-areas: '. m m m m m m m m m . .';} Honestly, that’s not a whole lot better CSS-wise. But removing the conditional modifier classes from the HTML template is a nice win if you ask me.

It’s easy to think of micro design decisions for :has() — like a card when it has an image in it — but I think it’ll be really useful for these macro layout changes too.

Better specificity managementIf you read my last article, you’ll know I’m a stickler for specificity. If, like me, you don’t want your specificity scores blowing out when adding :has() and :not() throughout your styles, be sure to use :where().

That’s because the specificity of :has() is based on the most specific element in its argument list. So, if you have something like an ID in there, your selector is going to be tough to override in the cascade.

On the other hand, the specificity of :where() is always zero, never adding to the specificity score.

/* specificity score: 0,1,0. Same as a .standard-page--with-sidebar modifier class*/.standard-page:where(:has(.sidebar)) { /* etc */} The future’s brightThese are just a few things I can’t wait to be able to use in production. The CSS-Tricks Almanac has a bunch of examples, too. What are you looking forward to doing with :has()? What sort of some real-world examples have you run into where :has() would have been the perfect solution?


More Real-World Uses for :has() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

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 importantManifest 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 2023 | June 2023 | January 2024 | | --- | --- | --- | | Support 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 V3There 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 V3I 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 structureUpdating 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 permissionsThe 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 policyIn 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 handlingThe 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 checkWhat 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.

View Details

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 3The vertical space between Heading 3 and a paragraphThe traditional solutionThe 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 FallbackThe traditional solution sounds reasonable… what’s the problem?

Rigid structureHaving 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 propertiesIn 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 marginsBecause 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() solutionAnd here is my attempt at solving these issues with :has().

CodePen Embed FallbackTo 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 structureI 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 upSo 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.

View Details

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 valuesThe 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> ReframingSo, 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.

ZoomingWe 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 1Figure 2There 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 Fallback2. Missing width and heightAnother 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).

ChromeSafariFirefoxThe 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 Fallback3. Inadvertent fill and stroke colorsIt 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.

  1. Missing IDsThis 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.

  1. Checklist for clipping and maskingIf 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.

  1. NamespacesDid 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:

  • Using SVG with CSS3 and HTML5 (Amelia Bellamy-Royds, Kurt Cagle, Dudley Storey) — I consider it the SVG Bible.
  • Lea Verou has a wealth of SVG knowledge and has spoken on the topic quite a bit (like this video from Frontend United 2019).
  • SVG Animations (Sarah Drasner)
  • SVG Essentials (Amelia Bellamy-Royds, J. David Eisenberg)
  • Practical SVG (Chris Coyier)

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

  • Sara Soueidan
  • Carl Schoof
  • Cassie Evans
  • Val Head
  • Ana Tudor

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.

View Details

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 FallbackWe 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.

View Details

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 problemFirst 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 FallbackIf 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 FallbackBut 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 FallbackI 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 FallbackThis 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 solutionThe 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:

```

Project name Amount Date Edit

``` 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.)

AccessibilityLet’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.

View Details

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 buttonsWhen 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:

```

`` As we can see, the HTML tag names are different. It’s the common classes —.wp-block-buttonand.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 stylestheme.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 stylesSince 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 blocksLet’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 upWhile 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.

View Details

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 trafficOverall, 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 a 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 numbersThis 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 Alamanac 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.

NewsletterThe 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 updatesIt’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 reviewOy, 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.

  • 🚫 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.
  • 🚫 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-settingNew 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.

View Details

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 * The WebAIM Million 2022 * State of CSS 2022 Survey * Interop 2022 Dashboard * Jamstack Community Survey 2022 * 2022 State of Open Source * StackOverflow 2022 Developer Survey * GitHub’s 2022 State of the Octoverse * GitHub Copilot’s impact on developer productivity and happiness * The Software House State of Frontend 2022 * Sparkbox 2022 Design Systems Survey * UXTools.co 2022 Design Tools Survey * 2023 HackerRank Developer Skills Report * Tower Git Mac Dev Survey 2022 * Developer Nation 2022 Q1 Pulse Report * Postman 2022 State of the API Report * CodeinWP WordPress Hosting Survey 2022 * WordPress LMS Websites: A Data Study * UN E-Government Survey 2022 * LinkedIn 2022 Workplace Learning Report * UpWork: The Great Work Teardown * UpWork 2022 Labor Market Trends and Insights * Reblaze 2022 State of Web Security Survey * Trend Micro 2022 Midyear Cybersecurity Report * 1Password: The realities of parenting and growing up online * The Eclipse Foundation 2022 IoT & Edge Developer Survey * CampaignMonitor Ultimate Email Marketing Benchmarks for 2022 * Wrapping up

HTTP Archive Annual State of the WebWhat 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 ReportThe WebAIM Million 2022What 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 list, the 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 ReportState of CSS 2022 SurveyWhat 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 ResultsInterop 2022 DashboardWhat 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 DashboardJamstack Community Survey 2022What 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 Results2022 State of Open SourceWhat 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 PDFStackOverflow 2022 Developer SurveyWhat 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 ResultsGitHub’s 2022 State of the OctoverseWhat 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 ReportGitHub Copilot’s impact on developer productivity and happinessWhat 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 reportThe Software House State of Frontend 2022What 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 ReportSparkbox 2022 Design Systems SurveyWhat 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 ResultsUXTools.co 2022 Design Tools SurveyWhat 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 Results2023 HackerRank Developer Skills ReportWhat 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 ReportTower Git Mac Dev Survey 2022What 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 ResultsDeveloper Nation 2022 Q1 Pulse ReportWhat 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 ReportPostman 2022 State of the API ReportWhat 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 ReportCodeinWP WordPress Hosting Survey 2022What 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. 🤷‍♂️

See ResultsWordPress LMS Websites: A Data StudyWhat 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 ResearchUN E-Government Survey 2022What 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 ResultsLinkedIn 2022 Workplace Learning ReportWhat 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 ReportUpWork: The Great Work TeardownWhat 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 ReportUpWork 2022 Labor Market Trends and InsightsWhat 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 TrendsReblaze 2022 State of Web Security SurveyWhat 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 ReportWhat 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 Report1Password: The realities of parenting and growing up onlineWhat 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 PDFThe Eclipse Foundation 2022 IoT & Edge Developer SurveyWhat 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 2022What 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 BenchmarksWrapping upWe 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.

View Details

CSS Nesting is making the rounds yet again. Remember earlier this year when Adam and Mia put three syntax options up for a vote? Those results were tallied and it wasn’t even even close.

Now there’s another chance to speak into the future of nesting, this time over at the WebKit blog. The results from the Adam and Mia’s survey sparked further discussion and two more ideas were added to the mix. This new survey lets you choose from all five options.

Jen Simmons has put together a thorough outline of those options, including a refresher on nesting, details on how we arrived at the five options, and tons of examples that show the options in various use cases. Let’s return the favor of all the hard work that’s being done here by taking this quick one-question survey.

To Shared Link — Permalink on CSS-Tricks


Help choose the syntax for CSS Nesting originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Being able to quickly spin up a WordPress instance has been the strength of WordPress ever since its famous “five-minute install”. Upload a few files, configure a few settings, and you’re off.

The friction of uploading files has gotten a lot easier, thanks to plenty of “one-click” install options many hosts offer (including DigitalOcean and Cloudways).

Some companies have tried to abstract the process even more, using the multi-site features of WordPress to fire up disposable instances for testing and demos. WordPress Sandbox and WP Sandbox come to mind. Scaling can be an issue here, as instances run on the same install adding lag to the entire network. I worked on a headless WordPress project that did this in the background for users, and I recall the incredibly long wait it would take users to create a new account as the number of sites in the network piled up.

Enter WordPress Playground. It runs entirely in the browser which is mindblowing to me as a long-time WordPress user. If you’re having a hard time wrapping your head around how it all works like I did, that link to the overview spells it out nicely:

  • PHP runs as a WebAssembly binary
  • MySQL is replaced for SQLite via a WordPress plugin
  • Web server is implemented with the Service Worker API

Dang, that’s cool. The move to SQLite is especially interesting, as it could bring huge performance gains to many sites that might not need the full heft of WordPress — a “WordPress Lite” as Chris recently described it in a different context. In fact, that work is already happening in the experimental WordPress performance plugin.

The evolution to a light, frictionless WordPress is a fun space to watch. I imagine there’s a good chunk of existing WordPress sites that stand to benefit from a slimmed-down CMS. The demo offers a glimpse at what an onboarding experience for that sort of thing could look like.

Select a theme, choose your features, and go!


WordPress Playground: Running WordPress in the Browser originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

In this series, we’ve been making image sliders with nothing but HTML and CSS. The idea is that we can use the same markup but different CSS to get wildly different results, no matter how many images we toss in. We started with a circular slider that rotates infinitely, sort of like a fidget spinner that holds images. Then we made one that flips through a stack of photos.

This time around, we’re diving into the third dimension. It’s going to look tough at first, but lots of the code we’re looking at is exactly what we used in the first two articles in this series, with some modifications. So, if you’re just now getting into the series, I’d suggest checking out the others for context on the concepts we’re using here.

CSS Sliders series Circular Rotating Image Slider * Flipping Through Polaroid Images * Infinite 3D Sliders (you are here!)* This is what we’re aiming for:

CodePen Embed FallbackAt first glance, it looks like we have a rotating cube with four images. But in reality, we’re dealing with six images in total. Here is the slider from a different angle:

Now that we have a good visual for how the images are arranged, let’s dissect the code to see how we get there.

The basic setupSame HTML as the rest of the sliders we’ve used for the other sliders:

```

``` And once again, we’re using CSS Grid to place the images in a stack, one on top of another:

.gallery { display: grid;}.gallery > img { grid-area: 1 / 1; width: 160px; aspect-ratio: 1; object-fit: cover;} The animationThe logic for this slider is very similar to the circular slider from the first article. In fact, if you check the video above again, you can see that the images are placed in a way that creates a polygon. After a full rotation, it returns to the first image.

We relied on the CSS transform-origin and animation-delay properties for that first slider. The same animation is applied to all of the image elements, which rotate around the same point. Then, by using different delays, we correctly place all the images around a big circle.

The implementation will be a bit different for our 3D slider. Using transform-origin won’t work here because we’re working in 3D, so we will use transform instead to correctly place all the images, then rotate the container.

We’re reaching for Sass again so we can loop through the number of images and apply our transforms:

@for $i from 1 to ($n + 1) { .gallery > img:nth-child(#{$i}) { transform: rotate(#{360*($i - 1) / $n}deg) /* 1 */ translateY(50% / math.tan(180deg / $n)) /* 2 */ rotateX(90deg); /* 3 */ }} You might be wondering why we’re jumping straight into Sass. We started with a fixed number of images using vanilla CSS in the other articles before generalizing the code with Sass to account for any number (N) of images. Well, I think you get the idea now and we can cut out all that discovery work to get to the real implementation.

The transform property is taking three values, which I’ve illustrated here:

We first rotate all the images above each other. The angle of rotation depends on the number of images. For N images, we have an increment equal to 360deg/N. Then we translate all of the images by the same amount in a way that makes their center points meet on the sides.

There’s some boring geometry that helps explain how all this works, but the distance is equal to 50%/tan(180deg/N). We dealt with a similar equation when making the circular slider ( transform-origin: 50% 50%/sin(180deg/N) ).

Finally, we rotate the images around the x-axis by 90deg to get the arrangement we want. Here is a video that illustrates what the last rotation is doing:

Now all we have to do is to rotate the whole container to create our infinite slider.

.gallery { transform-style: preserve-3d; --_t: perspective(280px) rotateX(-90deg); animation: r 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;}@keyframes r { 0%, 3% {transform: var(--_t) rotate(0deg); } @for $i from 1 to $n { #{($i/$n)*100 - 2}%, #{($i/$n)*100 + 3}% { transform: var(--_t) rotate(#{($i / $n) * -360}deg); } } 98%, 100% { transform: var(--_t) rotate(-360deg); }} That code might be hard to understand, so let’s actually step back a moment and revisit the animation we made for the circular slider. This is what we wrote in that first article:

.gallery { animation: m 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;}@keyframes m { 0%, 3% { transform: rotate(0); } @for $i from 1 to $n { #{($i / $n) * 100 - 2}%, #{($i / $n) * 100 + 3}% { transform: rotate(#{($i / $n) * -360}deg); } } 98%, 100% { transform: rotate(-360deg); }} The keyframes are almost identical. We have the same percentage values, the same loop, and the same rotation.

Why are both the same? Because their logic is the same. In both cases, the images are arranged around a circular shape and we need to rotate the whole thing to show each image. That’s how I was able to copy the keyframes from the circular slider and use that same code for our 3D slider. The only difference is that we need to rotate the container by -90deg along the x-axis to see the images since we have already rotated them by 90deg on the same axis. Then we add a touch of perspective to get the 3D effect.

That’s it! Our slider is done. Here is the full demo again. All you have to do is to add as many images as you want and update one variable to get it going.

CodePen Embed FallbackVertical 3D sliderSince we are playing in the 3D space, why not make a vertical version of the previous slider? The last one rotates along the z-axis, but we can also move along the x-axis if we want.

CodePen Embed FallbackIf you compare the code for both versions of this slider, you might not immediately spot the difference because it’s only one character! I replaced rotate() with rotateX() inside the keyframes and the image transform. That’s it!

It should be noted that rotate() is equivalent to rotateZ(), so by changing the axis from Z to X we transform the slider from the horizontal version into the vertical one.

Cube sliderWe cannot talk about 3D in CSS without talking about cubes. And yes, that means we are going to make another version of the slider.

The idea behind this version of the slider is to create an actual cube shape with the images and rotate the full thing in around the different axis. Since it’s a cube, we’re dealing with six faces. We’ll use six images, one for each face of the cube. So, no Sass but back to vanilla CSS.

CodePen Embed FallbackThat animation is a little overwhelming, right? Where do you even start?

We have six faces, so we need to perform at least six rotations so that each image gets a turn. Well, actually, we need five rotations — the last one brings us back to the first image face. If you go grab a Rubik’s Cube — or some other cube-shaped object like dice — and rotate it with your hand, you’ll have a good idea of what we’re doing.

.gallery { --s: 250px; /* the size */ transform-style: preserve-3d; --_p: perspective(calc(2.5*var(--s))); animation: r 9s infinite cubic-bezier(.5, -0.5, .5, 1.5);}@keyframes r { 0%, 3% { transform: var(--_p); } 14%, 19% { transform: var(--_p) rotateX(90deg); } 31%, 36% { transform: var(--_p) rotateX(90deg) rotateZ(90deg); } 47%, 52% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); } 64%, 69% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg); } 81%, 86% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg); } 97%, 100%{ transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }} The transform property starts with zero rotations and, on each state, we append a new rotation on a specific axis until we reach six rotations. Then we are back to the first image.

Let’s not forget the placement of our images. Each one is applied to a face of the cube using transform:

.gallery img { grid-area: 1 / 1; width: var(--s); aspect-ratio: 1; object-fit: cover; transform: var(--_t,) translateZ(calc(var(--s) / 2));}.gallery img:nth-child(2) { --_t: rotateX(-90deg); }.gallery img:nth-child(3) { --_t: rotateY( 90deg) rotate(-90deg); }.gallery img:nth-child(4) { --_t: rotateX(180deg) rotate( 90deg); }.gallery img:nth-child(5) { --_t: rotateX( 90deg) rotate( 90deg); }.gallery img:nth-child(6) { --_t: rotateY(-90deg); } You are probably thinking there is weird complex logic behind the values I’m using there, right? Well, no. All I did was open DevTools and play with different rotation values for each image until I got it right. It may sound stupid but, hey, it works — especially since we have a fixed number of images and we are not looking for something that supports N images.

In fact, forget the values I’m using and try to do the placement on your own as an exercise. Start with all the images stacked on top of each other, open the DevTools, and go! You will probably end up with different code and that’s totally fine. There can be different ways to position the images.

What’s the trick with the comma inside the var()? Is it a typo?

It’s not a typo so don’t remove it! If you do remove it, you will notice that it affects the placement of the first image. You can see that in my code I defined --_t for all the images except the first one because I only need a translation for it. That comma makes the variable fall back to a null value. Without the comma, we won’t have a fallback and the whole value will be invalid.

From the specification:

Note: That is, var(--a,) is a valid function, specifying that if the --a custom property is invalid or missing, the `var()`` should be replaced with nothing.

Random cube sliderA little bit of randomness can be a nice enhancement for this sort of animation. So, rather than rotate the cube in sequential order, we can roll the dice so to speak, and let the cube roll however it will.

CodePen Embed FallbackCool right? I don’t know about you, but I like this version better! It’s more interesting and the transitions are satisfying to watch. And guess what? You can play with the values to create your own random cube slider!

The logic is actual not random at all — it just appears that way. You define a transform on each keyframe that allows you to show one face and… well, that’s really it! You can pick any order you want.

@keyframes r { 0%, 3% { transform: var(--_p) rotate3d( 0, 0, 0, 0deg); } 14%,19% { transform: var(--_p) rotate3d(-1, 1, 0,180deg); } 31%,36% { transform: var(--_p) rotate3d( 0,-1, 0, 90deg); } 47%,52% { transform: var(--_p) rotate3d( 1, 0, 0, 90deg); } 64%,69% { transform: var(--_p) rotate3d( 1, 0, 0,-90deg); } 81%,86% { transform: var(--_p) rotate3d( 0, 1, 0, 90deg); } 97%,100% { transform: var(--_p) rotate3d( 0, 0, 0, 0deg); }} I am using rotate3d() this time but am still relying on DevTools to find the values that feel “right” to me. Don’t try to find a relationship between the keyframes because there simply isn’t one. I’m defining separate transforms and then watching the “random” result. Make sure the first image is the first and last frames, respectively, and show a different image on each of the other frames.

You are not obligated to use a rotate3d() transform as I did. You can also chain different rotations like we did in the previous example. Play around and see what you can come up with! I will be waiting for you to share your version with me in the comments section!

Wrapping upI hope you enjoyed this little series. We built some fun (and funny) sliders while learning a lot about all kinds of CSS concepts along the way — from grid placement and stacking order, to animation delays and transforms. We even got to play with a dash of Sass to loop through an array of elements.

And we did it all with the exact same HTML for each and every slider we made. How cool is that? CSS is dang powerful and capable of accomplishing so much without the aid of JavaScript.


CSS Infinite 3D Sliders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The CSS Working Group gave that a thumbs-up a couple weeks ago. The super-duper conceptual proposal being that we can animate or transition from, say, display: block to display: none.

It’s a bit of a brain-twister to reason about because setting display: none on an element cancels animations. And adding it restarts animations. Per the spec:

Setting the display property to none will terminate any running animation applied to the element and its descendants. If an element has a display of none, updating display to a value other than none will start all animations applied to the element by the animation-name property, as well as all animations applied to descendants with display other than none.

That circular behavior is what makes the concept seemingly dead on arrival. But if @keyframes supported any display value other than none, then there’s no way for none to cancel or restart things. That gives non-none values priority, allowing none to do its thing only after the animation or transition has completed.

Miriam’s toot (this is what we’re really calling these, right?) explains how this might work:

We’re not exactly interpolating between, say, block and none, but allowing block to stay intact until the time things stop moving and it’s safe to apply none. These are keywords, so there are no explicit values between the two. As such, this remains a discrete animation. We’re toggling between two values once that animation is complete.

This is the Robert Flack’s example pulled straight from the discussion:

@keyframes slideaway { from { display: block; } to { transform: translateY(40px); opacity: 0;}}.hide { animation: slideaway 200ms; display: none;} This is a helpful example because it shows how the first frame sets the element to display: block, which is given priority over the underlying display: none as a non-none value. That allows the animation to run and finish without none cancelling or resetting it in the process since it only resolves after the animation.

This is the example Miriam referenced on Mastodon:

.hide { transition: opacity 200ms, display 200ms; display: none; opacity: 0;} We’re dealing with a transition this time. The underlying display value is set to none before anything happens, so it’s completely out of the document flow. Now, if we were to transition this on hover, maybe like this:

.hide:hover { display: block; opacity: 1;} …then the element should theoretically fade in at 200ms. Again, we’re toggling between display values, but block is given priority so the transition isn’t cancelled up front and is actually applied after opacity finishes its transition.

At least that’s how my mind is reading into it. I’m glad there are super smart people thinking these things through because I imagine there’s a ton to sort out. Like, what happens if multiple animations are assigned to an element — will none reset or cancel any of those? I’m sure everything from infinite animations, reversed directions, and all sorts of other things will be addressed in time.

But what a super cool first step!


So, you’d like to animate the display property originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Every so often, I find that the links I save to read later fall into natural groups or patterns that reveal common threads of interest. The past couple of weeks have produced a lot of thoughts about ChatGPT, an AI-powered interface that responds to requests in a chat-like exchange. Sorta like a “Hey Siri” request, but in a Discord channel.

ChatGPT is just one of several AI-flavored tech, including GitHub’s CoPilot (writing code) and Dall-E (generative images and art).

Is it the end of human development? A new and exciting way to produce art? Just cocktail party conversation fodder? There are lots of opinions…

  • A Conversation With ChatGPT (Matthias Ott) — Matthias has a conversation with ChatGPT about typography that delves into deeply theoretical thoughts on design process. My favorite is in response to whether designers should learn to code: “Ultimately, whether or not designers should learn to code is a decision that each individual designer must make for themselves, based on their own goals and circumstances. Some designers may benefit from learning to code, while others may be better served by focusing on design principles and concepts.”
  • They were supposed to replace the creative jobs last (Dave Rupert)“As interesting a future this creates, I’m a member of an old caste of people that still believes massive gains don’t come without realized costs; or more explicitly, electricity isn’t the only cost. What if the cost we’re paying is our perception of reality itself? It’s increasingly likely that the next thing you read or watch is the product of a content extruder.”
  • I just used ChatGPT to help with a complicated equation. — A reddit user used ChatGPT to write a complex equation in Notion. There were a couple hiccups, but it worked in the end.
  • ChatGPT Creates a Working WordPress Plugin – On the First Try (WP Tavern) — Sarah Gooding reporting on a ChatGPT experiment where Johnathon Williams was able to spit out a fully-functional WordPress plugin with a simple chat command. This is the sort of thing that both terrifies me but also blows my mind-hole.
  • ChatGPT Is a Smart Computer’s Impression of a Know-It-All (Pixel Envy) — Nick Heer points to an article on The Atlantic about ChatGPT that opens with three paragraphs written by ChatGPT. It’s crazy that it comes off as naturally as it does, even if it smells slightly fishy at first.
  • Use of ChatGPT1 generated text for content on Stack Overflow is temporarily banned. (Stack Overflow) — A mild dose of #HotDrama as far as Stack Overflow users posting ChatGPT-produced code as answers.
  • Midjourney vs. human illustrators: has AI already won? (Evil Martians) — I love the experiment in this post because it’s a clear example that AI doesn’t just work. In its current state, at best, AI is a junior designer when put to the task of creating an image: “After two and a half hours of back and forth with the AI, I was completely exhausted and decided to just upscale the most promising result.” A bonus is that the post concludes with a list of situations where AI might realistically help the team with future work — and it ain’t an entire person’s job.
  • Quick Thoughts on AI (Collaborative Fund) — Ha! Crazy to see a chart comparing how fast ChatGPT reached one million users to other popular services. It took Facebook 10 months, but only five days for ChatGPT.

Dall-E, I want a photo of a developer sitting at a desk with his head exploding while having a chat conversation on a desktop computer with an artificial intelligence algorithm.

Not bad, not bad.


Some Links on AI-Related Stuff originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Suzy Naschansky from the HTMHell Advent Calendar:

```

All About Dragons

I like dragons. Blah blah blah blah blah.

Read more

`` See thataria-labelledbyattribute? It chains two IDs from the markup, one for the heading (#article1-heading) and one for the link (#article1-read-more`). What happens there is a screenreader will replace the existing semantic label between the link tags and use the content from both elements and announce them together as a single string of text:

Read more All About Dragons I’m always sheepish when realizing there’s something I think I should know but don’t. This is definitely one of those cases and I’m thankful as all heck that Suzy shared it.

I was actually in a situation just recently where I ~~could’ve~~ should’ve done this. I always try to avoid a bunch of “Read more” links on the same page but coming up with different flavors of the same thing is tough when you’re working with something like a loop of 15 posts (even though there are resources to help). And if we need to keep labels short for aesthetic reasons — design requirements and whatnot — it’s even more challenging. The aria-labelledby attribute gives me exactly what I want: consistent visual labels and more contextual announcements for assistive tech.

And this is only a thing when the text you want to use for the accessible label already exists on the page. Otherwise, you’d want to go with aria-label and with the caveat that it’s purely for interactive elements that are unable to label things accessibly with semantic HTML.

If you are working in a CMS like WordPress (which I am), you might need to do a little extra work. Like when I drop a Button block on the page, these are the options I have to work with:

Some nice options in there, but nothing to do with accessible labelling. If you’re wondering what’s buried in that Advanced panel:

Close, but no cigar.Instead, you’ll need to edit the button in HTML mode:

But before doing that, you gotta add an ID to the heading you want to use. The Heading block has the same Advanced panel setting for adding an anchor, which’ll inject an ID on the element:

Then you can go edit the Button block in HTML mode and add the accessible-labels ID as well as an ID for the button itself. This is an example of the edited markup:

```

``` Great! But WordPress just ain’t cool with that:

You can try to resolve the issue:

Le sigh. The Button block has to be converted to a Custom HTML block. Kinda defeats the whole visual editing thing that WordPress is so good at. I did a super quick search for a plugin that might add ARIA labelling options to certain blocks, but came up short. Seems like a ripe opportunity to make one or submit PRs for the blocks that could use those options.


Unchain My Inaccessibly-Labelled Heart originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

We’ve got ourselves a real holiday treat! Join host Alex Trost from the Frontend Horse community for the Holiday Snowtacular 2022 this Friday, December 16.

There’s a lineup of 12 awesome speakers — including Chris Coyier, Cassidy Williams, Kevin Powell, and Angie Jones — each discussing various front-end and web dev topics. It’s like the 12 days of Christmas, but wrapped up in a four-hour session for web nerds like us.

It’s a real good cause, too. The event is free, but includes fundraising Doctors Without Borders with a goal of reaching $20,000. You can donate here any time and anything you give will be matched by the event’s sponors. So, come for the front-end fun and help a great cause in the process.

To Shared Link — Permalink on CSS-Tricks


Holiday Snowtacular 2022 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

CSS Container Queries are still gaining traction and many of us are getting our hands wet with them, even if it’s for little experiments or whatnot. They’ve got great, but not quite full, browser support — enough to justify using them in some projects, but maybe not to the extent where we might be tempted to start replacing media queries from past projects with shiny new container size queries.

They sure are handy though! In fact, I’ve already run into a few situations where I really wanted to reach for them but just couldn’t overcome the support requirements. If I had been able to use them, this is how it would have looked in those situations.

All of the following demos will be best viewed in Chrome or Safari at the time of this writing. Firefox plans to ship support in Version 109.

Case 1: Card gridYou kind of had to expect this one, right? It’s such a common pattern that all of us seem to run into it at some point. But the fact is that container size queries would have been a huge time-saver for me with a better outcome had I been able to use them over standard media queries.

Let’s say you’ve been tasked with building this card grid with the requirement that each card needs to keep it’s 1:1 aspect ratio:

It’s tougher than it looks! The problem is that sizing a component’s contents on the viewport’s width leaves you at the mercy of how the component responds to the viewport — as well the way any other ancestor containers respond to it. If, for example, you want the font size of a card heading to reduce when the card hits a certain inline size there’s no reliable way to do it.

You could set the font size in vw units, I suppose, but the component is still tied to the browser’s viewport width. And that can cause problems when the card grid is used other in contexts that may not have the same breakpoints.

In my real-world project, I landed on a JavaScript approach that would:

  1. Listen for a resize event.
  2. Calculate the width of each card.
  3. Add an inline font size to each card based on its width.
  4. Style everything inside using em units.

Seems like a lot of work, right? But it is a stable solution to get the required scaling across different screen sizes in different contexts.

Container queries would have been so much better because they provide us with container query units, such as the cqw unit. You probably already get it, but 1cqw is equal to 1% of a container’s width. We also have the cqi unit that’s a measure of a container’s inline width, and cqb for a container’s block width. So, if we have a card container that is 500px wide, a 50cqw value computes to 250px.

If I had been able to use container queries in my card grid, I could have set up the .card component as a container:

.card { container: card / size;} Then I could have set an inner wrapper with padding that scales at 10% of the .card‘s width using the cqw unit:

.card__inner { padding: 10cqw; } That’s a nice way to scale the spacing between the card’s edges and its contents consistently no matter where the card is used at any given viewport width. No media queries required!

Another idea? Use cqw units for the font size of the inner contents, then apply padding in em units:

.card__inner { font-size: 5cqw; padding: 2em;} 5cqw is an arbitrary value — just one that I settled on. That padding is still equal to 10cqw since the em unit is relative to the .card__inner font size!

Did you catch that? The 2em is relative to the 5cqw font size that is set on the same container. Containers work different than what we’re used to, as em units are relative to the same element’s font-size value. But what I quickly noticed is that container query units relate to the nearest parent that is also a container.

For example, 5cqw does not scale based on the .card element’s width in this example:

.card { container: card / size; container-name: card; font-size: 5cqw; } Rather, it scales to whatever the nearest parent that’s defined as a container. That’s why I set up a .card__inner wrapper.

CodePen Embed FallbackCase 2: Alternating layoutI needed yet another card component in a different project. This time, I needed the card to transition from a landscape layout to a portrait layout… then back to landscape, and back to portrait again as the screen gets smaller.

I did the dirty work of making this component go to portrait at those two specific viewport ranges (shout out to the new media query range syntax!), but again, the problem is that it is then locked to the media queries set on it, its parent, and anything else that might respond to the viewport’s width. We want something that works in any condition without worrying about wondering where the content is going to break!

Container queries would have made this a breeze, thanks to the @container rule:

.info-card { container-type: inline-size; container-name: info-card;}@container info-card (max-width: 500px) { .info-card__inner { flex-direction: column; }} One query, infinite fluidity:

CodePen Embed FallbackBut hold on! There’s something you might want to watch out for. Specifically, it could be difficult to use a container query like this within a prop-based design system. For example, this .info-card component could contain child components that rely on props to change their appearance.

Why’s that a big deal? The card’s portrait layout might require the alternate styling but you can’t change JavaScript props with CSS. As such, you risk duplicating the required styles. I actually touched on this and how to work around it in another article. If you need to use container queries for a significant amount of your styling, then you may need to base your entire design system around them rather than trying to shoehorn them into an existing design system that’s heavy on media queries.

Case 3: SVG strokesHere’s another super common pattern I’ve recently used where container size queries would have resulted in a more polished product. Say you have an icon locked up with a heading:

```

Heading

`` It’s pretty straightforward to scale the icon with the title’s size, even without media queries. The problem, though, is that the SVG’sstroke-width` might get too thin to be noticed all that well at a smaller size, and perhaps catch too much attention with a super thick stroke at a larger size.

I’ve had to create and apply classes to each icon instance to determine its size and stroke width. That’s OK if the icon is next to a heading that’s styled with a fixed font size, I guess, but it’s not so great when working with fluid type that constantly changes.

The heading’s font size might be based on the viewport’s width, so the SVG icon needs to adjust accordingly where its stroke works at any size. You could make the stroke width relative to the heading’s font-size by setting it in em units. But if you have a specific set of stroke sizes that you need to stick to, then this wouldn’t work because it otherwise scales linearly — there’s no way to adjust it to a specific stroke-width value at certain points without resorting to media queries on the viewport width.

But here’s what I would have done if I had the luxury of container queries at that time:

.icon { container: icon / size; width: 1em; height: 1em; }.icon svg { width: 100%; height: 100%; fill: none; stroke: #ccc; stroke-width: 0.8; }@container icon (max-width: 70px) { .icon svg { stroke-width: 1.5; }}@container icon (max-width: 35px) { .icon svg { stroke-width: 3; }} Compare the implementations and see how the container query version snaps the SVG’s stroke to the specific widths I want based on the container’s width.

CodePen Embed FallbackBonus: Other types of container size queriesOK, so I haven’t actually run into this on a real project. But as I was combing through information on container queries, I noticed that there are additional things we can query on a container that are related to the container’s size or physical dimensions.

Most examples I’ve seen query the width, max-width, and min-width, height, block-size, and inline-size as I’ve been doing throughout this article.

@container info-card (max-width: 500px) { .info-card__inner { flex-direction: column; }} But MDN outlines two more things we can query against. One is orientation which makes perfect sense because we use it all the time in media queries. It’s no different with container queries:

@media screen (orientation: landscape) { .info-card__inner { /* Style away! */ }} @container info-card (orientation: landscape) { .info-card__inner { /* Style away! */ }} The other? It’s aspect-ratio, believe it or not:

@container info-card (aspect-ratio: 3/2) { .info-card__inner { /* Style away! */ }} Here’s an editable demo to play around with both examples:

CodePen Embed FallbackI haven’t really found a good use case for either of these yet. If you have any ideas or feel like it could’ve helped you in your projects, let me know in the comments!


A Few Times Container Size Queries Would Have Helped Me Out originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Sara Soueidan with everything you need, from what screen reading options are out there all the way to setting up virtual machines for them, installing them, and confguring keyboard options. It’s truly a one-stop reference that pulls together disparate tips for getting the most out of your screen reading accessibility testing.

Thanks, Sara, for putting together this guide, and especially doing so while making no judgments or assumptions about what someone may or may not know about accessibility testing. The guide is just one part of Sara’s forthcoming Practical Accessibility course, which is available for pre-order.

To Shared Link — Permalink on CSS-Tricks


Setting up a screen reader testing environment on your computer originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

We’ve accomplished a bunch of stuff in this series! We created a custom WordPress block that fetches data from an external API and renders it on the front end. Then we took that work and extended it so the data also renders directly in the WordPress block editor. After that, we created a settings UI for the block using components from the WordPress InspectorControls package.

There’s one last bit for us to cover and that’s saving the settings options. If we recall from the last article, we’re technically able to “save” our selections in the block settings UI, but those aren’t actually stored anywhere. If we make a few selections, save them, then return to the post, the settings are completely reset.

Let’s close the loop and save those settings so they persist the next time we edit a post that contains our custom block!

Working With External APIs in WordPress Blocks Rendering Data on the Front End * Rendering Data on the Back End * Creating a Custom Settings UI * Saving Custom Block Settings (you are here!) * Working With Live API Data (coming soon)* Saving settings attributesWe’re working with an API that provides us with ~~soccer~~ football team ranking and we’re using it to fetch for displaying rankings based on country, league, and season. We can create new attributes for each of those like this:

// index.jsattributes: { data: { type: "object", }, settings: { type: "object", default: { country: { type: "string", }, league: { type: "string", }, season: { type: "string", }, }, },}, Next, we need to set the attributes from LeagueSettings.js. Whenever a ComboboxControl is updated in our settings UI, we need to set the attributes using the setAttributes() method. This was more straightfoward when we were only working with one data endpoint. But now that we have multiple inputs, it’s a little more involved.

This is how I am going to organize it. I am going to create a new object in LeagueSettings.js that follows the structure of the settings attributes and their values.

// LeagueSettings.jslet localSettings = { country: attributes.settings.country, league: attributes.settings.league, season: attributes.settings.season,}; I am also going to change the initial state variables from null to the respective settings variables.

// LeagueSettings.jsconst [country, setCountry] = useState(attributes.settings.country);const [league, setLeague] = useState(attributes.settings.league);const [season, setSeason] = useState(attributes.settings.season); In each of the handle______Change(), I am going to create a setLocalAttributes() that has an argument that clones and overwrites the previous localSettings object with the new country, league, and season values. This is done using the help of the spread operator.

// LeagueSettings.jsfunction handleCountryChange(value) { // Initial code setLocalAttributes({ ...localSettings, country: value }); // Rest of the code}function handleLeagueChange(value) { // Initial code setLocalAttributes({ ...localSettings, league: value }); // Rest of the code}function handleSeasonChange(value) { // Initial code setLocalAttributes({ ...localSettings, season: value }); // Rest of the code} We can define the setLocalAttributes() like this:

// LeagueSettings.jsfunction setLocalAttributes(value) { let newSettings = Object.assign(localSettings, value); localSettings = { ...newSettings }; setAttributes({ settings: localSettings });} So, we’re using Object.assign() to merge the two objects. Then we can clone the newSettings object back to localSettings because we also need to account for each settings attribute when there a new selection is made and a change occurs.

Finally, we can use the setAttributes() as we do normally to set the final object. You can confirm if the above attributes are changing by updating the selections in the UI.

Another way to confirm is to do a console.log() in DevTools to find the attributes.

Look closer at that screenshot. The values are stored in attributes.settings. We are able to see it happen live because React re-renders every time we make a change in the settings, thanks to the useState() hook.

Displaying the values in the blocks settings UIIt isn’t very useful to store the setting values in the control options themselves since each one is dependent on the other setting value (e.g. rankings by league depends on which season is selected). But it is very useful in situations where the settings values are static and where settings are independent of each other.

Without making the current settings complicated, we can create another section inside the settings panel that shows the current attributes. You can choose your way to display the settings values but I am going to import a Tip component from the @wordpress/components package:

// LeagueSettings.jsimport { Tip } from "@wordpress/components"; While I’m here, I am going to do a conditional check for the values before displaying them inside the Tip component:

<Tip> {country && league && season && ( <> <h2>Current Settings: </h2> <div className="current-settings"> <div className="country"> Country: {attributes.settings.country} </div> <div className="league"> League: {attributes.settings.league} </div> <div className="season"> Season: {attributes.settings.season} </div> </div> )}</Tip> Here’s how that winds up working in the block editor:

API data is more powerful when live data can be shown without having to manually update them each and every time. We will look into that in the next installment of this series.


Saving Settings for a Custom WordPress Block in the Block Editor originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

In the last article, we made a pretty cool little slider (or “carousel” if that’s what you prefer) that rotates in a circular direction. This time we are going to make one that flips through a stack of Polaroid images.

CodePen Embed FallbackCool right? Don’t look at the code quite yet because there’s a lot to unravel. Join me, will ya?

CSS Sliders series Circular Rotating Image Slider * Flipping Through Polaroid Images (you are here!) * Infinite 3D Sliders (coming Dec. 16)* The basic setupMost of the HTML and CSS for this slider is similar to the circular one we made last time. In fact, we’re using the exact same markup:

```

`` And this is the basic CSS that sets our parent.gallery` container as a grid where all the images are stacked one on top of one another:

.gallery { display: grid; width: 220px; /* controls the size */}.gallery > img { grid-area: 1 / 1; width: 100%; aspect-ratio: 1; object-fit: cover; border: 10px solid #f2f2f2; box-shadow: 0 0 4px #0007;} Nothing complex so far. Even for the Polaroid-like style for the images, all I’m using is some border and box-shadow. You might be able to do it better, so feel free to play around with those decorative styles! We’re going to put most of our focus on the animation, which is the trickiest part.

What’s the trick?The logic of this slider relies on the stacking order of the images — so yes, we are going to play with z-index. All of the images start with the same z-index value (2) which will logically make the last image on the top of the stack.

We take that last image and slide it to the right until it reveals the next image in the stack. Then we decrease the image’s z-index value then we slide it back into the deck. And since its z-index value is lower than the rest of the images, it becomes the last image in the stack.

Here is a stripped back demo that shows the trick. Hover the image to activate the animation:

CodePen Embed FallbackNow, imagine the same trick applied to all the images. Here’s the pattern if we’re using the :nth-child() pseudo-selector to differentiate the images:

  • We slide the last image (N). The next image is visible (N - 1).
  • We slide the next image (N - 1). The next image is visible (N - 2)
  • We slide the next image (N - 2). The next image is visible (N - 3)
  • (We continue the same process until we reach the first image)
  • We slide the first image (1). The last image (N) is visible again.

That’s our infinite slider!

Dissecting the animationIf you remember the previous article, I defined only one animation and played with delays to control each image. We will be doing the same thing here. Let’s first try to visualize the timeline of our animation. We will start with three images, then generalize it later for any number (N) of images.

Our animation is divided into three parts: “slide to right”, “slide to left” and “don’t move”. We can easily identify the delay between each image. If we consider that the first image starts at 0s, and the duration is equal to 6s, then the second one will start at -2s and the third one at -4s.

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 6s / 3 */.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 6s / 3 */ We can also see that the “don’t move” part takes two-thirds of the whole animation (2*100%/3) while the “slide to right” and “slide to left” parts take one-third of it together — so, each one is equal to 100%/6 of the total animation.

We can write our animation keyframes like this:

@keyframes slide { 0% { transform: translateX(0%); } 16.67% { transform: translateX(120%); } 33.34% { transform: translateX(0%); } 100% { transform: translateX(0%); } } That 120% is an arbitrary value. I needed something bigger than 100%. The images need to slide to the right away from the rest of the images. To do that, it needs to move by at least 100% of its size. That’s why I went 120% — to gain some extra space.

Now we need to consider the z-index. Don’t forget that we need to update the image’s z-index value after it slides to the right of the pile, and before we slide it back to the bottom of the pile.

@keyframes slide { 0% { transform: translateX(0%); z-index: 2; } 16.66% { transform: translateX(120%); z-index: 2; } 16.67% { transform: translateX(120%); z-index: 1; } /* we update the z-order here */ 33.34% { transform: translateX(0%); z-index: 1; } 100% { transform: translateX(0% ); z-index: 1; } } Instead of defining one state at the 16.67% (100%/6) point in the timeline, we are defining two states at nearly identical points (16.66% and 16.67%) where the z-index value decreases before we slide back the image back to the deck.

Here’s what happens when we pull of all that together:

CodePen Embed FallbackHmmm, the sliding part seems to work fine, but the stacking order is all scrambled! The animation starts nicely since the top image is moving to the back… but the subsequent images don’t follow suit. If you notice, the second image in the sequence returns to the top of the stack before the next image blinks on top of it.

We need to closely follow the z-index changes. Initially, all the images have are z-index: 2. That means the stacking order should go…

Our eyes 👀 --> 3rd (2) | 2nd (2) | 1st (2) We slide the third image and update its z-index to get this order:

Our eyes 👀 --> 2nd (2) | 1st (2) | 3rd (1) We do the same with the second one:

Our eyes 👀 --> 1st (2) | 3rd (1) | 2nd (1) …and the first one:

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1) We do that and everything seems to be fine. But in reality, it’s not! When the first image is moved to the back, the third image will start another iteration, meaning it returns to z-index: 2:

Our eyes 👀 --> 3rd (2) | 2nd (1) | 1st (1) So, in reality we never had all the images at z-index: 2 at all! When the images aren’t moving (i.e., the “don’t move” part of the animation) the z-index is 1. If we slide the third image and update its z-index value from 2 to 1, it will remain on the top! When all the images have the same z-index, the last one in the source order — our third image in this case — is on top of the stack. Sliding the third image results in the following:

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1) The third image is still on the top and, right after it, we move the second image to the top when its animation restarts at z-index: 2:

Our eyes 👀 --> 2nd (2) | 3rd (1) | 1st (1) Once we slide it, we get:

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1) Then the first image will jump on the top:

Our eyes 👀 --> 1st(2) | 3rd (1) | 2nd (1) OK, I am lost. All the logic is wrong then?I know, it’s confusing. But our logic is not completely wrong. We only have to rectify the animation a little to make everything work the way we want. The trick is to correctly reset the z-index.

Let’s take the situation where the third image is on the top:

Our eyes 👀 --> 3rd (2) | 2nd (1) | 1st (1) We saw that sliding the third image and changing its z-index keeps it on top. What we need to do is update the z-index of the second image. So, before we slide the third image away from the deck, we update the z-index of the second image to 2.

In other words, we reset the z-index of the second image before the animation ends.

The green plus symbol represents increasing z-index to 2, and the red minus symbol correlates to z-index: 1. The second image starts with z-index: 2, then we update it to 1 when it slides away from the deck. But before the first image slides away from the deck, we change the z-index of the second image back to 2. This will make sure both images have the same z-index, but still, the third one will remain on the top because it appears later in the DOM. But after the third image slides and its z-index is updated, it moves to the bottom.

This two-thirds through the animation, so let’s update our keyframes accordingly:

@keyframes slide { 0% { transform: translateX(0%); z-index: 2; } 16.66% { transform: translateX(120%); z-index: 2; } 16.67% { transform: translateX(120%); z-index: 1; } /* we update the z-order here */ 33.34% { transform: translateX(0%); z-index: 1; } 66.33% { transform: translateX(0%); z-index: 1; } 66.34% { transform: translateX(0%); z-index: 2; } /* and also here */ 100% { transform: translateX(0%); z-index: 2; } } CodePen Embed FallbackA little better, but still not quite there. There’s another issue…

Oh no, this will never end!Don’t worry, we are not going to change the keyframes again because this issue only happens when the last image is involved. We can make a “special” keyframe animation specifically for the last image to fix things up.

When the first image is on the top, we have the following situation:

Our eyes 👀 --> 1st (2) | 3rd (1) | 2nd (1) Considering the previous adjustment we made, the third image will jump on the top before the first image slides. It only happens in this situation because the next image that moves after the first image is the last image which has a higher order in the DOM. The rest of the images are fine because we have N, then N - 1, then we go from 3 to 2, and 2 to 1… but then we go from 1 to N.

To avoid that, we will use the following keyframes for the last image:

@keyframes slide-last { 0% { transform: translateX(0%); z-index: 2;} 16.66% { transform: translateX(120%); z-index: 2; } 16.67% { transform: translateX(120%); z-index: 1; } /* we update the z-order here */ 33.34% { transform: translateX(0%); z-index: 1; } 83.33% { transform: translateX(0%); z-index: 1; } 83.34% { transform: translateX(0%); z-index: 2; } /* and also here */ 100% { transform: translateX(0%); z-index: 2; }} We reset the z-index value 5/6 through the animation (instead of two-thirds) which is when the first image is out of the pile. So we don’t see any jumping!

CodePen Embed FallbackTADA! Our infinite slider is now perfect! Here’s our final code in all its glory:

.gallery > img { animation: slide 6s infinite;}.gallery > img:last-child { animation-name: slide-last;}.gallery > img:nth-child(2) { animation-delay: -2s; } .gallery > img:nth-child(3) { animation-delay: -4s; }@keyframes slide { 0% { transform: translateX(0%); z-index: 2; } 16.66% { transform: translateX(120%); z-index: 2; } 16.67% { transform: translateX(120%); z-index: 1; } 33.34% { transform: translateX(0%); z-index: 1; } 66.33% { transform: translateX(0%); z-index: 1; } 66.34% { transform: translateX(0%); z-index: 2; } 100% { transform: translateX(0%); z-index: 2; }}@keyframes slide-last { 0% { transform: translateX(0%); z-index: 2; } 16.66% { transform: translateX(120%); z-index: 2; } 16.67% { transform: translateX(120%); z-index: 1; } 33.34% { transform: translateX(0%); z-index: 1; } 83.33% { transform: translateX(0%); z-index: 1; } 83.34% { transform: translateX(0%); z-index: 2; } 100% { transform: translateX(0%); z-index: 2; }} Supporting any number of imagesNow that our animation works for three images, let’s make it work for any number (N) of images. But first, we can optimize our work a little by splitting the animation up to avoid redundancy:

.gallery > img { z-index: 2; animation: slide 6s infinite, z-order 6s infinite steps(1);}.gallery > img:last-child { animation-name: slide, z-order-last;}.gallery > img:nth-child(2) { animation-delay: -2s; } .gallery > img:nth-child(3) { animation-delay: -4s; }@keyframes slide { 16.67% { transform: translateX(120%); } 33.33% { transform: translateX(0%); }}@keyframes z-order { 16.67%, 33.33% { z-index: 1; } 66.33% { z-index: 2; }}@keyframes z-order-last { 16.67%, 33.33% { z-index: 1; } 83.33% { z-index: 2; }} Way less code now! We make one animation for the sliding part and another one for the z-index updates. Note that we use steps(1) on the z-index animation. That’s because I want to abruptly change the z-index value, unlike the sliding animation where we want smooth movement.

Now that the code is easier to read and maintain, we have a better view for figuring out how to support any number of images. What we need to do is update the animation delays and the percentages of the keyframes. The delay are easy because we can use the exact same loop we made in the last article to support multiple images in the circular slider:

@for $i from 2 to ($n + 1) { .gallery > img:nth-child(#{$i}) { animation-delay: calc(#{(1 - $i)/$n}*6s); }} That means we’re moving from vanilla CSS to Sass. Next, we need to imagine how the timeline scale with N images. Let’s not forget that the animation happens in three phases:

After “slide to right” and “slide to left”, the image should stay put until the rest of the images go through the sequence. So the “don’t move” part needs to take the same amount of time as (N - 1) as “slide to right” and “slide to left”. And within one iteration, N images will slide. So, “slide to right” and “slide to left” both take 100%/N of the total animation timeline. The image slides away from the pile at (100%/N)/2 and slides back at 100%/N .

We can change this:

@keyframes slide { 16.67% { transform: translateX(120%); } 33.33% { transform: translateX(0%); }} …to this:

@keyframes slide { #{50/$n}% { transform: translateX(120%); } #{100/$n}% { transform: translateX(0%); }} If we replace N with 3, we get 16.67% and 33.33% when there are 3 images in the stack. It’s the same logic with the stacking order where we will have this:

@keyframes z-order { #{50/$n}%, #{100/$n}% { z-index: 1; } 66.33% { z-index: 2; }} We still need to update the 66.33% point. That’s supposed to be where the image resets its z-index before the end of the animation. At that same time, the next image starts to slide. Since the sliding part takes 100%/N, the reset should happen at 100% - 100%/N:

@keyframes z-order { #{50/$n}%, #{100/$n}% { z-index: 1; } #{100 - 100/$n}% { z-index: 2; }} But for our z-order-last animation to work, it should happen a bit later in the sequence. Remember the fix we did for the last image? Resetting the z-index value needs to happen when the first image is out of the pile and not when it starts sliding. We can use the same reasoning here in our keyframes:

@keyframes z-order-last { #{50/$n}%, #{100/$n}% { z-index: 1; } #{100 - 50/$n}% { z-index: 2; }} We are done! Here’s what we get when using five images:

CodePen Embed FallbackWe can add a touch of rotation to make things a bit fancier:

CodePen Embed FallbackAll I did is append rotate(var(--r)) to the transform property. Inside the loop, --r is defined with a random angle:

@for $i from 1 to ($n + 1) { .gallery > img:nth-child(#{$i}) { --r: #{(-20 + random(40))*1deg}; /* a random angle between -20deg and 20deg */ }} The rotation creates small glitches as we can sometimes see some of the images jumping to the back of the stack, but it’s not a big deal.

Wrapping upAll that z-index work was a big balancing act, right? If you were unsure how stacking order work before this exercise, then you probably have a much better idea now! If you found some of the explanations hard to follow, I highly recommend you to take another read of the article and map things out with pencil and paper. Try to illustrate each step of the animation using a different number of images to better understand the trick.

Last time, we used a few geometry tricks to create a circular slider that rotates back to the first image after a full sequence. This time, we accomplished a similar trick using z-index. In both cases, we didn’t duplicate any of the images to simulate a continuous animation, nor did we reach for JavaScript to help with the calculations.

Next time, we will make 3D sliders. Stay tuned!


CSS Infinite Slider Flipping Through Polaroid Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

How often to do you reach for the CSS background-size property? If you’re like me — and probably lots of other front-end folks — then it’s usually when you background-size: cover an image to fill the space of an entire element.

Well, I was presented with an interesting challenge that required more advanced background sizing: background stripes that transition on hover. Check this out and hover it with your cursor:

CodePen Embed FallbackThere’s a lot more going on there than the size of the background, but that was the trick I needed to get the stripes to transition. I thought I’d show you how I arrived there, not only because I think it’s a really nice visual effect, but because it required me to get creative with gradients and blend modes that I think you might enjoy.

Let’s start with a very basic setup to keep things simple. I’m talking about a single <div> in the HTML that’s styled as a green square:

```

```

div { width: 500px; height: 500px; background: palegreen;} Setting up the background stripesIf your mind went straight to a CSS linear gradient when you saw those stripes, then we’re already on the same page. We can’t exactly do a repeating gradient in this case since we want the stripes to occupy uneven amounts of space and transition them, but we can create five stripes by chaining five backgrounds on top of our existing background color and placing them to the top-right of the container:

div { width: 500px; height: 500px; background: linear-gradient(black, black) top right, linear-gradient(black, black) top 100px right, linear-gradient(black, black) top 200px right, linear-gradient(black, black) top 300px right, linear-gradient(black, black) top 400px right, palegreen;} I made horizontal stripes, but we could also go vertical with the approach we’re covering here. And we can simplify this quite a bit with custom properties:

div { --gt: linear-gradient(black, black); --n: 100px; width: 500px; height: 500px; background: var(--gt) top right, var(--gt) top var(--n) right, var(--gt) top calc(var(--n) * 2) right, var(--gt) top calc(var(--n) * 3) right, var(--gt) top calc(var(--n) * 4) right, palegreen;} So, the --gt value is the gradient and --n is a constant we’re using to nudge the stripes downward so they are offset vertically. And you may have noticed that I haven’t set a true gradient, but rather solid black stripes in the linear-gradient() function — that’s intentional and we’ll get to why I did that in a bit.

One more thing we ought to do before moving on is prevent our backgrounds from repeating; otherwise, they’ll tile and fill the entire space:

div { --gt: linear-gradient(black, black); --n: 100px; width: 500px; height: 500px; background: var(--gt) top right, var(--gt) top var(--n) right, var(--gt) top calc(var(--n) * 2) right, var(--gt) top calc(var(--n) * 3) right, var(--gt) top calc(var(--n) * 4) right, palegreen; background-repeat: no-repeat;} We could have set background-repeat in the background shorthand, but I decided to break it out here to keep things easy to read.

Offsetting the stripesWe technically have stripes, but it’s pretty tough to tell because there’s no spacing between them and they cover the entire container. It’s more like we have a solid black square.

This is where we get to use the background-size property. We want to set both the height and the width of the stripes and the property supports a two-value syntax that allows us to do exactly that. And, we can chain those sizes by comma separating them the same way we did on background.

Let’s start simple by setting the widths first. Using the single-value syntax for background-size sets the width and defaults the height to auto. I’m using totally arbitrary values here, so set the values to what works best for your design:

div { --gt: linear-gradient(black, black); --n: 100px; width: 500px; height: 500px; background: var(--gt) top right, var(--gt) top var(--n) right, var(--gt) top calc(var(--n) * 2) right, var(--gt) top calc(var(--n) * 3) right, var(--gt) top calc(var(--n) * 4) right, palegreen; background-repeat: no-repeat; background-size: 60%, 90%, 70%, 40%, 10%;} If you’re using the same values that I am, you’ll get this:

CodePen Embed FallbackDoesn’t exactly look like we set the width for all the stripes, does it? That’s because of the auto height behavior of the single-value syntax. The second stripe is wider than the others below it, and it is covering them. We ought to set the heights so we can see our work. They should all be the same height and we can actually re-use our --n variable, again, to keep things simple:

div { --gt: linear-gradient(black, black); --n: 100px; width: 500px; height: 500px; background: var(--gt) top right, var(--gt) top var(--n) right, var(--gt) top calc(var(--n) * 2) right, var(--gt) top calc(var(--n) * 3) right, var(--gt) top calc(var(--n) * 4) right, palegreen; background-repeat: no-repeat; background-size: 60% var(--n), 90% var(--n), 70% var(--n), 40% var(--n), 10% var(--n); // HIGHLIGHT 15} Ah, much better!

CodePen Embed FallbackAdding gaps between the stripesThis is a totally optional step if your design doesn’t require gaps between the stripes, but mine did and it’s not overly complicated. We change the height of each stripe’s background-size a smidge, decreasing the value so they fall short of filling the full vertical space.

We can continue to use our --n variable, but subtract a small amount, say 5px, using calc() to get what we want.

background-size: 60% calc(var(--n) - 5px), 90% calc(var(--n) - 5px), 70% calc(var(--n) - 5px), 40% calc(var(--n) - 5px), 10% calc(var(--n) - 5px); That’s a lot of repetition we can eliminate with another variable:

div { --h: calc(var(--n) - 5px); /* etc. */ background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);} CodePen Embed FallbackMasking and blendingNow let’s swap the palegreen background color we’ve been using for visual purposes up to this point for white.

div { /* etc. */ background: var(--gt) top right, var(--gt) top var(--n) right, var(--gt) top calc(var(--n) * 2) right, var(--gt) top calc(var(--n) * 3) right, var(--gt) top calc(var(--n) * 4) right, #fff; /* etc. */} A black and white pattern like this is perfect for masking and blending. To do that, we’re first going to wrap our <div> in a new parent container and introduce a second <div> under it:

```

`` We’re going to do a little CSS re-factoring here. Now that we have a new parent container, we can pass the fixedwidthandheightproperties we were using on our

` over there:

section { width: 500px; height: 500px;} I’m also going to use CSS Grid to position the two <div> elements on top of one another. This is the same trick Temani Afif uses to create his super cool image galleries. The idea is that we place both divs over the full container using the grid-area property and align everything toward the center:

section { display: grid; align-items: center; justify-items: center; width: 500px; height: 500px;} section > div { width: inherit; height: inherit; grid-area: 1 / 1;} Now, check this out. The reason I used a solid gradient that goes from black to black earlier is to set us up for masking and blending the two <div> layers. This isn’t true masking in the sense that we’re calling the mask property, but the contrast between the layers controls what colors are visible. The area covered by white will remain white, and the area covered by black leaks through. MDN’s documentation on blend modes has a nice explanation of how this works.

To get that working, I’ll apply the real gradient we want to see on the first <div> while applying the style rules from our initial <div> on the new one, using the :nth-child() pseudo-selector:

div:nth-child(1) { background: linear-gradient(to right, red, orange); }div:nth-child(2) { --gt: linear-gradient(black, black); --n: 100px; --h: calc(var(--n) - 5px); background: var(--gt) top right, var(--gt) top var(--n) right, var(--gt) top calc(var(--n) * 2) right, var(--gt) top calc(var(--n) * 3) right, var(--gt) top calc(var(--n) * 4) right, white; background-repeat: no-repeat; background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);} If we stop here, we actually won’t see any visual difference from what we had before. That’s because we haven’t done the actual blending yet. So, let’s do that now using the screen blend mode:

div:nth-child(2) { /* etc. */ mix-blend-mode: screen;} CodePen Embed FallbackI used a beige background color in the demo I showed at the beginning of this article. That slightly darker sort of off-white coloring allows a little color to bleed through the rest of the background:

CodePen Embed FallbackThe hover effectThe last piece of this puzzle is the hover effect that widens the stripes to full width. First, let’s write out our selector for it. We want this to happen when the parent container (<section> in our case) is hovered. When it’s hovered, we’ll change the background size of the stripes contained in the second <div>:

/* When <section> is hovered, change the second div's styles */section:hover > div:nth-child(2){ /* styles go here */} We’ll want to change the background-size of the stripes to the full width of the container while maintaining the same height:

section:hover > div:nth-child(2){ background-size: 100% var(--h);} That “snaps” the background to full-width. If we add a little transition to this, then we see the stripes expand on hover:

section:hover > div:nth-child(2){ background-size: 100% var(--h); transition: background-size 1s;} Here’s that final demo once again:

CodePen Embed FallbackI only added text in there to show what it might look like to use this in a different context. If you do the same, then it’s worth making sure there’s enough contrast between the text color and the colors used in the gradient to comply with WCAG guidelines. And while we’re touching briefly on accessibility, it’s worth considering user preferences for reduced motion when it comes to the hover effect.

That’s a wrap!Pretty neat, right? I certainly think so. What I like about this, too, is that it’s pretty maintainable and customizable. For example, we can alter the height, colors, and direction of the stripes by changing a few values. You might even variablize a few more things in there — like the colors and widths — to make it even more configurable.

I’m really interested if you would have approached this a different way. If so, please share in the comments! It’d be neat to see how many variations we can collect.


Animated Background Stripes That Transition on Hover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I stumbled across this tweet from Ana Segota looking for a way to add a CSS box-shadow to a button’s hover state in WordPress in the theme.json file.

Is it possible to add a box-shadow for the button on hover state in theme.json? #WordPress

— Ana Segota (@Ana_Segota) November 1, 2022

She’s asking because theme.json is where WordPress wants us to start moving basic styles for block themes. Traditionally, we’d do any and all styling in style.css when working in a “classic” theme. But with the default Twenty Twenty-Three (TT3) theme that recently shipped with WordPress 6.1 moving all of its styles to theme.json, we’re getting closer and closer to being able to do the same with our own themes. I covered this in great detail in a recent article.

I say “closer and closer” because there are still plenty of CSS properties and selectors that are unsupported in theme.json. For example, if you’re hoping to style something with like perspective-origin in theme.json, it just won’t happen — at least as I’m writing this today.

Ana is looking at box-shadow and, luckily for her, that CSS property is supported by theme.json as of WordPress 6.1. Her tweet is dated Nov. 1, the same exact day that 6.1 released. It’s not like support for the property was a headline feature in the release. The bigger headlines were more related to spacing and layout techniques for blocks and block themes.

Here’s how we can apply a box-shadow to a specific block — say the Featured Image block — in theme.json:

{ "version": 2, "settings": {}, // etc. "styles": { "blocks" :{ "core/post-featured-image": { "shadow": "10px 10px 5px 0px rgba(0, 0, 0, 0.66)" } } }} Wondering if the new color syntax works? Me too! But when I tried — rgb(0 0 0 / 0.66) — I got nothing. Perhaps that’s already in the works or could use a pull request.

Easy, right? Sure, it’s way different than writing vanilla CSS in style.css and takes some getting used to. But it is indeed possible as of the most recent WordPress release.

And, hey, we can do the same thing to individual “elements”, like a button. A button is a block in and of itself, but it can also be a nested block within another block. So, to apply a box-shadow globally to all buttons, we’d do something like this in theme.json:

{ "version": 2, "settings": {}, // etc. "styles": { "elements": { "button": { "shadow": "10px 10px 5px 0px rgba(0,0,0,0.66)" } } }} But Ana wants to add the shadow to the button’s :hover state. Thankfully, support for styling interactive states for certain elements, like buttons and links, using pseudo-classes — including :hover, :focus, :active, and :visited — also gained theme.json support in WordPress 6.1.

{ "version": 2, "settings": {}, // etc. "styles": { "elements": { "button": { ":hover": { "shadow": "10px 10px 5px 0px rgba(0,0,0,0.66)" } } } }} If you’re using a parent theme, you can certainly override a theme’s styles in a child theme. Here, I am completely overriding TT3’s button styles.

View full code
{ "version": 2, "settings": {}, // etc. "styles": { "elements": { "button": { "border": { "radius": "0" }, "color": { "background": "var(--wp--preset--color--tertiary)", "text": "var(--wp--preset--color--contrast)" }, "outline": { "offset": "3px", "width": "3px", "style": "dashed", "color": "red" }, "typography": { "fontSize": "var(--wp--preset--font-size--medium)" }, "shadow": "5px 5px 5px 0px rgba(9, 30, 66, 0.25), 5px 5px 5px 1px rgba(9, 30, 66, 0.08)", ":hover": { "color": { "background": "var(--wp--preset--color--contrast)", "text": "var(--wp--preset--color--base)" }, "outline": { "offset": "3px", "width": "3px", "style": "solid", "color": "blue" } }, ":focus": { "color": { "background": "var(--wp--preset--color--contrast)", "text": "var(--wp--preset--color--base)" } }, ":active": { "color": { "background": "var(--wp--preset--color--secondary)", "text": "var(--wp--preset--color--base)" } } } } }} Here’s how that renders:

The button’s natural state (left) and it’s hovered state (right)Another way to do it: custom stylesThe recently released Pixl block theme provides another example of real-world usage of the box-shadow property in theme.json using an alternative method that defines custom values. In the theme, a custom box-shadow property is defined as .settings.custom.shadow:

{ "version": 2, "settings": { // etc. "custom": { // etc. "shadow": "5px 5px 0px -2px var(--wp--preset--color--background), 5px 5px var(--wp--preset--color--foreground)" }, // etc. }} Then, later in the file, the custom shadow property is called on a button element:

{ "version": 2, "settings": { // etc. }, "styles": { "elements": { "button": { // etc. "shadow": "var(--wp--custom--shadow) !important", // etc. ":active": { // etc. "shadow": "2px 2px var(--wp--preset--color--primary) !important" } }, // etc. }} I’m not totally sure about the use of !important in this context. My hunch is that it’s an attempt to prevent overriding those styles using the Global Styles UI in the Site Editor, which has high specificity than styles defined in theme.json. Here’s an anchored link to more information from my previous article on managing block theme styles.

Update: Turns out there was a whole discussion about this in Pull Request #34689, which notes that it was addressed in WordPress 5.9.

And there’s more…In addition to shadows, the CSS outline property also gained theme.json support in WordPress 6.1 and can be applied to buttons and their interactive states. This GitHub PR shows a good example.

"elements": { "button": { "outline": { "offset": "3px", "width": "3px", "style": "dashed", "color": "red" }, ":hover": { "outline": { "offset": "3px", "width": "3px", "style": "solid", "color": "blue" } } }} You can also find the real examples of how the outline property works in other themes, including Loudness, Block Canvas, and Blockbase.

Wrapping upWho knew there was so much to talk about with a single CSS property when it comes to block theming in WordPress 6.1? We saw the officially supported methods for setting a box-shadow on blocks and individual elements, including the interactive states of a button element. We also checked out how we could override shadows in a child theme. And, finally, we cracked open a real-world example that defines and sets shadows in a custom property.

You can find more detailed in-depth discussions about the WordPress and it’s box-shadow implementation in this GitHub PR. There is also a GitHub proposal for adding UI directly in WordPress to set shadow values on blocks — you can jump directly to an animated GIF showing how that would work.

Speaking of which, Justin Tadlock recently developed a block that renders a progress bar and integrated box shadow controls into it. He shows it off in this video:

More informationIf you’d like to dig deeper into the box-shadow and other CSS properties that are supported by the theme.json file in a block theme, here are a couple of resources you can use:

  • Managing CSS Styles in a WordPress Block Theme (CSS-Tricks)
  • Styling elements in block themes (Dev Notes)
  • A Walk-Through of Layout Classes in WordPress 6.1 (Gutenberg Times)
  • How to add box-shadows with theme.json (Full Site Editing)
  • Box-Shadow: Add UI tools to set box shadow to a block (Gutenberg Pull Request #45507)
  • Add box-shadow support for blocks via theme.json (Gutenberg Pull Request #41972)
  • Added outline support for blocks via theme.json (Gutenberg Pull Request #43526)

Adding Box Shadows to WordPress Blocks and Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Nothing but ear-to-ear smiles as I was watching this video from @quayjn on YouTube. (No actual name in the byline, though I think it’s Brian Katz if my paper trail is correct).

The best is this Pen you can use to sing along…

CodePen Embed FallbackThe little song Una did for memorizing for JavaScript’s map(), filter(), and reduce()methods at the end of this article comes to mind for sure.

To Shared Link — Permalink on CSS-Tricks


CSS is OK, I guess. originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

For years, a small pedantry war has been raging in our address bars. In one corner are brands like Google, Instagram, and Facebook. This group has chosen to redirect example.com to www.example.com. In the opposite corner: GitHub, DuckDuckGo, and Discord. This group has chosen to do the reverse and redirect www.example.com to example.com.

Does “WWW” belong in a URL? Some developers hold strong opinions on the subject. We’ll explore arguments for and against it after a bit of history.

What’s with the Ws?The three Ws stand for “World Wide Web”, a late-1980s invention that introduced the world to browsers and websites. The practice of using “WWW” stems from a tradition of naming subdomains after the type of service they provide:

  • a web server at www.example.com
  • an FTP server at ftp.example.com
  • an IRC server at irc.example.com

WWW-less domain concern 1: Leaking cookies to subdomainsCritics of “WWW-less” domains have pointed out that in certain situations, subdomain.example.com would be able to read cookies set by example.com. This may be undesirable if, for example, you are a web hosting provider that lets clients operate subdomains on your domain. While the concern is valid, the behavior was specific to Internet Explorer.

RFC 6265 standardizes how browsers treat cookies and explicitly calls out this behavior as incorrect.

Another potential source of leaks is the Domain value of any cookies set by example.com. If the Domain value is explicitly set to example.com, the cookies will also be exposed to its subdomains.

| Cookie value | Exposed to example.com | Exposed to subdomain.example.com | | --- | --- | --- | | secret=data | ✅ | ❌ | | secret=data; Domain=example.com | ✅ | ✅ |

In conclusion, as long as you don’t explicitly set a Domain value and your users don’t use Internet Explorer, no cookie leaks should occur.

WWW-less domain concern 2: DNS headachesSometimes, a “WWW-less” domain may complicate your Domain Name System (DNS) setup.

When a user types example.com into their browser’s address bar, the browser needs to know the Internet Protocol (IP) address of the web server they’re trying to visit. The browser requests this IP address from your domain’s nameservers – usually indirectly through the DNS servers of the user’s Internet Service Provider (ISP). If your nameservers are configured to respond with an A record containing the IP address, a “WWW-less” domain will work fine.

In some cases, you may want to instead use a Canonical Name (CNAME) record for your website. Such a record can declare that www.example.com is an alias of example123.somecdnprovider.com, which tells the user’s browser to instead look up the IP address of example123.somecdnprovider.com and send the HTTP request there.

Notice that the example above used a WWW subdomain. It’s not possible to define a CNAME record for example.com. As per RFC 1912, CNAME records cannot coexist with other records. If you tried to define a CNAME record for example.com, the Nameserver (NS) records for example.com containing the IP addresses of the domain’s name servers would not be allowed to exist. As a result, browsers would not be able to figure out where your name servers are.

Some DNS providers will allow you to work around this limitation. Cloudflare calls their solution CNAME flattening. With this technique, domain administrators configure a CNAME record, but their nameservers will expose an A record.

For instance, if the administrator configures a CNAME record for example.com pointing to example123.somecdnprovider.com, and an A record for example123.somecdnprovider.com exists pointing to 1.2.3.4, then Cloudflare would expose an A record for example.com pointing to 1.2.3.4.

In conclusion, while the concern is valid for domain owners who wish to use CNAME records, certain DNS providers now offer a suitable workaround.

WWW-less benefitsMost of the arguments against WWW are practical or cosmetic. “No-WWW” advocates have argued that it’s easier to say and type example.com than www.example.com (which may be less confusing for less tech-savvy users).

Opponents of the WWW subdomain have also pointed out that dropping it comes with a humble performance advantage. Website owners could shave 4 bytes off each HTTP request by doing so. While these savings could add up for high-traffic websites like Facebook, bandwidth generally isn’t a scarce resource.

WWW benefitsOne practical argument in favor of WWW is in situations with newer top-level domains. For example, www.example.miami is immediately recognizable as a web address when example.miami isn’t. This is less of a concern for sites that have recognizable top-level domains like .com.

Impact on your search engine rankingThe current consensus is that your choice does not influence your search engine performance. If you wish to migrate from one to the other, you’ll want to configure permanent redirects (HTTP 301) instead of temporary ones (HTTP 302). Permanent redirects ensure that the SEO value of your old URLs transfers to the new ones.

Tips for supporting bothSites typically pick either example.com or www.example.com as their official website and configure HTTP 301 redirects for the other. In theory, it is possible to support both www.example.com and example.com. In practice, the costs may outweigh the benefits.

From a technical perspective, you’ll want to verify that your tech stack can handle it. Your content management system (CMS) or statically generated site would have to output internal links as relative URLs to preserve the visitor’s preferred hostname. Your analytics tools may log traffic to both hostnames separately unless you can configure the hostnames as aliases.

Lastly, you’ll need to take an extra step to safeguard your search engine performance. Google will consider the “WWW” and “non-WWW” versions of a URL to be duplicate content. To deduplicate content in its search index, Google will display whichever of the two it thinks the user will prefer – for better or worse.

To preserve control over how you appear in Google, it recommends inserting canonical link tags. First, decide which hostname will be the official (canonical) one.

For example, if you pick www.example.com, you will have to insert the following snippet in the <head> tag on https://example.com/my-article:

<link href="https://www.example.com/my-article" rel="canonical"> This snippet indicates to Google that the “WWW-less” variant represents the same content. In general, Google will prefer the version you’ve marked as canonical in search results, which would be the “WWW” variant in this example.

ConclusionDespite intense campaigning on either side, both approaches remain valid as long as you are aware of the benefits and limitations. To cover all your bases, be sure to set up permanent redirects from one to the other and you’re all set.


Does WWW still belong in URLs? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Image sliders (also called carousels) are everywhere. There are a lot of CSS tricks to create the common slider where the images slide from left to right (or the opposite). It’s the same deal with the many JavaScript libraries out …


CSS Infinite and Circular Rotating Image Slider originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I wrote up some early thoughts on container style queries a little while back. It’s still early days. They’re already defined in the CSS Containment Module Level 1 specification (currently in Editor’s Draft status) but there’s still a couple of …


Digging Deeper Into Container Style Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

One of the main goals of the WordPress Site Editor (and, yes, that is now the “official” name) is to move basic block styling from CSS to structured JSON. JSON files are machine-readable, which makes it consumable by …


Using The New Constrained Layout In WordPress Block Themes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I’m a sucker for anything about front-end job titles.

Anselm Hannemann:

CSS evolved and we’re beyond the point where everyone can just do it as a side interest. We all can learn it and build amazing stuff with it,


More Than “Slapping Paint on a Website” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

HTML lists are boring. They don’t do much, so we don’t really think about them despite how widely used they are. And we’re still able to do the same things we’ve always done to customize them, like removing markers, reversing …


Newer Things to Know About Good Ol’ HTML Lists originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Well, color me this! I was griping to myself last night about just how gosh dang hard it is to read text messages in Apple Messages. You know, not the blue bubbles that you get when messaging other iPhone users. …


Apple Messages & Color Contrast originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Well, hey check this out. Looks like there is a brand spankin’ new blog over at WordPress.org all about WordPress development. In the original proposal for the blog, Birgit Pauli-Haak writes:

The Make Core blog has a heavy


WordPress Developer Blog originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format can be polarizing. But it is – at least in my Twitter bubble – one of the better-liked CSS methodologies.

Personally, I think …


Taming the Cascade With BEM and Modern CSS Selectors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

What I will be doing here is kind of an experiment to explore tricks that leverage a bug with the way CSS gradients handle sub-pixel rendering to create a static noise effect — like you might see on a TV with no signal.


Making Static Noise From a Weird CSS Gradient Bug originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

So far, we’ve covered how to work with data from an external API in a custom WordPress block. We walked through the process of fetching that data for use on the front end of a WordPress site, and how to …


Creating a Settings UI for a Custom WordPress Block originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Hey folks! If you’ve been keeping up with the latest DigitalOcean news, you might be aware that we recently announced our acquisition of a company called Cloudways. In case you’re curious about what this means, we thought it might …


DigitalOcean Welcomes Cloudways to the Family originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I’m often asked where to learn web development. The answer varies, of course, and we’ve published a few posts on the topic over the years, the most recent of which was Chris taking a stab at different learning paths in …


Finding Front-End Development Scholarships originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Is it Fall? Winter? I don’t know, but I woke up with snow in the front yard this morning and felt like it was time to write a little update about what’s been happening around CSS-Tricks this past month, as …


Behind the CSScenes, November 2022 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

In this article we will be diving into the world of scrollbars. I know, it doesn’t sound too glamorous, but trust me, a well-designed page goes hand-in-hand with a matching scrollbar. The old-fashioned chrome scrollbar just doesn’t fit in as …


Classy and Cool Custom CSS Scrollbars: A Showcase originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

After Part 1 and Part 2, I am back with a third article to explore more fancy shapes. Like the previous articles, we are going to combine CSS Grid with clipping and masking to create fancy layouts for image …


CSS Grid and Custom Shapes, Part 3 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Sacha Greif openly wondered whether CSS has gotten to be, you know, too big. With all the goodies that’ve shipped in browsers the past couple of years — container queries! relative color syntax! cascade layers! logical properties…


What CSS Do You Absolutely Have to Know in 2022? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The way we write CSS for WordPress themes is in the midst of sweeping changes. I recently shared a technique for adding fluid type support in WordPress by way of theme.json, a new file that WordPress has been pushing …


Managing CSS Styles in a WordPress Block Theme originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

“A change to overflow on replaced elements in CSS”:

From Chrome 108, the following replaced elements respect the overflow property: imgvideo and canvas. In earlier versions of Chrome, this property was ignored on these elements.

This


A Couple Changes Coming in Chrome 108 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

“A change to overflow on replaced elements in CSS”:

From Chrome 108, the following replaced elements respect the overflow property: imgvideo and canvas. In earlier versions of Chrome, this property was ignored on these elements.

This


A Couple Changes Coming in Chrome 108 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Web Sockets, Web Workers, Service Workers… these are terms you may have read or overheard. Maybe not all of them, but likely at least one of them. And even if you have a good handle on front-end development, there’s a …


The Difference Between Web Sockets, Web Workers, and Service Workers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Web Sockets, Web Workers, Service Workers… these are terms you may have read or overheard. Maybe not all of them, but likely at least one of them. And even if you have a good handle on front-end development, there’s a …


The Difference Between Web Sockets, Web Workers, and Service Workers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Every once in a while, the blogging zeitgiest seems to coalesce around a certain topic and it’s like the saved articles in my bookmarks folder are having a conversation. The conversation sitting in there now is all about CSS Gradients …


Some Links About CSS Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

This is a continuation of my last article about “Rendering External API Data in WordPress Blocks on the Front End”. In that last one, we learned how to take an external API and integrate it with a block that …


Rendering External API Data in WordPress Blocks on the Back End originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The Media Queries Level 4 specification has introduced a new syntax for targeting a range of viewport widths using common mathematical comparison operators, like <, >, and =, that make more sense syntactically while writing less code for responsive web design.


The New CSS Media Query Range Syntax originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

We’ve spent the last two articles in this three-part series playing with gradients to make really neat image decorations using nothing but the <img> element. In this third and final piece, we are going to explore more techniques using the …


Fancy Image Decorations: Outlines and Complex Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Simon Goellner (@simeydotme)’s collection of Holographic Trading Cards have captured our attention.

Under the hood there is a suite of filter(), background-blend-mode(), mix-blend-mode(), and clip-path() combinations that have been painstakingly tweaked to reach the desired effect. I …


Holographic Trading Card Effect originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The CSS :has() pseudo class is rolling out in many browsers with Chrome and Safari already fully supporting it. It’s often referred to it as “the parent selector” — as in, we can select style a parent element from a …


Creating Animated, Clickable Cards With the :has() Relational Pseudo Class originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

As front-end developers, we’ve wished for a lot of things over the years — ways to center things in CSS, encapsulate styles, set an element’s aspect ratio, get finer-grained control over our colors, select an element based on its children’s …


Is There Too Much CSS Now? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Welcome to Part 2 of this three-part series! We are still decorating images without any extra elements and pseudo-elements. I hope you already took the time to digest Part 1 because we will continue working with a lot of gradients …


Fancy Image Decorations: Masks and Advanced Hover Effects originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I love it when there’s a sense of synergy in the blogosphere. First, I caught Nick Heer’s coverage of Meta ending support for Instant Articles, its proprietary format for stripped-down performant news articles. He also compares it to the similar …


Instant Articles, Proprietary Syndication, and a Web Built on User Fidelity Preferences originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Before I career jumped into development, I did a bunch of motion graphics work in After Effects. But even with that background, I still found animating on the web pretty baffling.

Video graphics are designed within a specific ratio and …


Responsive Animations for Every Screen Size and Device originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

When you put something — say a regular sheet of paper — in a manilla folder, a part of that thing might peek out of the folder a little bit. The same sort of thing with a wallet and credit …


How to Make a Folder “Slit” Effect With CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

By way of a post by Manuel Matuzović which is by way of a demo by Temani Afif.

.wrapper { margin-inline: max(0px, ((100% - 64rem) / 2)); }

You’d be doing yourself a favor to read Manuel’s breakdown of …


Manuel Matuzovic: max() Trickery originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Besides being elegant, Bezier curves have nice mathematical properties due to their definition and construction. No wonder they are widely used in so many areas! Now, how about using Bezier curves as motion paths with CSS?


Pure CSS Bezier Curve Motion Paths originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Well, hey, welcome back to Behind the CSScenes! These posts are like little check-ins we’re doing each month to give you a peek behind what we’re doing here at CSS-Tricks, as well as a chance for us to pause …


Behind the CSScenes, October 2022 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

As the title says, we are going to decorate images! There’s a bunch of other articles out there that talk about this, but what we’re covering here is quite a bit different because it’s more of a challenge. The challenge? …


Fancy Image Decorations: Single Element Magic originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Fluid typography is a fancy way of “describing font properties, such as size or line height, that scale fluidly according to the size of the viewport”. It’s also known by other names, like responsive typography, flexible type, fluid type, …


Adding Fluid Typography Support to WordPress Block Themes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Spammers are a huge deal nowadays. If you want to share your contact information without getting overwhelmed by spam email you need a solution. I run into this problem a few months ago. While I was researching how to solve …


How to Safely Share Your Email Address on a Website originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

In my previous post we looked at Shoelace, which is a component library with a full suite of UX components that are beautiful, accessible, and — perhaps unexpectedly — built with Web Components. This means they can be used …


Using Web Components With Next (or Any SSR Framework) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The State of CSS survey recently opened up. Last year, the survey confirmed everyone’s assumptions that TailwindCSS is super popular and CSS variables are mainstream. It also codified what many of us want from CSS, from Container Queries to …


State of CSS 2022 Survey Now Open originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

This is a post about Shoelace, a component library by Cory LaViska, but with a twist. It defines all your standard UX components: tabs, modals, accordions, auto-completes, and much, much more. They look beautiful out of the …


Introducing Shoelace, a Framework-Independent Component-Based UX Library originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Let’s acknowledge that developing for WordPress is weird right now. Whether you’re new to WordPress or have worked with it for eons, the introduction of “Full-Site Editing” (FSE) features, including the Block Editor (WordPress 5.0) and the Site Editor (WordPress …


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

View Details

So you want an auto-playing looping video without sound? In popular vernacular this is the very meaning of the word GIF. The word has stuck around but the image format itself is ancient and obsolete. Twitter, for example, has …


GIFs Without the .gif: The Most Performant Image and Video Options Right Now originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Did you know that DOM elements with IDs are accessible in JavaScript as global variables? It’s one of those things that’s been around, like, forever but I’m really digging into it for the first time.

If this is the first …


Named Element IDs Can Be Referenced as JavaScript Globals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The wave is probably one of the most difficult shapes to make in CSS. We always try to approximate it with properties like border-radius and lots of magic numbers until we get something that feels kinda close. And that’s before …


How to Create Wavy Shapes & Patterns in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

If we browse the WordPress theme directory, a majority of themes showcase cover images. It is a feature in popular demand. The cover page trend is true even in the block theme directory screenshots as well.

Let’s consider the …


How To Customize WordPress Block Theme Cover Templates with Dynamic Post Feature Images originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The video of Chris Coyier’s talk at CascadiaJS 2022 is now available. It’s his first in-person talk in more than two years, so it’s great to see our good friend back on stage slinging gems on what makes the web …


The Web is Good Now originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover …


How I Made an Icon System Out of CSS Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

The latest spec:

A style rule is a qualified rule that associates a selector list with a list of property declarations and possibly a list of nested rules. They are also called rule sets in CSS2.

Louis Lazaris:

As the above quote from W3C indicates, it seems


CSS Rules vs. CSS Rulesets originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

On one hand, creating simple checkered backgrounds with CSS is easy. On the other hand, though, unless we are one of the CSS-gradient-ninjas, we are kind of stuck with basic patterns.

At least that’s what I thought while staring at …


CSS Checkerboard Background… But With Rounded Corners and Hover Styles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Gradients have been a part of the CSS spectrum for quite some time now. We see a lot of radial and linear gradients in a lot of projects, but there is one type of gradient that seems to be a …


Making a Real-Time Clock With a Conic Gradient Face originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

A lot of chatter around the ol’ <details> and <summary> elements lately! I saw Lea Verou recently tweet an observation about the element’s display behavior and that sorta splintered into more observations and usage notes from folks, including a revived …


More Details on details originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Using CSS, it’s possible to prevent users from selecting text within an element using user-select: none. Now, it’s understandable why doing so might be considered “controversial”. I mean, should we be disabling standard user behaviors? Generally speaking, no, we …


When is it OK to Disable Text Selection? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Whew boy, Safari 16 is officially out in the wild and it packs in a bunch of features, some new and exciting (Subgrid! Container Queries! Font Palettes!) and others we’ve been waiting on for better cross-browser support (Motion Path! Overscroll …


WebKit Features in Safari 16.0 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

You’ve probably heard a lot of hype around one of the newest kids on the framework block, Remix. It may be surprising that it got its start back in 2019, but it was originally only available as a subscription-based premium …


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

View Details

Figma has always encouraged collaboration between developers and designers. It strives on an endless treasury of community-made plugins. Need 3D elements? There’s a plugin for that. Need abstract SVGs? There’s a plugin for that, too.

That said, the design part of Figma has …


Building Interactive Figma Widgets originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ …


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

Browsers are constantly adding new HTML, JavaScript and CSS features. Here are some useful additions to working with forms that you might have missed…

requestSubmit()

Safari 16 will be the final browser to add support for requestSubmit.

Before we …


What’s New With Forms in 2022? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

As the name suggests, overlapping charts visualize two different sets of data in a single diagram. The idea is that the overlapping bars allow us to compare data, say, year-over-year. They are also useful for things like tracking progress for …


Overlapping Bar Charts originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

View Details

CSS-only Wolfenstein is a little project that I made a few weeks ago. It was an experiment with CSS 3D transformations and animations.

Inspired by the FPS demo and another Wolfenstein CodePen, I decided to build my own version. …


Hacking CSS Animation State and Playback Time originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.