Front End Web Development
Experimenting With Scroll-Driven corner-shape Animations
Over the last few years, there’s been a lot of talk about and experimentation with scroll-driven animations. It’s a very shiny feature for sure, and as soon as it’s supported in Firefox (without a flag), it’ll be baseline. It’s part of Interop 2026, so that should be relatively soon. Essentially, scroll-driven animations tie an animation timeline’s position to a scroll position, so if you were 50% scrolled then you’d also be 50% into the animation, and they’re surprisingly easy to set up too.
I’ve been seeing significant interest in the new CSS corner-shape property as well, even though it only works in Chrome for now. This enables us to create corners that aren’t as rounded, or aren’t even rounded at all, allowing for some intriguing shapes that take little-to-no effort to create. What’s even more intriguing though is that corner-shape is mathematical, so it’s easily animated.
Hence, say hello to scroll-driven corner-shape animations (requires Chrome 139+ to work fully):
CodePen Embed Fallback corner-shape in a nutshellReal quick — the different values for corner-shape:
corner-shape keywordsuperellipse() equivalentsquaresuperellipse(infinity)squirclesuperellipse(2)roundsuperellipse(1)bevelsuperellipse(0)scoopsuperellipse(-1)notchsuperellipse(-infinity) CodePen Embed FallbackBut what’s this superellipse() function all about? Well, basically, these keyword values are the result of this function. For example, superellipse(2) creates corners that aren’t quite squared but aren’t quite rounded either (the “squircle”). Whether you use a keyword or the superellipse() function directly, a mathematical equation is used either way, which is what makes it animatable. With that in mind, let’s dive into that demo above.
Animating corner-shapeThe demo isn’t too complicated, so I’ll start off by dropping the CSS here, and then I’ll explain how it works line-by-line:
@keyframes bend-it-like-beckham { from { corner-shape: superellipse(notch); /* or */ corner-shape: superellipse(-infinity); } to { corner-shape: superellipse(square); /* or */ corner-shape: superellipse(infinity); } } body::before { /* Fill viewport */ content: ""; position: fixed; inset: 0; /* Enable click-through */ pointer-events: none; /* Invert underlying layer */ mix-blend-mode: difference; background: white; /* Don’t forget this! */ border-bottom-left-radius: 100%; /* Animation settings */ animation: bend-it-like-beckham; animation-timeline: scroll(); } /* Added to cards */ .no-filter { isolation: isolate; } CodePen Embed FallbackIn the code snippet above, body::before combined with content: "" creates a pseudo-element of the <body> with no content that is then fixed to every edge of the viewport. Also, since this animating shape will be on top of the content, pointer-events: none ensures that we can still interact with said content.
For the shape’s color I’m using mix-blend-mode: difference with background: white, which inverts the underlying layer, a trendy effect that to some degree only maintains the same level of color contrast. You won’t want to apply this effect to everything, so here’s a utility class to exclude the effect as needed:
/* Added to cards */ .no-filter { isolation: isolate; }A comparison:
Left: Full application of blend mode. Right: Blend mode excluded from cards.You’ll need to combine corner-shape with border-radius, which uses corner-shape: round under the hood by default. Yes, that’s right, border-radius doesn’t actually round corners — corner-shape: round does that under the hood. Rather, border-radius handles the x-axis and y-axis coordinates to draw from:
/* Syntax */ border-bottom-left-radius: <x-axis-coord> <y-axis-coord>; /* Usage */ border-bottom-left-radius: 50% 50%; /* Or */ border-bottom-left-radius: 50%;In our case, we’re using border-bottom-left-radius: 100% to slide those coordinates to the opposite end of their respective axes. However, we’ll be overwriting the implied corner-shape: round in our @keyframe animation, so we refer to that with animation: bend-it-like-beckham. There’s no need to specify a duration because it’s a scroll-driven animation, as defined by animation-timeline: scroll().
In the @keyframe animation, we’re animating from corner-shape: superellipse(notch), which is like an inset square. This is equivalent to corner-shape: superellipse(-infinity), so it’s not actually squared but it’s so aggressively sharp that it looks squared. This animates to corner-shape: superellipse(square) (an outset square), or corner-shape: superellipse(infinity).
Animating corner-shape… revisitedThe demo above is actually a bit different to the one that I originally shared in the intro. It has one minor flaw, and I’ll show you how to fix it, but more importantly, you’ll learn more about an intricate detail of corner-shape.
The flaw: at the beginning and end of the animation, the curvature looks quite harsh because we’re animating from notch and square, right? It also looks like the shape is being sucked into the corners. Finally, the shape being stuck to the sides of the viewport makes the whole thing feel too contained.
The solution is simple:
/* Change this... */ inset: 0; /* ...to this */ inset: -1rem;This stretches the shape beyond the viewport, and even though this makes the animation appear to start late and finish early, we can fix that by not animating from/to -infinity/infinity:
@keyframes bend-it-like-beckham { from { corner-shape: superellipse(-6); } to { corner-shape: superellipse(6); } }Sure, this means that part of the shape is always visible, but we can fiddle with the superellipse() value to ensure that it stays outside of the viewport. Here’s a side-by-side comparison:
And the original demo (which is where we’re at now):
CodePen Embed Fallback Adding more scroll featuresScroll-driven animations work very well with other scroll features, including scroll snapping, scroll buttons, scroll markers, simple text fragments, and simple JavaScript methods such as scrollTo()/scroll(), scrollBy(), and scrollIntoView().
For example, we only have to add the following CSS snippet to introduce scroll snapping that works right alongside the scroll-driven corner-shape animation that we’ve already set up:
:root { /* Snap vertically */ scroll-snap-type: y; section { /* Snap to section start */ scroll-snap-align: start; } } CodePen Embed Fallback “Masking” with corner-shapeIn the example below, I’ve essentially created a border around the viewport and then a notched shape (corner-shape: notch) on top of it that’s the same color as the background (background: inherit). This shape completely covers the border at first, but then animates to reveal it (or in this case, the four corners of it):
CodePen Embed FallbackIf I make the shape a bit more visible, it’s easier to see what’s happening here, which is that I’m rotating this shape as well (rotate: 5deg), making the shape even more interesting.
This time around we’re animating border-radius, not corner-shape. When we animate to border-radius: 20vw / 20vh, 20vw and 20vh refers to the x-axis and y-axis of each corner, respectively, meaning that 20% of the border is revealed as we scroll.
The only other thing worth mentioning here is that we need to mess around with z-index to ensure that the content is higher up in the stacking context than the border and shape. Other than that, this example simply demonstrates another fun way to use corner-shape:
@keyframes tech-corners { from { border-radius: 0; } to { border-radius: 20vw / 20vh; } } /* Border */ body::before { /* Fill (- 1rem) */ content: ""; position: fixed; inset: 1rem; border: 1rem solid black; } /* Notch */ body::after { /* Fill (+ 3rem) */ content: ""; position: fixed; inset: -3rem; /* Rotated shape */ background: inherit; rotate: 5deg; corner-shape: notch; /* Animation settings */ animation: tech-corners; animation-timeline: scroll(); } main { /* Stacking fix */ position: relative; z-index: 1; } Animating multiple corner-shape elementsIn this example, we have multiple nested diamond shapes thanks to corner-shape: bevel, all leveraging the same scroll-driven animation where the diamonds increase in size, using padding:
CodePen Embed Fallback <div id="diamonds"> <div> <div> <div> <div> <div> <div> <div> <div> <div> <div></div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> <main> <!-- Content --> </main> @keyframes diamonds-are-forever { from { padding: 7rem; } to { padding: 14rem; } } #diamonds { /* Center them */ position: fixed; inset: 50% auto auto 50%; translate: -50% -50%; /* #diamonds, the <div>s within */ &, div { corner-shape: bevel; border-radius: 100%; animation: diamonds-are-forever; animation-timeline: scroll(); border: 0.0625rem solid #00000030; } } main { /* Stacking fix */ position: relative; z-index: 1; } That’s a wrapWe just explored animating from one custom superellipse() value to another, using corner-shape as a mask to create new shapes (again, while animating it), and animating multiple corner-shape elements at once. There are so many ways to animate corner-shape other than from one keyword to another, and if we make them scroll-driven animations, we can create some really interesting effects (although, they’d also look awesome if they were static).
Experimenting With Scroll-Driven corner-shape Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
JavaScript for Everyone: Destructuring
Editor’s note: Mat Marquis and Andy Bell have released JavaScript for Everyone, an online course offered exclusively at Piccalilli. This post is an excerpt from the course taken specifically from a chapter all about JavaScript destructuring. We’re publishing it here because we believe in this material and want to encourage folks like yourself to sign up for the course. So, please enjoy this break from our regular broadcasting to get a small taste of what you can expect from enrolling in the full JavaScript for Everyone course.
I’ve been writing about JavaScript for long enough that I wouldn’t rule out a hubris-related curse of some kind. I wrote JavaScript for Web Designers more than a decade ago now, back in the era when packs of feral var still roamed the Earth. The fundamentals are sound, but the advice is a little dated now, for sure. Still, despite being a web development antique, one part of the book has aged particularly well, to my constant frustration.
An entire programming language seemed like too much to ever fully understand, and I was certain that I wasn’t tuned for it. I was a developer, sure, but I wasn’t a developer-developer. I didn’t have the requisite robot brain; I just put borders on things for a living.
JavaScript for Web DesignersI still hear this sentiment from incredibly talented designers and highly technical CSS experts that somehow can’t fathom calling themselves “JavaScript developers,” as though they were tragically born without whatever gland produces the chemicals that make a person innately understand the concept of variable hoisting and could never possibly qualify — this despite the fact that many of them write JavaScript as part of their day-to-day work. While I may not stand by the use of alert() in some of my examples (again, long time ago), the spirit of JavaScript for Web Designers holds every bit as true today as it did back then: type a semicolon and you’re writing JavaScript. Write JavaScript and you’re a JavaScript developer, full stop.
Now, sooner or later, you do run into the catch: nobody is born thinking like JavaScript, but to get really good at JavaScript, you will need to learn how. In order to know why JavaScript works the way it does, why sometimes things that feel like they should work don’t, and why things that feel like they shouldn’t work sometimes do, you need to go one step beyond the code you’re writing or even the result of running it — you need to get inside JavaScript’s head. You need to learn to interact with the language on its own terms.
That deep-magic knowledge is the goal of JavaScript for Everyone, a course designed to help you get from junior- to senior developer. In JavaScript for Everyone, my aim is to help you make sense of the more arcane rules of JavaScript as-it-is-played — not just teach you the how but the why, using the syntaxes you’re most likely to encounter in your day-to-day work. If you’re brand new to the language, you’ll walk away from this course with a foundational understanding of JavaScript worth hundreds of hours of trial-and-error; if you’re a junior developer, you’ll finish this course with a depth of knowledge to rival any senior.
Thanks to our friends here at CSS-Tricks, I’m able to share the entire lesson on destructuring assignment. These are some of my favorite JavaScript syntaxes, which I’m sure we can all agree are normal and in fact very cool things to have —syntaxes are as powerful as they are terse, all of them doing a lot of work with only a few characters. The downside of that terseness is that it makes these syntaxes a little more opaque than most, especially when you’re armed only with a browser tab open to MDN and a gleam in your eye. We got this, though — by the time you’ve reached the end of this lesson, you’ll be unpacking complex nested data structures with the best of them.
And if you missed it before, there’s another excerpt from the JavaScript for Everyone course covering JavaScript Expressions available here on CSS-Tricks.
Destructuring AssignmentWhen you’re working with a data structure like an array or object literal, you’ll frequently find yourself in a situation where you want to grab some or all of the values that structure contains and use them to initialize discrete variables. That makes those values easier to work with, but historically speaking, it can lead to pretty wordy code:
const theArray = [ false, true, false ]; const firstElement = theArray[0]; const secondElement = theArray[1]; const thirdElement = theArray[2];This is fine! I mean, it works; it has for thirty years now. But as of 2015’s ES6, we’ve had a much more elegant option: destructuring.
Destructuring allows you to extract individual values from an array or object and assign them to a set of identifiers without needing to access the keys and/or values one at a time. In its most simple form — called binding pattern destructuring — each value is unpacked from the array or object literal and assigned to a corresponding identifier, all of which are declared with a single let or const (or var, technically, yes, fine). Brace yourself, because this is a strange one:
const theArray = [ false, true, false ]; const [ firstElement, secondElement, thirdElement ] = theArray; console.log( firstElement ); // Result: false console.log( secondElement ); // Result: true console.log( thirdElement ); // Result: falseThat’s the good stuff, even if it is a little weird to see brackets on that side of an assignment operator. That one binding covers all the same territory as the much more verbose snippet above it.
When working with an array, the individual identifiers are wrapped in a pair of array-style brackets, and each comma separated identifier you specify within those brackets will be initialized with the value in the corresponding element in the source Array. You’ll sometimes see destructuring referred to as unpacking a data structure, but despite how that and “destructuring” both sound, the original array or object isn’t modified by the process.
Elements can be skipped over by omitting an identifier between commas, the way you’d leave out a value when creating a sparse array:
const theArray = [ true, false, true ]; const [ firstElement, , thirdElement ] = theArray; console.log( firstElement ); // Result: true console.log( thirdElement ); // Result: trueThere are a couple of differences in how you destructure an object using binding pattern destructuring. The identifiers are wrapped in a pair of curly braces rather than brackets; sensible enough, considering we’re dealing with objects. In the simplest version of this syntax, the identifiers you use have to correspond to the property keys:
const theObject = { "theProperty" : true, "theOtherProperty" : false }; const { theProperty, theOtherProperty } = theObject; console.log( theProperty ); // result: true console.log( theOtherProperty ); // result: falseAn array is an indexed collection, and indexed collections are intended to be used in ways where the specific iteration order matters — for example, with destructuring here, where we can assume that the identifiers we specify will correspond to the elements in the array, in sequential order.
That’s not the case with an object, which is a keyed collection — in strict technical terms, just a big ol’ pile of properties that are intended to be defined and accessed in whatever order, based on their keys. No big deal in practice, though; odds are, you’d want to use the property keys’ identifier names (or something very similar) as your identifiers anyway. Simple and effective, but the drawback is that it assumes a given… well, structure to the object being destructured.
This brings us to the alternate syntax, which looks absolutely wild, at least to me. The syntax is object literal shaped, but very, very different — so before you look at this, briefly forget everything you know about object literals:
const theObject = { "theProperty" : true, "theOtherProperty" : false }; const { theProperty : theIdentifier, theOtherProperty : theOtherIdentifier } = theObject; console.log( theIdentifier ); // result: true console.log( theOtherIdentifier ); // result: falseYou’re still not thinking about object literal notation, right? Because if you were, wow would that syntax look strange. I mean, a reference to the property to be destructured where a key would be and identifiers where the values would be?
Fortunately, we’re not thinking about object literal notation even a little bit right now, so I don’t have to write that previous paragraph in the first place. Instead, we can frame it like this: within the parentheses-wrapped curly braces, zero or more comma-separated instances of the property key with the value we want, followed by a colon, followed by the identifier we want that property’s value assigned to. After the curly braces, an assignment operator (=) and the object to be destructured. That’s all a lot in print, I know, but you’ll get a feel for it after using it a few times.
The second approach to destructuring is assignment pattern destructuring. With assignment patterns, the value of each destructured property is assigned to a specific target — like a variable we declared with let (or, technically, var), a property of another object, or an element in an array.
When working with arrays and variables declared with let, assignment pattern destructuring really just adds a step where you declare the variables that will end up containing the destructured values:
const theArray = [ true, false ]; let theFirstIdentifier; let theSecondIdentifier [ theFirstIdentifier, theSecondIdentifier ] = theArray; console.log( theFirstIdentifier ); // true console.log( theSecondIdentifier ); // falseThis gives you the same end result as you’d get using binding pattern destructuring, like so:
const theArray = [ true, false ]; let [ theFirstIdentifier, theSecondIdentifier ] = theArray; console.log( theFirstIdentifier ); // true console.log( theSecondIdentifier ); // falseBinding pattern destructuring will allow you to use const from the jump, though:
const theArray = [ true, false ]; const [ theFirstIdentifier, theSecondIdentifier ] = theArray; console.log( theFirstIdentifier ); // true console.log( theSecondIdentifier ); // falseNow, if you wanted to use those destructured values to populate another array or the properties of an object, you would hit a predictable double-declaration wall when using binding pattern destructuring:
// Error const theArray = [ true, false ]; let theResultArray = []; let [ theResultArray[1], theResultArray[0] ] = theArray; // Uncaught SyntaxError: redeclaration of let theResultArrayWe can’t make let/const/var do anything but create variables; that’s their entire deal. In the example above, the first part of the line is interpreted as let theResultArray, and we get an error: theResultArray was already declared.
No such issue when we’re using assignment pattern destructuring:
const theArray = [ true, false ]; let theResultArray = []; [ theResultArray[1], theResultArray[0] ] = theArray; console.log( theResultArray ); // result: Array [ false, true ]Once again, this syntax applies to objects as well, with a few little catches:
const theObject = { "theProperty" : true, "theOtherProperty" : false }; let theProperty; let theOtherProperty; ({ theProperty, theOtherProperty } = theObject ); console.log( theProperty ); // true console.log( theOtherProperty ); // falseYou’ll notice a pair of disambiguating parentheses around the line where we’re doing the destructuring. You’ve seen this before: without the grouping operator, a pair of curly braces in a context where a statement is expected is assumed to be a block statement, and you get a syntax error:
// Error const theObject = { "theProperty" : true, "theOtherProperty" : false }; let theProperty; let theOtherProperty; { theProperty, theOtherProperty } = theObject; // Uncaught SyntaxError: expected expression, got '='So far this isn’t doing anything that binding pattern destructuring couldn’t. We’re using identifiers that match the property keys, but any identifier will do, if we use the alternate object destructuring syntax:
const theObject = { "theProperty" : true, "theOtherProperty" : false }; let theFirstIdentifier; let theSecondIdentifier; ({ theProperty: theFirstIdentifier, theOtherProperty: theSecondIdentifier } = theObject ); console.log( theFirstIdentifier ); // true console.log( theSecondIdentifier ); // falseOnce again, nothing binding pattern destructuring couldn’t do. But unlike binding pattern destructuring, any kind of assignment target will work with assignment pattern destructuring:
const theObject = { "theProperty" : true, "theOtherProperty" : false }; let resultObject = {}; ({ theProperty : resultObject.resultProp, theOtherProperty : resultObject.otherResultProp } = theObject ); console.log( resultObject ); // result: Object { resultProp: true, otherResultProp: false }With either syntax, you can set “default” values that will be used if an element or property isn’t present at all, or it contains an explicit undefined value:
const theArray = [ true, undefined ]; const [ firstElement, secondElement = "A string.", thirdElement = 100 ] = theArray; console.log( firstElement ); // Result: true console.log( secondElement ); // Result: A string. console.log( thirdElement ); // Result: 100 const theObject = { "theProperty" : true, "theOtherProperty" : undefined }; const { theProperty, theOtherProperty = "A string.", aThirdProperty = 100 } = theObject; console.log( theProperty ); // Result: true console.log( theOtherProperty ); // Result: A string. console.log( aThirdProperty ); // Result: 100Snazzy stuff for sure, but where this syntax really shines is when you’re unpacking nested arrays and objects. Naturally, there’s nothing stopping you from unpacking an object that contains an object as a property value, then unpacking that inner object separately:
const theObject = { "theProperty" : true, "theNestedObject" : { "anotherProperty" : true, "stillOneMoreProp" : "A string." } }; const { theProperty, theNestedObject } = theObject; const { anotherProperty, stillOneMoreProp = "Default string." } = theNestedObject; console.log( stillOneMoreProp ); // Result: A string.But we can make this way more concise. We don’t have to unpack the nested object separately — we can unpack it as part of the same binding:
const theObject = { "theProperty" : true, "theNestedObject" : { "anotherProperty" : true, "stillOneMoreProp" : "A string." } }; const { theProperty, theNestedObject : { anotherProperty, stillOneMoreProp } } = theObject; console.log( stillOneMoreProp ); // Result: A string.From an object within an object to three easy-to-use constants in a single line of code.
We can unpack mixed data structures just as succinctly:
const theObject = [{ "aProperty" : true, },{ "anotherProperty" : "A string." }]; const [{ aProperty }, { anotherProperty }] = theObject; console.log( anotherProperty ); // Result: A string.A dense syntax, there’s no question of that — bordering on “opaque,” even. It might take a little experimentation to get the hang of this one, but once it clicks, destructuring assignment gives you an incredibly quick and convenient way to break down complex data structures without spinning up a bunch of intermediate data structures and values.
Rest PropertiesIn all the examples above we’ve been working with known quantities: “turn these X properties or elements into Y variables.” That doesn’t match the reality of breaking down a huge, tangled object, jam-packed array, or both.
In the context of a destructuring assignment, an ellipsis (that’s ..., not …, for my fellow Unicode enthusiasts) followed by an identifier (to the tune of ...theIdentifier) represents a rest property — an identifier that will represent the rest of the array or object being unpacked. This rest property will contain all the remaining elements or properties beyond the ones we’ve explicitly unpacked to their own identifiers, all bundled up in the same kind of data structure as the one we’re unpacking:
const theArray = [ false, true, false, true, true, false ]; const [ firstElement, secondElement, ...remainingElements ] = theArray; console.log( remainingElements ); // Result: Array(4) [ false, true, true, false ]Generally I try to avoid using examples that veer too close to real-world use on purpose where they can get a little convoluted and I don’t want to distract from the core ideas — but in this case, “convoluted” is exactly what we’re looking to work around. So let’s use an object near and dear to my heart: (part of) the data representing the very first newsletter I sent out back when I started writing this course.
const firstPost = { "id": "mat-update-1.md", "slug": "mat-update-1", "body": "Hey, great to meet you, everybody. I'm Mat — \\"Wilto\\" is good too — and I'm here to teach you JavaScript. Not just what JavaScript is or what JavaScript does, but the *how* and the *why* of JavaScript. The weird stuff. The *deep magic_.\\n\\nWell, okay, I'm not *currently* here to teach you JavaScript, but I will be soon. Right now I'm just getting things in order for the course — planning, outlining, polishing the fancy semicolons that I only take out when I'm having company over, writing like 5,000 words about `this` as a warm-up that completely got away from me, that kind of thing.", "collection": "emails", "data": { "title": "Meet your Instructor", "pubDate": "2025-05-08T09:55:00.630Z", "headingSize": "large", "showUnsubscribeLink": true, "stream": "javascript-for-everyone" } };Quite a bit going on in there. For purposes of this exercise, assume this is coming in from an external API the way it is over on my website — this isn’t an object we control. Sure, we can work with that object directly, but that’s a little unwieldy when all we need is, for example, the newsletter title and body:
const firstPost = { "id": "mat-update-1.md", "slug": "mat-update-1", "body": "Hey, great to meet you, everybody. I'm Mat — \\"Wilto\\" is good too — and I'm here to teach you JavaScript. Not just what JavaScript is or what JavaScript does, but the *how* and the *why* of JavaScript. The weird stuff. The *deep magic_.\\n\\nWell, okay, I'm not *currently* here to teach you JavaScript, but I will be soon. Right now I'm just getting things in order for the course — planning, outlining, polishing the fancy semicolons that I only take out when I'm having company over, writing like 5,000 words about `this` as a warm-up that completely got away from me, that kind of thing.", "data": { "title": "Meet your Instructor", "pubDate": "2025-05-08T09:55:00.630Z", "headingSize": "large", "showUnsubscribeLink": true, "stream": "javascript-for-everyone" } }; const { data : { title }, body } = firstPost; console.log( title ); // Result: Meet your Instructor console.log( body ); /* Result: Hey, great to meet you, everybody. I'm Mat — \\"Wilto\\" is good too — and I'm here to teach you JavaScript. Not just what JavaScript is or what JavaScript does, but the *how* and the *why* of JavaScript. The weird stuff. The *deep magic_. Well, okay, I'm not *currently* here to teach you JavaScript, but I will be soon. Right now I'm just getting things in order for the course — planning, outlining, polishing the fancy semicolons that I only take out when I'm having company over, writing like 5,000 words about `this` as a warm-up that completely got away from me, that kind of thing. */That’s tidy; a couple dozen characters and we have exactly what we need from that tangle. I know I’m not going to need those id or slug properties to publish it on my own website, so I omit those altogether — but that inner data object has a conspicuous ring to it, like maybe one could expect it to contain other properties associated with future posts. I don’t know what those properties will be, but I know I’ll want them all packaged up in a way where I can easily make use of them. I want the firstPost.data.title property in isolation, but I also want an object containing all the rest of the firstPost.data properties, whatever they end up being:
const firstPost = { "id": "mat-update-1.md", "slug": "mat-update-1", "body": "Hey, great to meet you, everybody. I'm Mat — \\"Wilto\\" is good too — and I'm here to teach you JavaScript. Not just what JavaScript is or what JavaScript does, but the *how* and the *why* of JavaScript. The weird stuff. The *deep magic_.\\n\\nWell, okay, I'm not *currently* here to teach you JavaScript, but I will be soon. Right now I'm just getting things in order for the course — planning, outlining, polishing the fancy semicolons that I only take out when I'm having company over, writing like 5,000 words about `this` as a warm-up that completely got away from me, that kind of thing.", "data": { "title": "Meet your Instructor", "pubDate": "2025-05-08T09:55:00.630Z", "headingSize": "large", "showUnsubscribeLink": true, "stream": "javascript-for-everyone" } }; const { data : { title, ...metaData }, body } = firstPost; console.log( title ); // Result: Meet your Instructor console.log( metaData ); // Result: Object { pubDate: "2025-05-08T09:55:00.630Z", headingSize: "large", showUnsubscribeLink: true, stream: "javascript-for-everyone" }Now we’re talking. Now we have a metaData object containing anything and everything else in the data property of the object we’ve been handed.
Listen. If you’re anything like me, even if you haven’t quite gotten your head around the syntax itself, you’ll find that there’s something viscerally satisfying about the binding in the snippet above. All that work done in a single line of code. It’s terse, it’s elegant — it takes the complex and makes it simple. That’s the good stuff.
And yet: maybe you can hear it too, ever-so-faintly? A quiet voice, way down in the back of your mind, that asks “I wonder if there’s an even better way.” For what we’re doing here, in isolation, this solution is about as good as it gets — but as far as the wide world of JavaScript goes: there’s always a better way. If you can’t hear it just yet, I bet you will by the end of the course.
Anyone who writes JavaScript is a JavaScript developer; there are no two ways about that. But the satisfaction of creating order from chaos in just a few keystrokes, and the drive to find even better ways to do it? Those are the makings of a JavaScript developer to be reckoned with.
You can do more than just “get by” with JavaScript; I know you can. You can understand JavaScript, all the way down to the mechanisms that power the language — the gears and springs that move the entire “interactive” layer of the web. To really understand JavaScript is to understand the boundaries of how users interact with the things we’re building, and broadening our understanding of the medium we work with every day sharpens all of our skills, from layout to accessibility to front-end performance to typography. Understanding JavaScript means less “I wonder if it’s possible to…” and “I guess we have to…” in your day-to-day decision making, even if you’re not the one tasked with writing it. Expanding our skillsets will always make us better — and more valued, professionally — no matter our roles.
JavaScript is a tricky thing to learn; I know that all too well — that’s why I wrote JavaScript for Everyone. You can do this, and I’m here to help.
I hope to see you there.
Check out the courseJavaScript for Everyone: Destructuring originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More
For this issue of What’s !important, we have a healthy balance of old CSS that you might’ve missed and new CSS that you don’t want to miss. This includes random(), random-item(), folded corners using clip-path, backdrop-filter, font-variant-numeric: tabular-nums, the Popover API, anchored container queries, anchor positioning in general, DOOM in CSS, customizable <select>, :open, scroll-triggered animations, <toolbar>, and somehow, more.
Let’s dig in.
Understanding random() and random-item()Alvaro Montoro explains how the random() and random-item() CSS functions work. As it turns out, they’re actually quite complex:
width: random(--w element-shared, 1rem, 2rem); color: random-item(--c, red, orange, yellow, darkkhaki); Creating folded corners using clip-pathMy first solution to folded corners involved actual images. Not a great solution, but that was the way to do it in the noughties. Since then we’ve been able to do it with box-shadow, but Kitty Giraudel has come up with a CSS clip-path solution that clips a custom shape (hover the kitty to see it in action):
CodePen Embed Fallback Revisiting backdrop-filter and font-variant-numeric: tabular-numsStuart Robson talks about backdrop-filter. It’s not a new CSS property, but it’s very useful and hardly ever talked about. In fact, up until now, I thought that it was for the ::backdrop pseudo-element, but we can actually use it to create all kinds of background effects for all kinds of elements, like this:
CodePen Embed Fallbackfont-variant-numeric: tabular-nums is another one. This property and value prevents layout shift when numbers change dynamically, as they do with live clocks, counters, timers, financial tables, and so on. Amit Merchant walks you through it with this demo:
CodePen Embed Fallback Getting started with the Popover APIGodstime Aburu does a deep dive on the Popover API, a new(ish) but everyday web platform feature that simplifies tooltip and tooltip-like UI patterns, but isn’t without its nuances.
Unraveling yet another anchor positioning quirkJust another anchor positioning quirk, this time from Chris Coyier. These quirks have been piling up for a while now. We’ve talked about them time and time again, but the thing is, they’re not bugs. Anchor positioning works in a way that isn’t commonly understood, so Chris’ article is definitely worth a read, as are the articles that he references.
Building dynamic toggletips using anchored container queriesIn this walkthrough, I demonstrate how to build dynamic toggletips using anchored container queries. Also, I ran into an anchor positioning quirk, so if you’re looking to solidify your understanding of all that, I think the walkthrough will help with that too.
Demo (full effect requires Chrome 143+):
CodePen Embed Fallback DOOM in CSSDOOM in CSS. DOOM. In CSS.
DOOM fully rendered in CSS. Every surface is a <div> that has a background image, with a clipping path with 3D transforms applied. Of course CSS does not have a movable camera, so we rotate and translate the scene around the user.
[image or embed]
- Safari Technology Preview 238
- Customizable <select>
- :open (to my surprise, as I thought it was Baseline already)
- Chrome 146
In addition, Chrome will ship every two weeks starting September.
From the Quick Hits reel, you might’ve missed that Font Awesome launched a Kickstarter campaign to transform Eleventy into Build Awesome, cancelled it because their emails failed to send (despite meeting their goal!), and vowed to try again. You can subscribe to the relaunch notification.
Also, <toolbar> is coming along according to Luke Warlow. This is akin to <focusgroup>, which we can actually test in Chrome 146 with the “Experimental Web Platform features” flag enabled.
Right, I’m off to slay some demons in DOOM. Until next time!
P.S. Congratulations to Kevin Powell for making it to 1 million YouTube subs!
What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
4 Reasons That Make Tailwind Great for Building Layouts
When I talk about layouts, I’m referring to how you place items on a page. The CSS properties that are widely used here include:
- display — often grid or flex nowadays
- margin
- padding
- width
- height
- position
- top, left, bottom, right
I often include border-width as a minor item in this list as well.
At this point, there’s only one thing I’d like to say.
Tailwind is really great for making layouts.
There are many reasons why.
First: Layout styles are highly dependent on the HTML structureWhen we shift layouts into CSS, we lose the mental structure and it takes effort to re-establish them. Imagine the following three-column grid in HTML and CSS:
<div class="grid"> <div class="grid-item"></div> <div class="grid-item"></div> </div> .grid { display: grid; grid-template-columns: 2fr 1fr; .grid-item:first-child { grid-column: span 2 } .grid-item:last-child { grid-column: span 1 } }Now cover the HTML structure and just read the CSS. As you do that, notice you need to exert effort to imagine the HTML structure that this applies to.
Now imagine the same, but built with Tailwind utilities:
<div class="grid grid-cols-3"> <div class="col-span-2"></div> <div class="col-span-1"></div> </div>You might almost begin to see the layout manifest in your eyes without seeing the actual output. It’s pretty clear: A three-column grid, first item spans two columns while the second one spans one column.
But grid-cols-3 and col-span-2 are kinda weird and foreign-looking because we’re trying to parse Tailwind’s method of writing CSS.
Now, watch what happens when we shift the syntax out of the way and use CSS variables to define the layout instead. The layout becomes crystal clear immediately:
<div class="grid-simple [--cols:3]"> <div class="[--span:2]"> ... </div> <div class="[--span:1]"> ... </div> </div>Same three-column layout.
But it makes the layout much easier to write, read, and visualize. It also has other benefits, but I’ll let you explore its documentation instead of explaining it here.
For now, let’s move on.
Why not use 2fr 1fr?It makes sense to write 2fr 1fr for a three-column grid, doesn’t it?
.grid { display: grid; grid-template-columns: 2fr 1fr; }Unfortunately, it won’t work. This is because fr is calculated based on the available space after subtracting away the grid’s gutters (or gap).
Since 2fr 1fr only contains two columns, the output from 2fr 1fr will be different from a standard three-column grid.
Alright. Let’s continue with the reasons that make Tailwind great for building layouts.
Second: No need to name layoutsI think layouts are the hardest things to name. I rarely come up with better names than:
- Number + Columns, e.g. .two-columns
- Semantic names, e.g. .content-sidebar
But these names don’t do the layout justice. You can’t really tell what’s going on, even if you see .two-columns, because .two-columns can mean a variety of things:
- Two equal columns
- Two columns with 1fr auto
- Two columns with auto 1fr
- Two columns that spans total of 7 “columns” and the first object takes up 4 columns while the second takes up 3…
You can already see me tripping up when I try to explain that last one there…
Instead of forcing ourselves to name the layout, we can let the numbers do the talking — then the whole structure becomes very clear.
<div class="grid-simple [--cols:7]"> <div class="[--span:4]"> ... </div> <div class="[--span:3]"> ... </div> </div>The variables paint a picture.
Third: Layout requirements can change depending on contextA “two-column” layout might have different properties when used in different contexts. Here’s an example.
In this example, you can see that:
- A larger gap is used between the I and J groups.
- A smaller gap is used within the I and J groups.
The difference in gap sizes is subtle, but used to show that the items are of separate groups.
Here’s an example where this concept is used in a real project. You can see the difference between the gap used within the newsletter container and the gap used between the newsletter and quote containers.
If this sort of layout is only used in one place, we don’t have to create a modifier class just to change the gap value. We can change it directly.
<div class="grid-simple [--cols:2] gap-8"> <div class="grid-simple gap-4 [--cols:2]"> ... </div> <div class="grid-simple gap-4 [--cols:2]"> ... </div> </div> Another common exampleLet’s say you have a heading for a marketing section. The heading would look nicer if you are able to vary its max-width so the text isn’t orphaned.
text-balance might work here, but this is often nicer with manual positioning.
Without Tailwind, you might write an inline style for it.
<h2 class="h2" style="max-width: 12em;"> Your subscription has been confirmed </h2>With Tailwind, you can specify the max-width in a more terse way:
<h2 class="h2 max-w-[12em]"> Your subscription has been confirmed </h2> Fourth: Responsive variants can be created on the fly“At which breakpoint would you change your layouts?” is another factor you’d want to consider when designing your layouts. I shall term this the responsive factor for this section.
Most likely, similar layouts should have the same responsive factor. In that case, it makes sense to group the layouts together into a named layout.
.two-column { @apply grid-simple; /* --cols: 1 is the default */ @media (width >= 800px) { --cols:2; } }However, you may have layouts where you want two-column grids on a mobile and a much larger column count on tablets and desktops. This layout style is commonly used in a site footer component.
Since the footer grid is unique, we can add Tailwind’s responsive variants and change the layout on the fly.
<div class="grid-simple [--cols:2] md:[--cols:5]"> <!-- span set to 1 by default so there's no need to specify them --> <div> ... </div> <div> ... </div> <div> ... </div> <div> ... </div> <div> ... </div> <div> ... </div> </div>Again, we get to create a new layout on the fly without creating an additional modifier class — this keeps our CSS clean and focused.
How to best use TailwindThis article is a sample lesson from my course, Unorthodox Tailwind, where I show you how to use Tailwind and CSS synergistically.
Personally, I think the best way to use Tailwind is not to litter your HTML with Tailwind utilities, but to create utilities that let you create layouts and styles easily.
I cover much more of that in the course if you’re interested to find out more!
4 Reasons That Make Tailwind Great for Building Layouts originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
The Value of z-index
The z-index property is one of the most important tools any UI developer has at their disposal, as it allows you to control the stacking order of elements on a webpage. Modals, toasts, popups, dropdowns, tooltips, and many other common elements rely on it to ensure they appear above other content.
While most resources focus on the technical details or the common pitfalls of the Stacking Context (we’ll get to that in a moment…), I think they miss one of the most important and potentially chaotic aspects of z-index: the value.
In most projects, once you hit a certain size, the z-index values become a mess of “magic numbers”, a chaotic battlefield of values, where every team tries to outdo the others with higher and higher numbers.
How This Idea StartedI saw this line on a pull request a few years ago:
z-index: 10001;I thought to myself, “Wow, that’s a big number! I wonder why they chose that specific value?” When I asked the author, they said: “Well, I just wanted to make sure it was above all the other elements on the page, so I chose a high number.”
This got me thinking about how we look at the stacking order of our projects, how we choose z-index values, and more importantly, the implications of those choices.
The Fear of Being HiddenThe core issue isn’t a technical one, but a lack of visibility. In a large project with multiple teams, you don’t always know what else is floating on the screen. There might be a toast notification from Team A, a cookie banner from Team B, or a modal from the marketing SDK.
The developer’s logic was simple in this case: “If I use a really high number, surely it will be on top.”
This is how we end up with magic numbers, these arbitrary values that aren’t connected to the rest of the application. They are guesses made in isolation, hoping to win the “arms race” of z-index values.
We’re Not Talking About Stacking Context… But…As I mentioned at the beginning, there are many resources that cover z-index in the context of the Stacking Context. In this article, we won’t cover that topic. However, it’s impossible to talk about z-index values without at least mentioning it, as it’s a crucial concept to understand.
Essentially, elements with a higher z-index value will be displayed in front of those with a lower value as long as they are in the same Stacking Context.
If they aren’t, then even if you set a massive z-index value on an element in a “lower” stack, elements in a “higher” stack will stay on top of it, even if they have a very low z-index value. This means that sometimes, even if you give an element the maximum possible value, it can still end up being hidden behind something else.
CodePen Embed Fallback CodePen Embed FallbackNow let’s get back to the values.
💡 Did you know? The maximum value for z-index is 2147483647. Why this specific number? It’s the maximum value for a 32-bit signed integer. If you try to go any higher, most browsers will simply clamp it to this limit.
The Problem With “Magic Numbers”Using arbitrary high values for z-index can lead to several issues:
- Lack of maintainability: When you see a z-index value like 10001, it doesn’t tell you anything about its relationship to other elements. It’s just a number that was chosen without any context.
- Potential for conflicts: If multiple teams or developers are using high z-index values, they might end up conflicting with each other, leading to unexpected behavior where some elements are hidden behind others.
- Difficult to debug: When something goes wrong with the stacking order, it can be challenging to figure out why, especially if there are many elements with high z-index values.A Better Approach
I’ve encountered this “arms race” in almost every large project I’ve been a part of. The moment you have multiple teams working in the same codebase without a standardized system, chaos eventually takes over.
The solution is actually quite simple: tokenization of z-index values.
Now, wait, stay with me! I know that the moment someone mentions “tokens”, some developers might roll their eyes or shake their heads, but this approach actually works. Most of the major (and better-designed) design systems include z-index tokens for a reason. Teams that adopt them swear by them and never look back.
By using tokens, you gain:
- Simple and easy maintenance: You manage values in one place.
- Conflict prevention: No more guessing if 100 is higher than whatever Team B is using.
- Easier debugging:: You can see exactly which “layer” an element belongs to.
- Better Stacking Context management: It forces you to think about layers systematically rather than as random numbers.
Let’s look at how this works in practice. I’ve prepared a simple demo where we manage our layers through a central set of tokens in the :root:
:root { --z-base: 0; --z-toast: 100; --z-popup: 200; --z-overlay: 300; } CodePen Embed FallbackThis setup is incredibly convenient. If you need to add a new popup or a toast, you know exactly which z-index to use. If you want to change the order — for example, to place toasts above the overlay — you don’t need to hunt through dozens of files. You just change the values in the :root, and everything updates accordingly in one place.
Handling New ElementsThe real power of this system shines when your requirements change. Suppose you need to add a new sidebar and place it specifically between the base content and the toasts.
In a traditional setup, you’d be checking every existing element to see what numbers they use. With tokens, we simply insert a new token and adjust the scale:
:root { --z-base: 0; --z-sidebar: 100; --z-toast: 200; --z-popup: 300; --z-overlay: 400; } CodePen Embed FallbackYou don’t have to touch a single existing component with this setup. You update the tokens and you’re good to go. The logic of your application remains consistent, and you’re no longer guessing which number is “high enough”.
The Power of Relative LayeringWe sometimes want to “lock” specific layers relative to each other. A great example of this is a background element for a modal or an overlay. Instead of creating a separate token for the background, we can calculate its position relative to the main layer.
Using calc() allows us to maintain a strict relationship between elements that always belong together:
.overlay-background { z-index: calc(var(--z-overlay) - 1); }This ensures that the background will always stay exactly one step behind the overlay, no matter what value we assign to the --z-overlay token.
CodePen Embed Fallback Managing Internal LayersUp until now, we’ve focused on the main, global layers of the application. But what happens inside those layers?
The tokens we created for the main layers (like 100, 200, etc.) are not suitable for managing internal elements. This is because most of these main components create their own Stacking Context. Inside a popup that has z-index: 300, a value of 301 is functionally identical to 1. Using large global tokens for internal positioning is confusing and unnecessary.
Note: For these local tokens to work as expected, you must ensure the container creates a Stacking Context. If you’re working on a component that doesn’t already have one (e.g., it doesn’t has a z-index set), you can create one explicitly using isolation: isolate.
To solve this, we can introduce a pair of “local” tokens specifically for internal use:
:root { /* ... global tokens ... */ --z-bottom: -10; --z-top: 10; }This allows us to handle internal positioning with precision. If you need a floating action button inside a popup to stay on top, or a decorative icon on a toast to sit behind the main content, you can use these local anchors:
.popup-close-button { z-index: var(--z-top); } .toast-decorative-icon { z-index: var(--z-bottom); }For even more complex internal layouts, you can still use calc() with these local tokens. If you have multiple elements stacking within a component, calc(var(--z-top) + 1) (or - 1) gives you that extra bit of precision without ever needing to look at global values.
CodePen Embed FallbackThis keeps our logic consistent: we think about layers and positions systematically, rather than throwing random numbers at the problem and hoping for the best.
Versatile Components: The Tooltip CaseOne of the biggest headaches in CSS is managing components that can appear anywhere, like a tooltip.
Traditionally, developers give tooltips a massive z-index (like 9999) because they might appear over a modal. But if the tooltip is physically inside the modal’s DOM structure, its z-index is only relative to that modal anyway.
A tooltip simply needs to be above the content it’s attached to. By using our local tokens, we can stop the guessing game:
.tooltip { z-index: var(--z-top); }Whether the tooltip is on a button in the main content, an icon inside a toast, or a link within a popup, it will always appear correctly above its immediate surroundings. It doesn’t need to know about the global “arms race” because it’s already standing on the “stable floor” provided by its parent layer’s token.
CodePen Embed Fallback Negative Values Can Be GoodNegative values often scare developers. We worry that an element with z-index: -1 will disappear behind the page background or some distant parent.
However, within our systematic approach, negative values are a powerful tool for internal decorations. When a component creates its own Stacking Context, the z-index is confined to that component. And z-index: var(--z-bottom) simply means “place this behind the default content of this specific container”.
This is perfect for:
- Component backgrounds: Subtle patterns or gradients that shouldn’t interfere with text.
- Shadow simulations: When you need more control than box-shadow provides.
- Inner glows or borders: Elements that should sit “under” the main UI.
With just a few CSS variables, we’ve built a complete management system for z-index. It’s a simple yet powerful way to ensure that managing layers never feels like a guessing game again.
To maintain a clean and scalable codebase, here are the golden rules for working with z-index:
- No magic numbers: Never use arbitrary values like 999 or 10001. If a number isn’t tied to a system, it’s a bug waiting to happen.
- Tokens are mandatory: Every z-index in your CSS should come from a token, either a global layer token or a local positioning token.
- It’s rarely the value: If an element isn’t appearing on top despite a “high” value, the problem is almost certainly its Stacking Context, not the number itself.
- Think in layers: Stop asking “how high should this be?” and start asking “which layer does this belong to?”
- Calc for connection: Use calc() to bind related elements together (like an overlay and its background) rather than giving them separate, unrelated tokens.
- Local contexts for local problems: Use local tokens (--z-top, --z-bottom) and internal stacking contexts to manage complexity within components.
By following these rules, you turn z-index from a chaotic source of bugs into a predictable, manageable part of your design system. The value of z-index isn’t in how high the number is, but in the system that defines it.
Bonus: Enforcing a Clean SystemA system is only as good as its enforcement. In a deadline-driven environment, it’s easy for a developer to slip in a quick z-index: 999 to “make it just work”. Without automation, your beautiful token system will eventually erode back into chaos.
To prevent this, I developed a library specifically designed to enforce this exact system: z-index-token-enforcer.
npm install z-index-token-enforcer --save-devIt provides a unified set of tools to automatically flag any literal z-index values and require developers to use your predefined tokens:
- Stylelint plugin: For standard CSS/SCSS enforcement
- ESLint plugin: To catch literal values in CSS-in-JS and React inline styles
- CLI scanner: A standalone script that can quickly scan files directly or be integrated into your CI/CD pipelines
By using these tools, you turn the “Golden Rules” from a recommendation into a hard requirement, ensuring that your codebase stays clean, scalable, and, most importantly, predictable.
The Value of z-index originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Steven Heller’s Font of the Month: Curve Display
Read the book, Typographic Firsts
What typographers and designers want most in a font is a family that, under the right conditions, exudes a familiar yet distinctive voice. As I was scrolling around for this month’s selection, I found just that very face. It is a distinctive yet suggestive legacy with a “feeling” that is […]
The post Steven Heller’s Font of the Month: Curve Display appeared first on I Love Typography Ltd.
Popover API or Dialog API: Which to Choose?
Choosing between Popover API and Dialog API is difficult because they seem to do the same job, but they don’t!
After a bit lots of research, I discovered that the Popover API and Dialog API are wildly different in terms of accessibility. So, if you’re trying to decide whether to use Popover API or Dialog’s API, I recommend you:
- Use Popover API for most popovers.
- Use Dialog’s API only for modal dialogs.
The relationship between Popovers and Dialogs are confusing to most developers, but it’s actually quite simple.
Dialogs are simply subsets of popovers. And modal dialogs are subsets of dialogs. Read this article if you want to understand the rationale behind this relationship.
This is why you could use the Popover API even on a <dialog> element.
<!-- Using popover on a dialog element --> <dialog popover>...</div>Stylistically, the difference between popovers and modals are even clearer:
- Modals should show a backdrop.
- Popovers should not.
Therefore, you should never style a popover’s ::backdrop element. Doing so will simply indicate that the popover is a dialog — which creates a whole can of problems.
You should only style a modal’s ::backdrop element.
Popover API and its accessibilityBuilding a popover with the Popover API is relatively easy. You specify three things:
- a popovertarget attribute on the popover trigger,
- an id on the popover, and
- a popover attribute on the popover.
The popovertarget must match the id.
<button popovertarget="the-popover"> ... </button> <dialog popover id="the-popover"> The Popover Content </dialog>Notice that I’m using the <dialog> element to create a dialog role. This is optional, but recommended. I do this because dialog is a great default role since most popovers are simply just dialogs.
This two lines of code comes with a ton of accessibility features already built-in for you:
- Automatic focus management
- Focus goes to the popover when opening.
- Focus goes back to the trigger when closing.
- Automatic aria connection
- No need to write aria-expanded, aria-popup and aria-controls. Browsers handle those natively. Woo!
- Automatic light dismiss
- Popover closes when user clicks outside.
- Popover closes when they press the Esc key.
Now, without additional styling, the popover looks kinda meh. Styling is a whole ‘nother issue, so we’ll tackle that in a future article. Geoff has a few notes you can review in the meantime.
CodePen Embed Fallback Dialog API and its accessibilityUnlike the Popover API, the Dialog API doesn’t have many built-in features by default:
- No automatic focus management
- No automatic ARIA connection
- No automatic light dismiss
So, we have to build them ourselves with JavaScript. This is why the Popover API is superior to the Dialog API in almost every aspect — except for one: when modals are involved.
The Dialog API has a showModal method. When showModal is used, the Dialog API creates a modal. It:
- automatically inerts other elements,
- prevents users from tabbing into other elements, and
- prevents screen readers from reaching other elements.
It does this so effectively, we no longer need to trap focus within the modal.
But we gotta take care of the focus and ARIA stuff when we use the Dialog API, so let’s tackle the bare minimum code you need for a functioning dialog.
We’ll begin by building the HTML scaffold:
<button class="modal-invoker" data-target="the-modal" aria-haspopup="dialog" >...</button> <dialog id="the-modal">The Popover Content</dialog>Notice I did not add any aria-expanded in the HTML. I do this for a variety of reasons:
- This reduces the complexity of the HTML.
- We can write aria-expanded, aria-controls, and the focus stuff directly in JavaScript – since these won’t work without JavaScript.
- Doing so makes this HTML very reusable.
I’m going to write about a vanilla JavaScript implementation here. If you’re using a framework, like React or Svelte, you will have to make a couple of changes — but I hope that it’s gonna be straightforward for you.
First thing to do is to loop through all dialog-invokers and set aria-expanded to false. This creates the initial state.
We will also set aria-controls to the <dialog> element. We’ll do this even though aria-controls is poop, ’cause there’s no better way to connect these elements (and there’s no harm connecting them) as far as I know.
const modalInvokers = Array.from(document.querySelectorAll('.modal-invoker')) modalInvokers.forEach(invoker => { const dialogId = invoker.dataset.target const dialog = document.querySelector(`#${dialogId}`) invoker.setAttribute('aria-expanded', false) invoker.setAttribute('aria-controls', dialogId) }) Opening the modalWhen the invoker/trigger is clicked, we gotta:
- change the aria-expanded from false to true to show the modal to assistive tech users, and
- use the showModal function to open the modal.
We don’t have to write any code to hide the modal in this click handler because users will never get to click on the invoker when the dialog is opened.
modalInvokers.forEach(invoker => { // ... // Opens the modal invoker.addEventListener('click', event => { invoker.setAttribute('aria-expanded', true) dialog.showModal() }) }) CodePen Embed FallbackGreat. The modal is open. Now we gotta write code to close the modal.
Closing the modalBy default, showModal doesn’t have automatic light dismiss, so users can’t close the modal by clicking on the overlay, or by hitting the Esc key. This means we have to add another button that closes the modal. This must be placed within the modal content.
<dialog id="the-modal"> <button class="modal-closer">X</button> <!-- Other modal content --> </dialog>When users click the close button, we have to:
- set aria-expanded on the opening invoker to false,
- close the modal with the close method, and
- bring focus back to the opening invoker element.
Phew, with this, we’re done with the basic implementation.
CodePen Embed FallbackOf course, there’s advanced work like light dismiss and styling… which we can tackle in a future article.
Can you use the Popover API to create modals?Yeah, you can.
But you will have to handle these on your own:
- Inerting other elements
- Trapping focus
I think what we did earlier (setting aria-expanded, aria-controls, and focus) are easier compared to inerting elements and trapping focus.
The Dialog API might become much easier to use in the futureA proposal about invoker commands has been created so that the Dialog API can include popovertarget like the Popover API.
This is on the way, so we might be able to make modals even simpler with the Dialog API in the future. In the meantime, we gotta do the necessary work to patch accessibility stuff.
Deep dive into building workable popovers and modalsWe’ve only began to scratch the surface of building working popovers and modals with the code above — they’re barebone versions that are accessible, but they definitely don’t look nice and can’t be used for professional purposes yet.
To make the process of building popovers and modals easier, we will dive deeper into the implementation details for a professional-grade popover and a professional-grade modal in future articles.
In the meantime, I hope these give you some ideas on when to choose the Popover API and the Dialog API!
Remember, there’s no need to use both. One will do.
Popover API or Dialog API: Which to Choose? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More
Despite what’s been a sleepy couple of weeks for new Web Platform Features, we have an issue of What’s !important that’s prrrretty jam-packed. The web community had a lot to say, it seems, so fasten your seatbelts!
@keyframes animations can be stringsPeter Kröner shared an interesting fact about @keyframes animations — that they can be strings:
@keyframes "@animation" { /* ... */ } #animate-this { animation: "@animation"; }Yo dawg, time for a #CSS fun fact: keyframe names can be strings. Why? Well, in case you want your keyframes to be named “@keyframes,” obviously!
#webdev
[image or embed]
I don’t know why you’d want to do that, but it’s certainly an interesting thing to learn about @keyframes after 11 years of cross-browser support!
: vs. = in style queriesAnother hidden trick, this one from Temani Afif, has revealed that we can replace the colon in a style query with an equals symbol. Temani does a great job at explaining the difference, but here’s a quick code snippet to sum it up:
.Jay-Z { --Problems: calc(98 + 1); /* Evaluates as calc(98 + 1), color is blueivy */ color: if(style(--Problems: 99): red; else: blueivy); /* Evaluates as 99, color is red */ color: if(style(--Problems = 99): red; else: blueivy); }In short, = evaluates --Problems differently to :, even though Jay-Z undoubtably has 99 of them (he said so himself).
Declarative <dialog>s (and an updated .visually-hidden)David Bushell demonstrated how to create <dialog>s declaratively using invoker commands, a useful feature that allows us to skip some J’Script in favor of HTML, and works in all web browsers as of recently.
Also, thanks to an inquisitive question from Ana Tudor, the article spawned a spin-off about the minimum number of styles needed for a visually-hidden utility class. Is it still seven?
Maybe not…
How to truncate text from the middleWes Bos shared a clever trick for truncating text from the middle using only CSS:
Someone on reddit posted a demo where CSS truncates text from the middle.
They didn't post the code, so here is my shot at it with Flexbox
[image or embed]
Donnie D’Amato attempted a more-native solution using ::highlight(), but ::highlight() has some limitations, unfortunately. As Henry Wilkinson mentioned, Hazel Bachrach’s 2019 call for a native solution is still an open ticket, so fingers crossed!
How to manage color variables with relative color syntaxTheo Soti demonstrated how to manage color variables with relative color syntax. While not a new feature or concept, it’s frankly the best and most comprehensive walkthrough I’ve ever read that addresses these complexities.
How to customize lists (the modern way)In a similar article for Piccalilli, Richard Rutter comprehensively showed us how to customize lists, although this one has some nuggets of what I can only assume is modern CSS. What’s symbols()? What’s @counter-style and extends? Richard walks you through everything.
Source: Piccalilli.Can’t get enough on counters? Juan Diego put together a comprehensive guide right here on CSS-Tricks.
How to create typescales using :headingSafari Technology Preview 237 recently began trialing :heading/:heading(), as Stuart Robson explains. The follow-up is even better though, as it shows us how pow() can be used to write cleaner typescale logic, although I ultimately settled on the old-school <h1>–<h6> elements with a simpler implementation of :heading and no sibling-index():
:root { --font-size-base: 16px; --font-size-scale: 1.5; } :heading { /* Other heading styles */ } /* Assuming only base/h3/h2/h1 */ body { font-size: var(--font-size-base); } h3 { font-size: calc(var(--font-size-base) * var(--font-size-scale)); } h2 { font-size: calc(var(--font-size-base) * pow(var(--font-size-scale), 2)); } h1 { font-size: calc(var(--font-size-base) * pow(var(--font-size-scale), 3)); } Una Kravets introduced border-shapeSpeaking of new features, border-shape came as a surprise to me considering that we already have — or will have — corner-shape. However, border-shape is different, as Una explains. It addresses the issues with borders (because it is the border), allows for more shapes and even the shape() function, and overall it works differently behind the scenes.
Source: Una Kravets. modern.css wants you to stop writing CSS like it’s 2015It’s time to start using all of that modern CSS, and that’s exactly what modern.css wants to help you do. All of those awesome features that weren’t supported when you first read about them, that you forgot about? Or the ones that you missed or skipped completely? Well, modern.css has 75 code snippets and counting, and all you have to do is copy ‘em.
Kevin Powell also has some CSS snippets for youAnd the commenters? They have some too!
Honestly, Kevin is the only web dev talker that I actually follow on YouTube, and he’s so close to a million followers right now, so make sure to hit ‘ol K-Po’s “Subscribe” button.
In case you missed itActually, you didn’t miss that much! Firefox 148 released the shape() function, which was being held captive by a flag, but is now a baseline feature. Safari Technology Preview 237 became the first to trial :heading. Those are all we’ve seen from our beloved browsers in the last couple of weeks (not counting the usual flurry of smaller updates, of course).
That being said, Chrome, Safari, and Firefox announced their targets for Interop 2026, revealing which Web Platform Features they intend to make consistent across all web browsers this year, which more than makes up for the lack of shiny features this week.
Also coming up (but testable in Chrome Canary now, just like border-shape) is the scrolled keyword for scroll-state container queries. Bramus talks about scrolled scroll-state queries here.
Remember, if you don’t want to miss anything, you can catch these Quick Hits as the news breaks in the sidebar of css-tricks.com.
See you in a fortnight!
What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Yet Another Way to Center an (Absolute) Element
TL;DR: We can center absolute-positioned elements in three lines of CSS. And it works on all browsers!
.element { position: absolute; place-self: center; inset: 0; }Why? Well, that needs a longer answer.
In recent years, CSS has brought a lot of new features that don’t necessarily allow us to do new stuff, but certainly make them easier and simpler. For example, we don’t have to hardcode indexes anymore:
<ul style="--t: 8"> <li style="--i: 1"></li> <li style="--i: 2"></li> <!-- ... --> <li style="--i: 8"></li> </ul>Instead, all this is condensed into the sibling-index() and sibling-count() functions. There are lots of recent examples like this.
Still, there is one little task that feels like we’ve doing the same for decades: centering an absolutely positioned element, which we usually achieve like this:
.element { position: absolute; top: 50%; left: 50%; translate: -50% -50%; }We move the element’s top-left corner to the center, then translate it back by 50% so it’s centered.
CodePen Embed FallbackThere is nothing wrong with this way — we’ve been doing it for decades. But still it feels like the old way. Is it the only way? Well, there is another not-so-known cross-browser way to not only center, but also easily place any absolutely-positioned element. And what’s best, it reuses the familiar align-self and justify-self properties.
Turns out that these properties (along with their place-self shorthand) now work on absolutely-positioned elements. However, if we try to use them as is, we’ll notice our element doesn’t even flinch.
/* Doesn't work!! */ .element { position: absolute; place-self: center; }So, how do align-self and justify-self work for absolute elements? It may be obvious to say they should align the element, and that’s true, but specifically, they align it within its Inset-Modified Containing Block (IMCB). Okay… But what’s the IMCB?
Imagine we set our absolute element width and height to 100%. Even if the element’s position is absolute, it certainly doesn’t grow infinitely, but rather it’s enclosed by what’s known as the containing block.
The containing block is the closest ancestor with a new stacking context. By default, the initial containing block has the same dimensions as the viewport and covers the start of the page.
We can modify that containing block using inset properties (specifically top, right, bottom, and left). I used to think that inset properties fixed the element’s corners (I even said it a couple of seconds ago), but under the hood, we are actually fixing the IMCB borders.
By default, the IMCB is the same size as the element’s dimensions. So before, align-self and justify-self were trying to center the element within itself, resulting in nothing. Then, our last step is to set the IMCB so that it is the same as the containing block.
.element { position: absolute; place-self: center; top: 0; right: 0; bottom: 0; left: 0; }Or, using their inset shorthand:
.element { position: absolute; place-self: center; inset: 0; }Only three lines! A win for CSS nerds. Admittedly, I might be cheating since, in the old way, we could also use the inset property and reduce it to three lines, but… let’s ignore that fact for now.
CodePen Embed FallbackWe aren’t limited to just centering elements, since all the other align-self and justify-self positions work just fine. This offers a more idiomatic way to position absolute elements.
CodePen Embed FallbackPro tip: If we want to leave a space between the absolutely-positioned element and its containing block, we could either add a margin to the element or set the container’s inset to the desired spacing.
What’s best, I checked Caniuse, and while initially Safari didn’t seem to support it, upon testing, it seems to work on all browsers!
Yet Another Way to Center an (Absolute) Element originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
How Not to Take 10 Years to Design a Typeface
Read the book, Typographic Firsts
I have often heard type designers talk about the many years they spend developing a typeface. I would listen with awe and think, “That must have been a real challenge. It must be exquisitely crafted and probably a little bit groundbreaking too.” So it feels slightly absurd to admit that […]
The post How Not to Take 10 Years to Design a Typeface appeared first on I Love Typography Ltd.
An Exploit … in CSS?!
Ok, take a deep breath.
We’ll have some fun understanding this vulnerability once you make sure your browser isn’t affected, using the table below.
Chromium-based browserAm I safe?Google ChromeEnsure you’re running version 145.0.7632.75 or later. Go to Settings > About Chrome and check for updates.Microsoft EdgeEnsure you’re running on version 145.0.3800.58 or later. Click on the three dots (…) on the very right-hand side of the window. Click on Help and Feedback > About Microsoft Edge.VivaldiEnsure you’re running on version 7.8 or later. Click the V icon (menu) in the top-left corner, select Help > About.BraveEnsure you’re running on version v1.87.188 or later. Click the hamburger menu on the top right, select Help > About Brave.So, you updated your browser and said a prayer. When you’re able to string whole sentences together again, your first question is: Has CSS really had the dubious honor of being the cause of the first zero-day exploit in Chromium-based browsers for 2026?
I mean, the Chrome update channel says they fixed a high-severity vulnerability described as “[u]ser after free in CSS” … on Friday the 13th no less! If you can’t trust a release with a description and date like that, what can you trust? Google credits security researcher Shaheen Fazim with reporting the exploit to Google. The dude’s LinkedIn says he’s a professional bug hunter, and I’d say he deserves the highest possible bug bounty for finding something that a government agency is saying “in CSS in Google Chrome before 145.0.7632.75 allowed a remote attacker to execute arbitrary code inside a sandbox via a crafted HTML page.”
Is this really a CSS exploit?Something doesn’t add up. Even this security researcher swears by using CSS instead of JavaScript, so her security-minded readers don’t need to enable JavaScript when they read her blog. She trusts the security of CSS, even though she understands it enough to create a pure CSS x86 emulator (sidenote: woah). So far, most of us have taken for granted that the possible security issues in CSS are relatively tame. Surely we don’t suddenly live in a world where CSS can hijack someone’s OS, right?
Well, in my opinion, the headlines describing the bug as a CSS exploit in Chrome are a bit clickbait-y, because they make it sound like a pure CSS exploit, as though malicious CSS and HTML would be enough to perform it. If I’m being honest, when I first skimmed those articles in the morning before rushing out to catch the train to work, the way the articles were worded made me imagine malicious CSS like:
.malicious-class { vulnerable-property: 'rm -rf *'; }In the fictional, nightmare version of the bug that my malinformed imagination had conjured, some such CSS could be “crafted” to inject that shell command somewhere it would run on the victim’s machine. Even re-reading the reports more carefully, they feel intentionally misleading, and it wasn’t just me. My security-minded friend’s first question to me was, “But… isn’t CSS, like, super validatable?” And then I dug deeper and found out the CSS in the proof of concept for the exploit isn’t the malicious bit, which is why CSS validation wouldn’t have helped!
It doesn’t help the misunderstanding when the SitePoint article about CVE-2026-2441 bizarrely lies to its readers about what this exploit is, instead describing a different medium-severity bug that allows sending the rendered value of an input field to a malicious server by loading images in CSS. That is not what this vulnerability is.
It’s not really a CSS exploit in the sense that JavaScript is the part that exploits the bug. I’ll concede that the line of code that creates the condition necessary for a malicious script to perform this attack was in Google Chrome’s Blink CSS engine component, but the CSS involved isn’t the malicious part.
So, how did the exploit work?The CSS involvement in the exploit lies in the way Chrome’s rendering engine turns certain CSS into a CSS object model. Consider the CSS below:
@font-feature-values VulnTestFont { @styleset { entry_a: 1; entry_b: 2; entry_c: 3; entry_d: 4; entry_e: 5; entry_f: 6; entry_g: 7; entry_h: 8; } }When this CSS is parsed, a CSSFontFeaturesValueMap is added to the collection of CSSRule objects in the document.styleSheets[0].cssRules. There was a bug in the way Chrome managed the memory for the HashMap data structure underlying the JavaScript representation of the CSSFontFeaturesValueMap, which inadvertently allowed a malicious script to access memory it shouldn’t be able to. This by itself isn’t sufficient to cause harm other than crashing the browser, but it can form the basis for a Use After Free (UAF) exploit.
Chrome’s description of the patch mentions that “Google is aware that an exploit for CVE-2026-2441 exists in the wild,” although for obvious reasons, they are coy about the details for a full end-to-end exploit. Worryingly, @font-feature-values isn’t new — it’s been available since early 2023 — but the discovery of an end-to-end UAF exploit may be recent. It would make sense if the code that created the possibility of this exploit is old, but someone only pulled off a working exploit recently. If you look at this detailed explanation of a 2020 Use After Free vulnerability in Chrome within the WebAudio API, you get the sense that accessing freed memory is only one piece of the puzzle to get a UAF exploit working. Modern operating systems create hoops that attackers have to go through, which can make this kind of attack quite hard.
Real-world examples of this kind of vulnerability get complex, especially in a Chrome vulnerability where you can only trigger low-level statements indirectly. But if you know C and want to understand the basic principles with a simplified example, you can try this coding challenge. Another way to help understand the ideas is this medium post about the recent Chrome CSSFontFeaturesValueMap exploit, which includes a cute analogy in which the pointer to the object is like a leash you are still holding even after you freed your dog — but an attacker hooks the leash to a cat instead (known as type confusion), so when you command your “dog” to bark, the attacker taught his cat to think that “bark” command means to do something malicious instead.
The world is safe again, but for how long?The one-line fix I mentioned Chrome made was to change the Blink code to work with a deep copy of the HashMap that underlies the CSSFontFeaturesValueMap rather than a pointer to it, so there is no possibility of referencing freed memory. By contrast, it seems Firefox rewrote its CSS renderer in Rust and therefore tends to handle memory management automatically. Chromium started to support the use of Rust since 2023. One of the motivations mentioned was “safer (less complex C++ overall, no memory safety bugs in a sandbox either)” and to “improve the security (increasing the number of lines of code without memory safety bugs, decreasing the bug density of code) of Chrome.” Since it seems the UAF class of exploit has recurred in Chromium over the years, and these vulnerabilities tend to be high-severity when discovered, a more holistic approach to defending against such vulnerabilities might be needed, so I don’t have to freak you out with another article like this.
An Exploit … in CSS?! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
A Complete Guide to Bookmarklets
You’re surely no stranger to bookmarks. The ability to favorite, save, or “bookmark” web pages has been a staple browser feature for decades. Browsers don’t just let you bookmark web pages, though. You can also bookmark JavaScript, allowing you to do so much more than merely save pages.
A JavaScript script saved as a bookmark is called a “bookmarklet,” although some people also use the term “favelet” or “favlet.” Bookmarklets have been around since the late 90s. The site that coined them, bookmarklets.com, even remains around today. They’re simple and versatile, a fact evidenced by most of the bookmarklets listed on the aforementioned site are still working today despite being untouched for over two decades.
While bookmarklets have fallen a bit to the wayside in more recent years as browsers have grown more capable and dev tools have matured, they’re still a valuable tool in any web developer’s arsenal. They’re simple but capable, and no additional software is needed to create or use them. If you watch any good machinist or engineer at work, they’re constantly building tools and utilities, even one-off contraptions, to address problems or come to a more graceful solution as they work. As developers, we should endeavor to do the same, and bookmarklets are a perfect way to facilitate such a thing.
Making a BookmarkletBookmarklets are extremely easy to make. You write a script in exactly the same manner you would if writing it for the browser console. You then save it as a bookmark, prefixing it with javascript: which designates it for use in the browser URL bar.
Let’s work through making a super basic bookmarklet, one that sends a simple alert. We’ll take the below code, which triggers a message using the alert() method, and bookmarklet-ify it.
alert("Hello, World!");Next, we will turn it into an Immediately Invoked Function Expression (IIFE), which has a few benefits. Firstly, it creates a new scope to avoid polluting the global namespace and prevents our bookmarklet from interfering with JavaScript already on the page, or vice versa. Secondly, it will cause the bookmarklet to trigger upon click.
We’ll achieve this by enclosing it within an anonymous function (lambda) (e.g., (() => {})) and suffixing it with ();, which will execute our function.
(() => { alert("Hello, World!"); })();For reliability across browsers, it is to our benefit to URL-encode our bookmarklet to escape special characters. Without doing so, browsers can go awry and misinterpret our code. Even if it isn’t entirely necessary with a simple bookmarklet like this, it can prevent a lot of trouble that may arise with more complexity. You can encode your bookmarklet yourself using JavaScript’s encodeURIComponent() function, or you can use one of a number of existing tools. We’ll also reduce it to a single line.
(()%3D%3E%7Balert(%22Hello%2C%20World!%22)%3B%7D)()%3BWe must prefix javascript: so that our browser knows this is not a standard URL to a webpage but instead a JavaScript bookmarklet.
javascript:(()%3D%3E%7Balert(%22Hello%2C%20World!%22)%3B%7D)()%3B Installing a BookmarkletFinally, we must add it to our browser as a bookmarklet. As you might expect, this is extremely dependent on the browser you’re using.
In Safari on macOS, the easiest way is to bookmark a webpage and then edit that bookmark into a bookmarklet:
In Firefox on desktop, the easiest way is to secondary click on the bookmark toolbar and then “Add Bookmark…”:
In Chrome on desktop, the easiest way is to secondary click on the bookmark toolbar and then “Add page…”:
Many mobile browsers also allow the creation and usage of bookmarks. This can be especially valuable, as browser dev tools are often unavailable on mobile.
CSS BookmarkletsYou’ve no doubt been looking at the word “JavaScript” above with a look of disdain. This is CSS-Tricks after all. Fear not, because we can make bookmarklets that apply CSS to our page in a plethora of ways.
My personal favorite method from an authoring perspective is to create a <style> element with my chosen content:
javascript: (() => { var style = document.createElement("style"); style.innerHTML = "body{background:#000;color:rebeccapurple}"; document.head.appendChild(style); })();The much more graceful approach is to use the CSSStyleSheet interface. This approach allows for incremental updates and lets you directly access the CSS Object Model (CSSOM) to read selectors, modify existing properties, remove or reorder rules, and inspect computed structure. The browser also validates values input this way, which helps prevent you from inputting broken CSS. It is more complex but also gives you greater control.
javascript: (() => { const sheet = new CSSStyleSheet(); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; sheet.insertRule("body { border: 5px solid rebeccapurple !important; }", 0); sheet.insertRule("img { filter: contrast(10); }", 1); })();As we’re writing CSS for general usage across whatever page we wish to use our bookmarklet on, it is important to remain aware that we may run into issues with specificity or conflicts with the page’s existing stylesheets. Using !important is usually considered a bad code smell, but in the context of overriding unknown existing styles, it is a reasonable way to address our needs.
LimitationsUnfortunately, there are a few roadblocks that can hinder our usage of bookmarklets. The most pervasive are Content Security Policies (CSP). A CSP is a security feature that attempts to prevent malicious actions, such as cross-site scripting attacks, by allowing websites to regulate what can be loaded. You wouldn’t want to allow scripts to run on your bank’s website, for instance. A bookmarklet that relies on cross-origin requests (requests from outside the current website) is very frequently blocked. For this reason, a bookmarklet should ideally be self-contained, rather than reliant on anything external. If you’re suspicious a bookmarklet is being blocked by a website’s security policies, you can check the console in your browser’s developer tools for an error.
As bookmarklets are just URLs, there isn’t any strict limit to the length specified. In usage, browsers do impose limits, though they’re higher than you’ll encounter in most cases. In my own testing (which may vary by version and platform), here are the upper limits I found: The largest bookmarklet I could create in both Firefox and Safari was 65536 bytes. Firefox wouldn’t let me create a bookmarklet of any greater length, and Safari would let me create a bookmarklet, but it would do nothing when triggered. The largest bookmarklet I could create in Chrome was 9999999 characters long, and I started having issues interacting with the textbox after that point. If you need something longer, you might consider loading a script from an external location, keeping in mind the aforementioned CSP limitations:
javascript:(() => { var script=document.createElement('script'); script.src='https://example.com/bookmarklet-script.js'; document.body.appendChild(script); })();Otherwise, you might consider a userscript tool like TamperMonkey, or, for something more advanced, creating your own browser extension. Another option is creating a snippet in your browser developer tools. Bookmarklets are best for small snippets.
Cool BookmarkletsNow that you’ve got a gauge on what bookmarklets are and, to an extent, what they’re capable of, we can take a look at some useful ones. However, before we do, I wish to stress that you should be careful running bookmarklets you find online. Bookmarklets you find online are code written by someone else. As always, you should be wary, cautious, and discerning. People can and have written malicious bookmarklets that steal account credentials or worse.
For this reason, if you paste code starting with javascript: into the address bar, browsers automatically strip the javascript: prefix to prevent people from unwittingly triggering bookmarklets. You’ll need to reintroduce the prefix. To get around the javascript: stripping, bookmarklets are often distributed as links on a page, which you’re expected to drag and drop into your bookmarks.
Specific bookmarklets have been talked about on CSS-Tricks before. Given the evolution of browsers and the web platform, much has been obsoleted now, but some more contemporary articles include:
- 6 Useful Bookmarklets to Boost Web Development by Daniel Schwarz.
- Using the CSS Me Not Bookmarklet to See (and Disable) CSS Files by Chris Coyier.
Be sure to check out the comments of those posts, for they’re packed with countless great bookmarklets from the community. Speaking of bookmarklets from the community:
- Adrian Roselli has a fantastic collection of “CSS Bookmarklets for Testing and Fixing.”
- Stuart Robson put together “A Few Useful Web Development Bookmarklets.”
- Ian Lloyd has a selection of bookmarklets for performing accessibility audits
If you’ve got any golden bookmarklets that you find valuable, be sure to share them in the comments.
A Complete Guide to Bookmarklets originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Event Feature: Type Drives Commerce
Read the book, Typographic Firsts
In global branding and design, typography drives consumer perception and commercial success. This idea sits at the heart of the Type Directors Club (TDC) Type Drives Commerce conference on March 13, at Fordham University, Lincoln Center in New York City. As part of the world’s leading typography organization, the conference is a curated exploration […]
The post Event Feature: Type Drives Commerce appeared first on I Love Typography Ltd.
Distinguishing “Components” and “Utilities” in Tailwind
Here’s a really quick tip. You can think of Tailwind utilities as components — because you can literally make a card “component” out of Tailwind utilities.
@utility card { border: 1px solid black; padding: 1rlh; } <div class="card"> ... </div>This blurs the line between “Components” and “Utilities” so we need to better define those terms.
The Great Divide — and The Great UnificationCSS developers often define Components and Utilities like this:
- Component = A group of styles
- Utility = A single rule
This collective thinking has emerged from the terminologies we have gathered over many years. Unfortunately, they’re not really the right terminologies.
So, let’s take a step back and consider the actual meaning behind these words.
Component means: A thing that’s a part of a larger whole.
Utility means: It’s useful.
So…
- Utilities are Components because they’re still part of a larger whole.
- Components are Utilities because they’re useful.
The division between Components and Utilities is really more of a marketing effort designed to sell those utility frameworks — nothing more than that.
It. Really. Doesn’t. Matter.
The meaningful divide?Perhaps the only meaningful divide between Components and Utilities (in the way they’re commonly defined so far) is that we often want to overwrite component styles.
It kinda maps this way:
- Components: Groups of styles
- Utilities: Styles used to overwrite component styles.
Personally, I think that’s a very narrow way to define something that actually means “useful.”
Just overwrite the dang styleTailwind provides us with an incredible feature that allows us to overwrite component styles. To use this feature, you would have to:
- Write your component styles in a components layer.
- Overwrite the styles via a Tailwind utility.
But this is a tedious way of doing things. Imagine writing @layer components in all of your component files. There are two problems with that:
- You lose the ability to use Tailwind utilities as components
- You gotta litter your files with many @layer component declarations — which is one extra indentation and makes the whole CSS a little more difficult to read.
There’s a better way of doing this — we can switch up the way we use CSS layers by writing utilities as components.
@utility card { padding: 1rlh; border: 1px solid black; }Then, we can overwrite styles with another utility using Tailwind’s !important modifier directly in the HTML:
<div class="card !border-blue-500"> ... </div>I put together an example over at the Tailwind Playground.
Unorthodox TailwindThis article comes straight from my course, Unorthodox Tailwind, where you’ll learn to use CSS and Tailwind in a synergistic way. If you liked this, there’s a lot more inside: practical ways to think about and use Tailwind + CSS that you won’t find in tutorials or docs.
Check it outDistinguishing “Components” and “Utilities” in Tailwind originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Spiral Scrollytelling in CSS With sibling-index()
Confession time: I’ve read about the performance benefits of scroll-timeline(), but when I see an impressive JavaScript scrollytelling site like this one, it makes me question if the performance of old-school, main-thread scrollytelling is all that bad. The other shoe drops when the creators of that site admit they “ran into real limits,” and “mobile technically works, but it loses parallax and chops compositions,” to the extent that they “chose to gate phones to protect the first impression.” Put another way: they couldn’t get it working on mobile, and it sounds like JavaScript performance may have been one of the culprits.
The creator of another of my favorite scrolling experiments — which also uses JavaScript and also works best on desktop — called out that his text vortex section “would look better if it were applied for each character rather than each word, but that’s incredibly difficult to pull off using this same technique without incurring an astronomical performance impact.”
Challenge accepted.
He may have inadvertently created a realistic benchmark test for smoothly animating hundreds of divs based on scrolling.
That’s our cue to see if we can make a lookalike effect using modern CSS features to smoothly spiral every character in a string of text as the user scrolls down. To give the original text vortex some CSS sibling rivalry, let’s give the new sibling-index() function a whirl, although it is still waiting on Firefox support at the time of writing. Therefore, as a fallback for the CodePen below, you can also watch the video of the screen recording.
CodePen Embed Fallback Confession #2: This uses some scriptThe only JavaScript is to split the text into a <div> for each character, but the animation is pure CSS. I could have hardcoded all the markup instead, but that would make the HTML annoying to read and maintain. The following script makes it easy for you to experiment with the pen by tweaking the text content.
const el = document.querySelector(".vortex"); el.innerHTML = el.innerHTML.replaceAll(/\s/g, '⠀'); new SplitText(".title", { type: "chars", charsClass: "char" });The SplitText plugin referenced here is from the freely available GSAP library. The plugin is designed to be usable standalone outside GSAP, which is what’s happening here. It is nice and simple to use, and it even populates aria-label so screen readers can see our text, regardless of the way we tokenize it. The one complication was that I wanted every space character to be in its own <div> that I could position. The simplest way I could find was to replace the spaces with a special space character, which SplitText will put into its own <div>. If anyone knows a better way, I’d love to hear about it in the comments.
Now that we have each character living in its own <div>, we can implement the CSS to handle the spiral animation.
.vortex { position: fixed; left: 50%; height: 100vh; animation-name: vortex; animation-duration: 20s; animation-fill-mode: forwards; animation-timeline: scroll(); .char { --radius: calc(10vh - (7vh/sibling-count() * sibling-index())); --rotation: calc((360deg * 3/sibling-count()) * sibling-index()); position: absolute !important; top: 50%; left: 50%; transform: rotate(var(--rotation)) translateY(calc(-2.9 * var(--radius))) scale(calc(.4 - (.25/(sibling-count()) * sibling-index()))); animation-name: fade-in; animation-ranger-start: calc(90%/var(sibling-count()) * var(--sibling-index())); animation-fill-mode: forwards; animation-timeline: scroll(); } } Spiral and fade the elements using sibling-index() and sibling-count()We use the sibling-count and sibling-index functions together to calculate a gradual decrease for several properties of the characters when the sibling-index increases, using a formula like this:
propertyValue = startValue - ((reductionValue/totalCharacters) * characterIndex)The first character starts near the maximum value. Each subsequent character subtracts a slightly larger fraction, so properties gradually dwindle to a chosen target value as the characters spiral inward. This technique is used to drive scale, rotation, and distance from the center.
If the goal had been to arrange the characters in a circle instead of a spiral, I would have used CSS trigonometric functions as demonstrated here. However, the spiral seemed simpler to calculate without trig. Evidently, the original JavaScript version that inspired my CSS text spiral didn’t use trig either. The scroll animation is relatively simple as it’s just scaling and rotating the entire parent element to give the illusion that the viewer is being sucked into the vortex.
The only animation applied to individual characters is fade-in which is delayed increasingly for each character in the string, using another variation on the usage of the ratio of sibling-index() to sibling-count(). In this case, we increment animation-range-start to stagger the delay before characters fade in as the user scrolls. It’s reminiscent of the infamous scroll-to-fade effect, and it makes me realize how often we reach for JavaScript just because it allows us to base styling on element indexes. Therefore, many JavaScript effects can likely be replaced with CSS once sibling-index() goes Baseline. Please do let me know in the comments if you can think of other examples of JavaScript effects we could recreate in CSS using sibling-index().
Spiral Scrollytelling in CSS With sibling-index() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Interop 2026
Interop 2026 is formally a thing. So, you know all of those wild, new CSS features we’re always poking at but always putting under a “lack of browser support” caveat? The Big Three — Blink (Chrome/Edge), WebKit (Safari), and Mozilla (Firefox) — are working together to bring full and consistent support to them!
You can read the blog posts yourself:
- Interop 2026: Continuing to improve the web for developers (web.dev)
- Announcing Interop 2026 (WekBit Blog)
- Launching Interop 2026 (Mozilla)
- Microsoft Edge and Interop 2026 (Edge)
An, yes, there’s plenty to get excited about specifically for CSS:
Anchor positioningFrom our guide:
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.
anchor positioning Article on Apr 28, 2025 Anchor Positioning Just Don’t Care About Source Order Geoff Graham accessibility anchor positioning positioning Article on Sep 10, 2024 Anchor Positioning Quirks Juan Diego Rodríguez anchor positioning Article on Oct 2, 2024 CSS Anchor Positioning Guide Juan Diego Rodríguez Interest Invoker API popover Article on Jul 23, 2025 A First Look at the Interest Invoker API (for Hover-Triggered Popovers) Daniel Schwarz anchor positioning Links & URLs Article on Jan 17, 2025 Fancy Menu Navigation Using Anchor Positioning Temani Afif Advanced attr()We’ve actually had the attr() function for something like 15 years. But now we’re gonna be able to pass variables in there… with type conversion!
Almanac on Mar 27, 2025 attr() .element { color: attr(data-color type()); } Daniel Schwarz charts data visualization Article on Feb 5, 2026 CSS Bar Charts Using Modern Functions Preethi attr() custom properties Article on Nov 2, 2017 The CSS attr() function got nothin’ on custom properties Chris Coyier Container style queriesWe can already query containers by “type” but only by size. It’ll be so much cooler when we can apply styles based on other styles. Say:
@container style((font-style: italic) and (--color-mode: light)) { em, i, q { background: lightpink; } } container-queries Article on Jun 9, 2021 A Cornucopia of Container Queries Geoff Graham container-queries Article on Oct 12, 2022 Early Days of Container Style Queries Geoff Graham container-queries Article on Dec 1, 2022 Digging Deeper Into Container Style Queries Geoff Graham Scroll Driven Animation Style Queries Article on Mar 31, 2025 Worlds Collide: Keyframe Collision Detection Using Style Queries Lee Meyer container-queries Article on Jun 10, 2024 CSS Container Queries Geoff Graham The contrast-color() functionGetting the right color contrast between foreground text and background can be easy enough, but it’s been more of a manual type thing that we might switch with a media query based on the current color scheme. With contrast-color() (I always want to write that as color-contrast(), maybe because that was the original name) we can dynamically toggle the color between white and black.
button { --background-color: darkblue; background-color: var(--background-color); color: contrast-color(var(--background-color)); } color-contrast() safari Article on Apr 26, 2021 Exploring color-contrast() for the First Time Chris Coyier accessibility color CSS functions Article on Jun 5, 2025 Exploring the CSS contrast-color() Function… a Second Time Daniel Schwarz color CSS functions Link on Oct 8, 2025 The thing about contrast-color Geoff Graham accessibility color color-contrast() relative color Article on Feb 11, 2026 Approximating contrast-color() With Other CSS Features Kevin Hamer Custom HighlightsHighlight all the things! We’ve had ::selection forever, but now we’ll have a bunch of others:
Pseudo-selectorSelects…Notes::search-textFind-in-page matches::search-text:currentselects the current target::target-textText fragmentsText fragments allow for programmatic highlighting using URL parameters. If you’re referred to a website by a search engine, it might use text fragments, which is why ::target-text is easily confused with ::search-text.::selectionText highlighted using the pointer::highlight()Custom highlights as defined by JavaScript’s Custom Highlight API::spelling-errorIncorrectly spelled wordsPretty much applies to editable content only::grammar-errorIncorrect grammarPretty much applies to editable content only ::selection custom highlight api selecting text Article on Mar 1, 2022 CSS Custom Highlight API: The Future of Highlighting Text Ranges on the Web Patrick Brosset pseudo elements Article on Jan 28, 2026 Styling ::search-text and Other Highlight-y Pseudo-Elements Daniel Schwarz Dialogs and popoversFinally, a JavaScript-less (and declarative) way to set elements on the top layer! We’ve really dug into these over the years.
accessibility dialog popover Article on Oct 23, 2024 Clarifying the Relationship Between Popovers and Dialogs Zell Liew popover Article on Jun 26, 2024 Poppin’ In Geoff Graham accessibility dialog modal Article on Jan 26, 2026 There is No Need to Trap Focus on a Dialog Element Zell Liew game popover Article on Jul 25, 2024 Pop(over) the Balloons John Rhea anchor positioning popover WordPress Article on Feb 19, 2025 Working With Multiple CSS Anchors and Popovers Inside the WordPress Loop Geoff Graham popover Article on Jun 9, 2025 Creating an Auto-Closing Notification With an HTML Popover Preethi Interest Invoker API popover Article on Jul 23, 2025 A First Look at the Interest Invoker API (for Hover-Triggered Popovers) Daniel Schwarz dialog Article on Jun 3, 2025 Getting Creative With HTML Dialog Andy Clarke dialog modal pseudo elements Link on Jan 15, 2018 Meet the New Dialog Element Robin Rendle css properties dialog Link on Dec 1, 2025 Prevent a page from scrolling while a dialog is open Geoff Graham attributes dialog popover Article on Nov 20, 2024 Invoker Commands: Additional Ways to Work With Dialog, Popover… and More? Daniel Schwarz accessibility dialog Article on Oct 7, 2019 Some Hands-On with the HTML Dialog Element Chris Coyier Media pseudo-classesHow often have you wanted to style an <audio> or <video> element based on its state? Perhaps with, JavaScript, right? We’ll have several states in CSS to work off:
- :playing
- :paused
- :seeking
- :buffering
- :stalled
- :muted
- :volume-locked
I love this example from the WebKit announcement:
video:buffering::after { content: "Loading..."; } Scroll-driven animationsOK, we all want this one. We’re talking specifically about animation that responds to scrolling. In other words, there’s a direct link between scrolling progress and the animation’s progress.
#progress { animation: grow-progress linear forwards; animation-timeline: scroll(); } carousel Article on May 15, 2025 Scroll-Driven Animations Inside a CSS Carousel Geoff Graham position Scroll Driven Animation Article on Jul 11, 2025 Scroll-Driven Sticky Heading Amit Sheen animation Scroll Driven Animation Article on Aug 6, 2025 Bringing Back Parallax With Scroll-Driven CSS Animations Blake Lundquist Scroll Driven Animation Article on May 5, 2025 Modern Scroll Shadows Using Scroll-Driven Animations Kevin Hamer Scroll Driven Animation Link on Feb 13, 2025 Scroll Driven Animations Notebook Geoff Graham animation Scroll Driven Animation Article on Oct 21, 2024 Unleash the Power of Scroll-Driven Animations Geoff Graham Scroll Driven Animation Article on Sep 27, 2024 Slide Through Unlimited Dimensions With CSS Scroll Timelines Lee Meyer Scroll Driven Animation Article on Nov 1, 2024 Web-Slinger.css: Like Wow.js But With CSS-y Scroll Animations Lee Meyer Scroll snappingNothing new here, but bringing everyone in line with how the specs have changed over the years!
Almanac on Feb 13, 2019 scroll-margin .scroll-element { scroll-margin: 50px 0 0 50px; } Andy Adams Almanac on Feb 12, 2019 scroll-padding .scroll-element{ scroll-padding: 50px 0 0 50px; } Andy Adams Almanac on Feb 21, 2019 scroll-snap-align .element { scroll-snap-align: start; } Andy Adams Almanac on Mar 7, 2019 scroll-snap-stop .element { scroll-snap-stop: always; } Andy Adams Almanac on Feb 4, 2019 scroll-snap-type .scroll-element { scroll-snap-type: y mandatory; } Andy Adams public speaking scroll-snap Article on Feb 7, 2022 CSS Scroll Snap Slide Deck That Supports Live Coding Stephanie Eckles scroll-snap twitter user styles Article on Aug 5, 2022 How I Added Scroll Snapping To My Twitter Timeline Šime Vidas scroll-snap Link on Mar 27, 2020 How to use CSS Scroll Snap Chris Coyier scroll-snap Article on Mar 2, 2016 Introducing CSS Scroll Snap Points Sarah Drasner scroll-snap scrolling Article on Aug 15, 2018 Practical CSS Scroll Snapping Max Kohler The shape() functionThis is one that Temani has been all over lately and his SVG Path to Shape Converter is a must-bookmark. The shape() can draw complex shapes when clipping elements with the clip-path property. We’ve had the ability to draw basic shapes for years — think circle, ellipse(), and polygon() — but no “easy” way to draw more complex shapes. And now we have something less SVG-y that accepts CSS-y units, calculations, and whatnot.
.clipped { width: 250px; height: 100px; box-sizing: border-box; background-color: blue; clip-path: shape( from top left, hline to 100%, vline to 100%, curve to 0% 100% with 50% 0%, ); } CSS functions Article on May 2, 2025 CSS shape() Commands Geoff Graham art clip-path CSS functions css shapes Article on May 23, 2025 Better CSS Shapes Using shape() — Part 1: Lines and Arcs Temani Afif art clip-path CSS functions css shapes Article on May 30, 2025 Better CSS Shapes Using shape() — Part 2: More on Arcs Temani Afif art clip-path CSS functions css shapes Article on Jun 6, 2025 Better CSS Shapes Using shape() — Part 3: Curves Temani Afif art clip-path CSS functions css shapes Article on Jul 7, 2025 Better CSS Shapes Using shape() — Part 4: Close and Move Temani Afif CSS functions resource shapes Link on May 21, 2025 SVG to CSS Shape Converter Geoff Graham View transitionsThere are two types of view transitions: same-document (transitions on the same page) and cross-document (or what we often call multi-page transitions). Same-page transitions went Baseline in 2025 and now browsers are working to be cross-compatible implementations of cross-document transitions.
view transitions Article on Feb 21, 2025 Toe Dipping Into View Transitions Geoff Graham view transitions Article on Jan 29, 2025 What on Earth is the `types` Descriptor in View Transitions? Juan Diego Rodríguez view transitions Almanac on Jun 7, 2024 ::view-transition ::view-transition { position: fixed; } Geoff Graham view transitions Almanac on Jun 12, 2024 ::view-transition-group ::view-transition-group(transition-name) { animation-duration: 1.25s; } Geoff Graham view transitions Almanac on Jun 14, 2024 ::view-transition-image-new ::view-transition-image-new(*) { animation-duration: 700ms; } Geoff Graham view transitions Almanac on ::view-transition-image-old ::view-transition-image-old(*) { animation-duration: 700ms; } Geoff Graham view transitions Almanac on ::view-transition-image-pair ::view-transition-image-pair(root) { animation-duration: 1s; } Geoff Graham view transitions Almanac on Jan 22, 2026 ::view-transition-new() ::view-transition-new(item) { animation-name: fade-in; } Sunkanmi Fafowora view transitions Almanac on ::view-transition-old() ::view-transition-old(item) { animation-name: fade-out; } Sunkanmi Fafowora view transitions Almanac on Jan 22, 2025 @view-transition @view-transition { navigation: auto; } Juan Diego Rodríguez Almanac on Jul 2, 2025 view() .element { animation-timeline: view(); } Saleh Mubashar view transitions Almanac on Jan 20, 2026 view-transition-class .element { view-transition-class: bearhugs; } Sunkanmi Fafowora view transitions Almanac on May 29, 2024 view-transition-name .element { view-transition-name: image-zoom; } Geoff Graham CSS zoom propertyOh, I wasn’t expecting this! I mean, we’ve had zoom for years — our Almanac page was published back in 2011 — but as a non-standard property. I must have overlooked that it was Baseline 2024 newly available and worked on as part of Interop 2025. It’s carrying over into this year.
zoom is sorta like the scale() function, but it actually affects the layout whereas scale() it’s merely visual and will run over anything in its way.
That’s a wrap! Bookmark the Interop 2026 Dashboard to keep tabs on how things are progressing along.
Interop 2026 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Making a Responsive Pyramidal Grid With Modern CSS
In the previous article, we built the classic hexagon grid. It was a responsive implementation without the use of media queries. The challenge was to improve a five-year old approach using modern CSS.
Support is limited to Chrome only because this technique uses recently released features, including corner-shape, sibling-index(), and unit division.
CodePen Embed FallbackIn this article, we will explore another type of grid: a pyramidal one. We are still working with hexagon shapes, but a different organization of the elements.
A demo worth a thousand words:
CodePen Embed FallbackFor better visualization, open the full-page view of the demo to see the pyramidal structure. On screen resize, you get a responsive behavior where the bottom part starts to behave similarly to the grid we created in the previous article!
Cool right? All of this was made without a single media query, JavaScript, or a ton of hacky CSS. You can chunk as many elements as you want, and everything will adjust perfectly.
Before we start, do yourself a favor and read the previous article if you haven’t already. I will skip a few things I have already explained there, such as how the shapes are created as well as a few formulas I will reuse here. Similar to the previous article, the implementation of the pyramidal grid is an improvement of a five-year old approach, so if you want to make a comparison between 2021 and 2026, check out that older article as well.
The Initial ConfigurationThis time, we will rely on CSS Grid instead of Flexbox. With this structure, it’s easy to control the placement of items inside columns and rows rather than adjusting margins.
<div class="container"> <div></div> <div></div> <div></div> <div></div> <!-- etc. --> </div> .container { --s: 40px; /* size */ --g: 5px; /* gap */ display: grid; grid-template-columns: repeat(auto-fit, var(--s) var(--s)); justify-content: center; gap: var(--g); } .container > * { grid-column-end: span 2; aspect-ratio: cos(30deg); border-radius: 50% / 25%; corner-shape: bevel; margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg))); }I am using the classic repeated auto-fit to create as many columns as the free space allows. For the items, it’s the same code of the previous article for creating hexagon shapes.
You wrote var(--s) twice. Is that a typo?
It’s not! I want my grid to always have an even number of columns, where each item spans two columns (that’s why I am using grid-column-end: span 2). With this configuration, I can easily control the shifting between the different rows.
Above is a screenshot of DevTools showing the grid structure. If, for example, item 2 spans columns 3 and 4, then item 4 should span columns 2 and 3, item 5 should span columns 4 and 5, and so on.
It’s the same logic with the responsive part. Each first item of every other row is shifted by one column and starts on the second column.
With this configuration, the size of an item will be equal to 2*var(--s) + var(--g). For this reason, the negative bottom margin is different from the previous example.
So, instead of this:
margin-bottom: calc(var(--s)/(-4*cos(30deg)));…I am using:
margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg))); CodePen Embed FallbackNothing fancy so far, but we already have 80% of the code. Believe it or not, we are only one property away from completing the entire grid. All we need to do is set the grid-column-start of a few elements to have the correct placement and, as you may have guessed, here comes the trickiest part involving a complex calculation.
The Pyramidal GridLet’s suppose the container is large enough to contain the pyramid with all the elements. In other words, we will ignore the responsive part for now. Let’s analyze the structure and try to identify the patterns:
Regardless of the number of items, the structure is somehow static. The items on the left (i.e., the first item of each row) are always the same (1, 2, 4, 7, 11, and so on). A trivial solution is to target them using the :nth-child() selector.
:nth-child(1) { grid-column-start: ?? } :nth-child(2) { grid-column-start: ?? } :nth-child(4) { grid-column-start: ?? } :nth-child(7) { grid-column-start: ?? } :nth-child(11) { grid-column-start: ?? } /* etc. */The positions of all of them are linked. If item 1 is placed in column x, then item 2 should be placed in column x - 1, item 4 in column x - 2, and so forth.
:nth-child(1) { grid-column-start: x - 0 } /* 0 is not need but useful to see the pattern*/ :nth-child(2) { grid-column-start: x - 1 } :nth-child(4) { grid-column-start: x - 2 } :nth-child(7) { grid-column-start: x - 3 } :nth-child(11) { grid-column-start: x - 4 } /* etc. */Item 1 is logically placed in the middle, so if our grid contains N columns, then x is equal to N/2:
:nth-child(1) { grid-column-start: N/2 - 0 } :nth-child(2) { grid-column-start: N/2 - 1 } :nth-child(4) { grid-column-start: N/2 - 2 } :nth-child(7) { grid-column-start: N/2 - 3 } :nth-child(11){ grid-column-start: N/2 - 4 }And since each item spans two columns, N/2 can also be seen as the number of items that can fit within the container. So, let’s update our logic and consider N to be the number of items instead of the number of columns.
:nth-child(1) { grid-column-start: N - 0 } :nth-child(2) { grid-column-start: N - 1 } :nth-child(4) { grid-column-start: N - 2 } :nth-child(7) { grid-column-start: N - 3 } :nth-child(11){ grid-column-start: N - 4 } /* etc. */To calculate the number of items, I will use the same formula as in the previous article:
N = round(down, (container_size + gap)/ (item_size + gap));The only difference is that the size of an item is no longer var(--s)but 2*var(--s) + var(--g), which gives us the following CSS:
.container { --s: 40px; /* size */ --g: 5px; /* gap */ container-type: inline-size; /* we make it a container to use 100cqw */ } .container > * { --_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g)))); } .container > *:nth-child(1) { grid-column-start: calc(var(--_n) - 0) } .container > *:nth-child(2) { grid-column-start: calc(var(--_n) - 1) } .container > *:nth-child(4) { grid-column-start: calc(var(--_n) - 2) } .container > *:nth-child(7) { grid-column-start: calc(var(--_n) - 3) } .container > *:nth-child(11){ grid-column-start: calc(var(--_n) - 4) } /* etc. */ CodePen Embed FallbackIt works! We have our pyramidal structure. It’s not yet responsive, but we will get there. By the way, if your goal is to build such a structure with a fixed number of items, and you don’t need responsive behavior, then the above is perfect and you’re done!
How come all the items are correctly placed? We only defined the column for a few items, and we didn’t specify any row!
That’s the power of the auto-placement algorithm of CSS Grid. When you define the column for an item, the next one will be automatically placed after it! We don’t need to manually specify a bunch of columns and rows for all the items.
Improving the ImplementationYou don’t like those verbose :nth-child() selectors, right? Me too, so let’s remove them and have a better implementation. Such a pyramid is well known in the math world, and we have something called a triangular number that I am going to use. Don’t worry, I will not start a math course, so here is the formula I will be using:
j*(j + 1)/2 + 1 = index…where j is a positive integer (zero included).
In theory, all the :nth-child can be generated using the following pseudo code:
for(j = 0; j< ?? ;j++) { :nth-child(j*(j + 1)/2 + 1) { grid-column-start: N - j } }We don’t have loops in CSS, so I will follow the same logic I did in the previous article (which I hope you read, otherwise you will get a bit lost). I express j using the index. I solved the previous formula, which is a quadratic equation, but I am sure you don’t want to get into all that math.
j = sqrt(2*index - 1.75) - .5We can get the index using the sibling-index() function. The logic is to test for each item if sqrt(2*index - 1.75) - .5 is a positive integer.
.container { --s: 40px; /* size */ --g: 5px; /* gap */ container-type: inline-size; /* we make it a container to use 100cqw */ } .container > * { --_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g)))); --_j: calc(sqrt(2*sibling-index() - 1.75) - .5); --_d: mod(var(--_j),1); grid-column-start: if(style(--_d: 0): calc(var(--_n) - var(--_j));); }When the --_d variable is equal to 0, it means that --_j is an integer; and when that’s the case I set the column to N - j. I don’t need to test if --_j is positive because it’s always positive. The smallest index value is 1, so the smallest value of --_j is 0.
CodePen Embed FallbackTada! We replaced all the :nth-child() selectors with three lines of CSS that cover any number of items. Now let’s make it responsive!
The Responsive BehaviorBack in my 2021 article, I switched between the pyramidal grid and the classic grid based on screen size. I will do something different this time. I will keep building the pyramid until it’s no longer possible, and from there, it will turn into the classic grid.
Items 1 to 28 form the pyramid. After that, we get the same classic grid we built in the previous article. We need to target the first items of some rows (29, 42, etc.) and shift them. We are not going to set a margin on the left this time, but we do need to set their grid-column-start value to 2.
As usual, we identify the formula of the items, express it using the index, and then test if the result is a positive integer or not:
N*i + (N - 1)*(i - 1) + 1 + N*(N - 1)/2 = indexSo:
i = (index - 2 + N*(3 - N)/2)/(2*N - 1)When i is a positive integer (zero excluded), we set the column start to 2.
.container { --s: 40px; /* size */ --g: 5px; /* gap */ container-type: inline-size; /* we make it a container to use 100cqw */ } .container > * { --_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g)))); /* code for the pyramidal grid */ --_j: calc(sqrt(2*sibling-index() - 1.75) - .5); --_d: mod(var(--_j),1); grid-column-start: if(style(--_d: 0): calc(var(--_n) - var(--_j));); /* code for the responsive grid */ --_i: calc((sibling-index() - 2 + (var(--_n)*(3 - var(--_n)))/2)/(2*var(--_n) - 1)); --_c: mod(var(--_i),1); grid-column-start: if(style((--_i > 0) and (--_c: 0)): 2;); }Unlike the --_j variable, I need to test if --_i is a positive value, as it can be negative for some index values. For this reason, I have an extra condition compared to the first one.
But wait! That’s no good at all. We are declaring grid-column-start twice, so only one of them will get used. We should have only one declaration, and for that, we can combine both conditions using a single if() statement:
grid-column-start: if( style((--_i > 0) and (--_c: 0)): 2; /* first condition */ style(--_d: 0): calc(var(--_n) - var(--_j)); /* second condition */ );If the first condition is true (the responsive grid), we set the value to 2; else if the second condition is true (the pyramidal grid), we set the value to calc(var(--_n) - var(--_j)); else we do nothing.
Why that particular order?
Because the responsive grid should have a higher priority. Check the figure below:
Item 29 is part of the pyramidal grid since it’s the first item in its row. This means that the pyramidal condition will always be true for that item. But when the grid becomes responsive, that item becomes part of the responsive grid, and the other condition is also true. When both conditions are true, the responsive condition one should win; that’s why it’s the first condition we test.
Let’s see this in play:
CodePen Embed FallbackOops! The pyramid looks good, but after that, things get messy.
To understand what is happening, let’s look specifically at item 37. If you check the previous figure, you will notice it’s part of the pyramidal structure. So, even if the grid becomes responsive, its condition is still true and it gets a column value from the formula calc(var(--_n) - var(--_j)) which is not good because we want to keep its default value for auto-placement. That’s the case for many items, so we need to fix them.
To find the fix, let’s see how the values in the pyramid behave. They all follow the formula N - j, where j is a positive integer. If, for example, N is equal to 10 we get:
10, 9, 8, 7, ... ,0, -1 , -2At certain points, the values become negative, and since negative values are valid, those items will be randomly placed, disrupting the grid. We need to ensure the negative values are ignored, and the default value is used instead.
We use the following to keep only the positive value and transform all the negative ones into zeroes:
max(0, var(--_n) - var(--_j))We set 0 as a minimum boundary (more on that here) and the values become:
10, 9, 8, 7, ... , 0, 0, 0, 0We either get a positive value for the column or we get 0.
But you said the value should be the default one and not 0.
Yes, but 0 is an invalid value for grid-column-start, so using 0 means the browser will ignore it and fall back to the default value!
Our new code is:
grid-column-start: if( style((--_i > 0) and (--_c: 0)): 2; /* first condition */ style(--_d: 0): max(0,var(--_n) - var(--_j)); /* second condition */ );And it works!
CodePen Embed FallbackYou can add as many items as you want, resize the screen, and everything will fit perfectly!
More ExamplesEnough code and math! Let’s enjoy more variations using different shapes. I’ll let you dissect the code as homework.
Rhombus grid CodePen Embed FallbackYou will notice a slightly different approach for setting the gap between the elements in the next three demos.
Octagon grid CodePen Embed Fallback Circle grid CodePen Embed FallbackAnd the other hexagon grid:
CodePen Embed Fallback ConclusionDo you remember when I told you that we were one property away from completing the grid? That one property (grid-column-start) took us literally the whole article to discuss! This demonstrates that CSS has evolved and requires a new mindset to work with. CSS is no longer a language where you simply set static values such color: red, margin: 10px, display: flex, etc.
Now we can define dynamic behaviors through complex calculations. It’s a whole process of thinking, finding formulas, defining variables, creating conditions, and so on. That’s not something new since I was able to do the same in 2021. However, we now have stronger features that allow us to have less hacky code and more flexible implementations.
Making a Responsive Pyramidal Grid With Modern CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Approximating contrast-color() With Other CSS Features
You have an element with a configurable background color, and you’d like to calculate whether the foreground text should be light or dark. Seems easy enough, especially knowing how mindful we ought to be with accessibility.
There have been a few drafts of a specification function for this functionality, most recently, contrast-color() (formerly color-contrast()) in the CSS Color Module Level 5 draft. But with Safari and Firefox being the only browsers that have implemented it so far, the final version of this functionality is likely still a ways off. There has been a lot of functionality added to CSS in the meantime; enough that I wanted to see whether we could implement it in a cross-browser friendly way today. Here’s what I have:
color: oklch(from <your color> round(1.21 - L) 0 0);Let me explain how I got here.
WCAG 2.2WCAG provides the formulas it uses for calculating the contrast between two RGB colors and Stacie Arellano has described in great detail. It’s based on older methods, calculating the luminance of colors (how perceptually bright they appear) and even tries to clamp for the limitations of monitors and screen flare:
L1 + 0.05 / L2 + 0.05…where the lighter color (L1) is on the top. Luminance ranges from 0 to 1, and this fraction is responsible for contrast ratios going from 1 (1.05/1.05) to 21 (1.05/.05).
The formulas for calculating the luminance of RGB colors are even messier, but I’m only trying to determine whether white or black will have higher contrast with a given color, and can get away with simplifying a little bit. We end up with something like this:
L = 0.1910(R/255+0.055)^2.4 + 0.6426(G/255+0.055)^2.4 + 0.0649(B/255+0.055)^2.4Which we can convert into CSS like this:
calc(.1910*pow(r/255 + .055,2.4)+.6426*pow(g/255 + .055,2.4)+.0649*pow(b/255 + .055,2.4))We can make this whole thing round to 1 or 0 using round(), 1 for white and 0 for black:
round(.67913 - .1910*pow(r/255 + .055, 2.4) - .6426*pow(g/255 + .055, 2.4) - .0649*pow(b/255 + .055, 2.4))Let’s multiply that by 255 and use it for all three channels with the relative color syntax. We end up with this:
color: rgb(from <your color> round(173.178 - 48.705*pow(r/255 + .055, 2.4) - 163.863*pow(g/255 + .055, 2.4) - 16.5495*pow(b/255 + .055, 2.4), 255) round(173.178 - 48.705*pow(r/255 + .055, 2.4) - 163.863*pow(g/255 + .055, 2.4) - 16.5495*pow(b/255 + .055, 2.4), 255) round(173.178 - 48.705*pow(r/255 + .055, 2.4) - 163.863*pow(g/255 + .055, 2.4) - 16.5495*pow(b/255 + .055, 2.4), 255) ); CodePen Embed FallbackA formula that, given a color, returns white or black based on WCAG 2. It’s not easy to read, but it works… except APCA is poised to replace it as a newer, better formula in future WCAG guidelines. We can do the math again, though APCA is an even more complicated formula. We could leverage CSS functions to clean it up a little, but ultimately this implementation is going to be inaccessible, hard to read, and difficult to maintain.
New ApproachI took a step back and thought about what else we have available. We do have another new feature we can try out: color spaces. The “L*” value in the CIELAB color space represents perceptual lightness. It is meant to reflect what our eyes can see. It’s not the same as luminance, but it’s close. Maybe we could guess whether to use black or white for better contrast based on perceptual lightness; let’s see if we can find a number where any color with lower lightness we use black, and higher lightness we use white.
You might instinctively think it should be 50% or .5, but it isn’t. A lot of colors, even when they’re bright, still contrast better with white than black. Here’s some examples using lch(), slowly increasing the lightness while keeping the hue the same:
CodePen Embed FallbackThe transition point where it’s easier to read the black text than white usually happens between 60-65. So, I put together a quick Node app using Colorjs.io to calculate where the cut off should be, using APCA for calculating contrast.
For oklch(), I found the threshold to be between .65 and .72, with an average of .69.
In other words:
- When the OKLCH lightness is .72 or above, black will always contrast better than white.
- Below .65, white will always contrast better than black.
- Between .65 and .72, typically both black and white have contrasts between 45-60.
So, just using round() and the upper bound of .72, we can make a new, shorter implementation:
color: oklch(from <your color> round(1.21 - L) 0 0); CodePen Embed FallbackIf you’re wondering where 1.21 came from, it’s so that .72 rounds down and .71 rounds up: 1.21 - .72 = .49 rounds down, and 1.21 - .71 = .5 rounds up.
This formula works pretty well, having put a couple iterations of this formula into production. It’s easier to read and maintain. That said, this formula more closely matches APCA than WCAG, so sometimes it disagrees with WCAG. For example, WCAG says black has a higher contrast (4.70 than white at 4.3) when placed on #407ac2, whereas APCA says the opposite: black has a contrast of 33.9, and white has a contrast of 75.7. The new CSS formula matches APCA and shows white:
Arguably, this formula may do a better job than WCAG 2.0 because it more closely matches APCA. That said, you’ll still need to check accessibility, and if you’re held legally to WCAG instead of APCA, then maybe this newer simpler formula is less helpful to you.
LCH vs. OKLCHI did run the numbers for both, and aside from OKLCH being designed to be a better replacement for LCH, I also found that the numbers support that OKLCH is a better choice.
With LCH, the gap between too dark for black and too light for white is often bigger, and the gap moves around more. For example, #e862e5 through #fd76f9 are too dark for black and too light for white. With LCH, that runs between lightness 63 through 70; for OKLCH, it’s .7 through .77. The scaling of OKLCH lightness just better matches APCA.
One Step FurtherWhile “most-contrast” will certainly be better, we can implement one more trick. Our current logic simply gives us white or black (which is what the color-contrast() function is currently limited to), but we can change this to give us white or another given color. So, for example, white or the base text color. Starting with this:
color: oklch(from <your color> round(1.21 - L) 0 0); /* becomes: */ --white-or-black: oklch(from <your color> round(1.21 - L) 0 0); color: rgb( from color-mix(in srgb, var(--white-or-black), <base color>) calc(2*r) calc(2*g) calc(2*b) ); CodePen Embed FallbackIt’s some clever math, but it isn’t pleasant to read:
- If --white-or-black is white, color-mix() results in rgb(127.5, 127.5, 127.5) or brighter; doubled we’re at rgb(255, 255, 255) or higher, which is just white.
- If --white-or-black is black, color-mix() cuts the value of each RGB channel by 50%; doubled we’re back to the original value of the <base color>.
Unfortunately, this formula doesn’t work in Safari 18 and below, so you need to target Chrome, Safari 18+ and Firefox. However, it does give us a way with pure CSS to switch between white and a base text color, instead of white and black alone, and we can fallback to white and black in Safari <18.
You can also rewrite these both using CSS Custom Functions, but those aren’t supported everywhere yet either:
@function --white-black(--color) { result: oklch(from var(--color) round(1.21 - l) 0 0); } @function --white-or-base(--color, --base) { result: rgb(from color-mix(in srgb, --white-black(var(--color)), var(--base)) calc(2*r) calc(2*g) calc(2*b)); } CodePen Embed Fallback ConclusionI hope this technique works well for you, and I’d like to reiterate that the point of this approach — looking for a threshold and a simple formula — is to make the implementation flexible and easy to adapt to your needs. You can easily adjust the threshold to whatever works best for you.
Approximating contrast-color() With Other CSS Features originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Trying to Make the Perfect Pie Chart in CSS
Speaking of charts… When was the last time you had to use a pie chart? If you are one of those people who have to give presentations right and left, then congratulations! You are both in my personal hell… and also surrounded by pie charts. Luckily, I think I haven’t needed to use them in ages, or at least that was until recently.
Last year, I volunteered to make ta webpage for a kids’ charity in México1. Everything was pretty standard, but the staff wanted some data displayed as pie charts on their landing page. They didn’t give us a lot of time, so I admit I took the easy route and used one of the many JavaScript libraries out there for making charts.
It looked good, but deep down I felt dirty; pulling in a whole library for a couple of simple pie charts. Feels like the easy way out rather than crafting a real solution.
I want to amend that. In this article, we’ll try making the perfect pie chart in CSS. That means avoiding as much JavaScript as possible while addressing major headaches that comes with handwriting pie charts. But first, let’s set some goals that our “perfect” should comply with.
In order of priority:
- This must be semantic! Meaning a screen reader should be able to understand the data shown in the pie chart.
- This should be HTML-customizable! Once the CSS is done, we only have to change the markup to customize the pie chart.
- This should keep JavaScript to a minimum! No problem with JavaScript in general, it’s just more fun this way.
Once we are done, we should get a pie chart like this one:
Is this too much to ask? Maybe, but we’ll try it anyways.
Conic gradients suck aren’t the bestWe can’t talk about pie charts without talking first about conic gradients. If you’ve read anything related to the conic-gradient() function, then you’ve likely seen that they can be used to create simple pie charts in CSS. Heck, even I have said so in the almanac entry. Why not? If only with one element and a single line of CSS…
.gradient { background: conic-gradient(blue 0% 12.5%, lightblue 12.5% 50%, navy 50% 100%); }We can have seemlessly perfect pie chart:
CodePen Embed FallbackHowever, this method blatantly breaks our first goal of semantic pie charts. As it’s later noted on the same entry:
Do not use the conic-gradient() function to create a real pie chart, or any other infographics for that matter. They don’t hold any semantic meaning and should only be used decoratively.
Remember that gradients are images, so displaying a gradient as a background-image doesn’t tell screen readers anything about the pie charts themselves; they only see an empty element.
This also breaks our second rule of making pie charts HTML-customizable, since for each pie chart we’d have to change its corresponding CSS.
So should we ditch conic-gradient() altogether? As much as I’d like to, its syntax is too good to pass so let’s at least try to up its shortcomings and see where that takes us.
Improving semanticsThe first and most dramatic problem with conic-gradient() is its semantics. We want a rich markup with all the data laid out so it can be understood by screen readers. I must admit I don’t know the best way to semantically write that, but after testing with NVDA, I believe this is a good enough markup for the task:
<figure> <figcaption>Candies sold last month</figcaption> <ul class="pie-chart"> <li data-percentage="35" data-color="#ff6666"><strong>Chocolates</strong></li> <li data-percentage="25" data-color="#4fff66"><strong>Gummies</strong></li> <li data-percentage="25" data-color="#66ffff"><strong>Hard Candy</strong></li> <li data-percentage="15" data-color="#b366ff"><strong>Bubble Gum</strong></li> </ul> </figure>Ideally, this is all we need for our pie chart, and once styles are done, just editing the data-* attributes or adding new <li> elements should update our pie chart.
Just one thing though: In its current state, the data-percentage attribute won’t be read out loud by screen readers, so we’ll have to append it to the end of each item as a pseudo-element. Just remember to add the “%” at the end so it also gets read:
.pie-chart li::after { content: attr(data-percentage) "%"; } CodePen Embed FallbackSo, is it accessible? It is, at least when testing in NVDA. Here it is in Windows:
You may have some questions regarding why I chose this or that. If you trust me, let’s keep going, but if not, here is my thought process:
Why use data-attributes instead of writing each percentage directly?We could easily write them inside each <li>, but using attributes we can get each percentage on CSS through the attr() function. And as we’ll see later it makes working with CSS a whole lot easier.
Why <figure>?The <figure> element can be used as a self-contained wrapper for our pie chart, and besides images, it’s used a lot for diagrams too. It comes in handy since we can give it a title inside <figcaption> and then write out the data on an unordered list, which I didn’t know was among the content permitted inside <figure> since <ul> is considered flow content.
Why not use ARIA attributes?We could have used an aria-description attribute so screen readers can read the corresponding percentage for each item, which is arguably the most important part. However, we may need to visually show the legend, too. That means there is no advantage to having percentages both semantically and visually since they might get read twice: (1) once on the aria-description and (2) again on the pseudo-element.
Making it a pie chartWe have our data on paper. Now it’s time to make it look like an actual pie chart. My first thought was, “This should be easy, with the markup done, we can now use a conic-gradient()!”
Well… I was very wrong, but not because of semantics, but how the CSS Cascade works.
Let’s peek again at the conic-gradient() syntax. If we have the following data:
- Item 1: 15%
- Item 2: 35%
- Item 3: 50%
…then we would write down the following conic-gradient():
.gradient { background: conic-gradient( blue 0% 15%, lightblue 15% 50%, navy 50% 100% ); }This basically says: “Paint the first color from 0 to 15%, the next color from 15% to 50% (so the difference is 35%), and so on.”
Do you see the issue? The pie chart is drawn in a single conic-gradient(), which equals a single element. You may not see it, but that’s terrible! If we want to show each item’s weight inside data-percentage — making everything prettier — then we would need a way to access all these percentages from the parent element. That’s impossible!
The only way we can get away with the simplicity of data-percentage is if each item draws its own slice. This doesn’t mean, however, that we can’t use conic-gradient(), but rather we’ll have to use more than one.
The plan is for each of these items to have their own conic-gradient() painting their slice and then place them all on top of each other:
To do this, we’ll first give each <li> some dimensions. Instead of hardcoding a size, we’ll define a --radius property that’ll come in handy later for keeping our styles maintainable when updating the HTML.
.pie-chart li { --radius: 20vmin; width: calc(var(--radius) * 2); /* radius twice = diameter */ aspect-ratio: 1; border-radius: 50%; }Then, we’ll get the data-percentage attribute into CSS using attr() and its new type syntax that allows us to parse attributes as something other than a string. Just beware that the new syntax is currently limited to Chromium as I’m writing this.
However, in CSS it is far better to work with decimals (like 0.1) instead of percentages (like 10%) because we can multiply them by other units. So we’ll parse the data-percentage attribute as a <number> and then divide it by 100 to get our percentage in decimal form.
.pie-chart li { /* ... */ --weighing: calc(attr(data-percentage type(<number>)) / 100); }We still need it as a percentage, which means multiplying that result by 1%.
.pie-chart li { /* ... */ --percentage: calc(attr(data-percentage type(<number>)) * 1%); }Lastly, we’ll get the data-color attribute from the HTML using attr() again, but with the <color> type this time instead of a <number>:
.pie-chart li { /* ... */ --bg-color: attr(data-color type(<color>)); }Let’s put the --weighing variable aside for now and use our other two variables to create the conic-gradient() slices. These should go from 0% to the desired percentage, and then become transparent afterwards:
.pie-chart li { /* ... */ background: conic-gradient( var(--bg-color) 0% var(--percentage), transparent var(--percentage) 100% ); }I am defining the starting 0% and ending 100% explicitly, but since those are the default values, we could technically remove them.
Here’s where we’re at:
CodePen Embed FallbackPerhaps an image will help if your browser lacks support for the new attr() syntax:
Now that all the slices are done, you’ll notice each of them starts from the top and goes in a clockwise direction. We need to position these, you know, in a pie shape, so our next step is to rotate them appropriately to form a circle.
This is when we hit a problem: the amount each slice rotates depends on the number of items that precede it. We’ll have to rotate an item by whatever size the slice before it is. It would be ideal to have an accumulator variable (like --accum) that holds the sum of the percentages before each item. However, due to the way the CSS Cascade works, we can neither share state between siblings nor update the variable on each sibling.
And believe me, I tried really hard to work around these issues. But it seems we are forced into two options:
- Hardcode the --accum variable on each <li> element.
- Use JavaScript to calculate the --accum variable.
The choice isn’t that hard if we revisit our goals: hardcoding --accum would negate flexible HTML since moving an item or changing percentages would force us to manually calculate the --accum variable again.
JavaScript, however, makes this a trivial effort:
const pieChartItems = document.querySelectorAll(".pie-chart li"); let accum = 0; pieChartItems.forEach((item) =>; { item.style.setProperty("--accum", accum); accum += parseFloat(item.getAttribute("data-percentage")); });With --accum out of the way, we can rotate each conic-gradient() using the from syntax, that tells the conic gradient the rotation’s starting point. The thing is that it only takes an angle, not a percentage. (I feel like a percentage should also work fine, but that’s a topic for another time).
To work around this, we’ll have to create yet another variable — let’s call it --offset — that is equal to --accum converted to an angle. That way, we can plug the value into each conic-gradient():
.pie-chart li { /* ... */ --offset: calc(360deg * var(--accum) / 100); background: conic-gradient( from var(--offset), var(--bg-color) 0% var(--percentage), transparent var(--percentage) 100% ); }We’re looking a lot better!
CodePen Embed FallbackWhat’s left is to place all items on top of each other. There are plenty of ways to do this, of course, though the easiest might be CSS Grid.
.pie-chart { display: grid; place-items: center; } .pie-chart li { /* ... */ grid-row: 1; grid-column: 1; }This little bit of CSS arranges all of the slices in the dead center of the .pie-chart container, where each slice covers the container’s only row and column. They slices won’t collide because they’re properly rotated!
CodePen Embed FallbackExcept for those overlapping labels, we’re in really, really good shape! Let’s clean that stuff up.
Positioning labelsRight now, the name and percentage labels inside the <figcaption> are splattered on top of one another. We want them floating next to their respective slices. To fix this, let’s start by moving all those items to the center of the .pie-chart container using the same grid-centering trick we we applied on the container itself:
.pie-chart li { /* ... */ display: grid; place-items: center; } .pie-chart li::after, strong { grid-row: 1; grid-column: 1; }Luckily, I’ve already explored how to lay things out in a circle using the newer CSS cos() and sin(). Give those links a read because there’s a lot of context in there. In short, given an angle and a radius, we can use cos() and sin() to get the X and Y coordinates for each item around a circle.
For that, we’ll need — you guessed it! — another CSS variable representing the angle (we’ll call it --theta) where we’ll place each label. We can calculate that angle this next formula:
.pie-chart li { /* ... */ --theta: calc((360deg * var(--weighing)) / 2 + var(--offset) - 90deg); }It’s worth knowing what that formula is doing:
- 360deg * var(--weighing)) / 2: Gets the percentage as an angle then divides it by two to find the middle point.
- + var(--offset): Moves the angle to match the current offset.
- - 90deg. cos() and sin(): The angles are measured from the right, but conic-gradient() starts from the top. This part corrects each angle by -90deg.
We can find the X and Y coordinates using the --theta and --radius variables, like the following pseudo code:
x = cos(theta) * radius y = sin(theta) * radiusWhich translates to…
.pie-chart li { /* ... */ --pos-x: calc(cos(var(--theta)) * var(--radius)); --pos-y: calc(sin(var(--theta)) * var(--radius)); }This places each item on the pie chart’s edge, so we’ll add in a --gap between them:
.pie-chart li { /* ... */ --gap: 4rem; --pos-x: calc(cos(var(--theta)) * (var(--radius) + var(--gap))); --pos-y: calc(sin(var(--theta)) * (var(--radius) + var(--gap))); }And we’ll translate each label by --pos-x and --pos-y:
.pie-chart li::after, strong { /* ... */ transform: translateX(var(--pos-x)) translateY(var(--pos-y)); }Oh wait, just one more minor detail. The label and percentage for each item are still stacked on top of each other. Luckily, fixing it is as easy as translating the percentage a little more on the Y-axis:
.pie-chart li::after { --pos-y: calc(sin(var(--theta)) * (var(--radius) + var(--gap)) + 1lh); }Now we’re cooking with gas!
CodePen Embed FallbackLet’s make sure this is screenreader-friendly:
That’s about it… for now…I’d call this a really good start toward a “perfect” pie chart, but there are still several things we could improve:
- The pie chart assumes you’ll write the percentages yourself, but there should be a way to input the raw number of items and then calculate their percentages.
- The data-color attribute is fine, but if it isn’t provided, we should still provide a way to let CSS generate the colors. Perhaps a good job for color-mix()?
- What about different types of charts? Bar charts, anyone?
- This is sorta screaming for a nice hover effect, like maybe scaling a slice and revealing it?
That’s all I could come up with for now, but I’m already planning to chip away at those at follow up with another piece (get it?!). Also, nothing is perfect without lots of feedback, so let me know what you would change or add to this pie chart so it can be truly perfect!
1 They are great people helping kids through extremely difficult times, so if you are interested in donating, you can find more on their socials. ↪️
Trying to Make the Perfect Pie Chart in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSS Bar Charts Using Modern Functions
New CSS features can sometimes make it easier and more efficient to code designs we already knew how to create. This efficiency could stem from reduced code or hacks, or improved readability due to the new features.
In that spirit, let’s revamp what’s under the hood of a bar chart.
<ul class="chart" tabindex="0" role="list" aria-labelledby="chart-title"> <li class="chart-bar" data-value="32" tabindex="0" role="img" aria-label="32 percentage">32%</li> <!-- etc. --> </ul>We begin by laying out a grid.
.chart { display: grid; grid-template-rows: repeat(100, 1fr); /* etc. */ }The chart metric is based on percentage, as in “some number out of 100.” Let’s say we’re working with a grid containing 100 rows. That ought to stress test it, right?
Next, we add the bars to the grid with the grid-column and grid-row properties:
.chart-bar { grid-column: sibling-index(); grid-row: span attr(data-value number); /* etc. */ }Right off the bat, I want to note a couple of things. First is that sibling-index() function. It’s brand new and has incomplete browser support as of this writing (come on, Firefox!), though it’s currently supported in the latest Chrome and Safari (but not on iOS apparently). Second is that attr() function. We’ve had it for a while, but it was recently upgraded and now accepts data-attributes. So when we have one of those in our markup — like data-value="32" — that’s something the function can read.
With those in place, that’s really all we need to create a pretty darn nice bar chart in vanilla CSS! The following demo has fallbacks in place so that you can still see the final result in case your browser hasn’t adopted those new features:
CodePen Embed FallbackYes, that was easy to do, but it’s best to know exactly why it works. So, let’s break that down.
Automatically Establishing Grid ColumnsDeclaring the sibling-index() function on the grid-column property explicitly places the list items in consecutive columns. I say “explicit” because we’re telling the grid exactly where to place each item by its data-value attribute in the markup. It goes first <li> in first column, second <li> in second column, and so forth.
That’s the power of sibling-index() — the grid intelligently generates the order for us without having to do it manually through CSS variables.
/* First bar: sibling-index() = 1 */ grid-column: sibling-index(); /* ...results in: */ grid-column: 1; grid-column-start: 1; grid-column-end: auto; /* Second bar: sibling-index() = 2 */ grid-column: sibling-index(); /* ...results in: */ grid-column: 2; grid-column-start: 2; grid-column-end: auto; /* etc. */ Automatically Establishing Grid RowsIt’s pretty much the same thing! But in this case, each bar occupies a certain number of rows based on the percentage it represents. The grid gets those values from the data-value attribute in the markup, effectively telling the grid how tall each bar in the chart should be.
/* First bar: data-value="32" */ grid-row: span attr(data-value number); /* ...results in: */ grid-row: span 32 /* Second bar: data-value="46" */ grid-row: span attr(data-value number); /* ...results in: */ grid-row: span 46The attr() function, when provided with a data type parameter (the parameter value number in our case), casts the value retrieved by attr() into that specific type. In our example, the attr() function returns the value of data-value as a <number> type, which is then used to determine the number of rows to span for each bar.
Let’s Make Different Charts!Since we have the nuts and bolts down on this approach, I figured I’d push things a bit and demonstrate how we can apply the same techniques for all kinds of CSS-only charts.
For example, we can use grid-row values to adjust the vertical direction of the bars:
CodePen Embed FallbackOr we can skip bars altogether and use markers instead:
CodePen Embed Fallback CodePen Embed FallbackWe can also swap the columns and rows for horizontal bar charts:
CodePen Embed Fallback Wrapping upPretty exciting, right? Just look at all the ways we used to pull this stuff off before the days of sibling-index() and an upgraded attr():
- Making Charts with CSS (Robin Rendle, 2015)
- Making A Bar Chart with CSS Grid (Robin Rendle, 2017)
- More CSS Charts, with Grid & Custom Properties (Miriam Suzanne, 2017)
- Overlapping Bar Charts (Saleh Mubasher, 2022)
CSS Bar Charts Using Modern Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.