Developer News

Subsetting Font Awesome to Improve Performance

Css Tricks - Thu, 02/17/2022 - 5:21am

Font Awesome is an incredibly popular icon library. Unfortunately, it’s somewhat easy to use in a way that results in less-than-ideal performance. By subsetting Font Awesome, we can remove any unused glyphs from the font files it provides. This will reduce the number of bytes transmitted over the wire, and improve performance.

Let’s subset fonts together in a Font Awesome project to see the difference it makes. As we go, I’ll assume you’re importing the CSS file Font Awesome provides, and using its web fonts to display icons.

Let’s set things up

For the sake of demonstration, I have nothing but an HTML file that imports Font Awesome’s base CSS file. To get a reasonable sample of icons, I’ve listed out each one that I use on one of my side projects.

Here’s what our HTML file looks like in the browser before subsetting fonts:

Here’s a look at DevTool’s Network tab to see what’s coming down.

Now let’s see how many bytes our font files take to render all that.

Here’s our base case

We want to see what the most straightforward, least performant use of Font Awesome looks like. In other words, we want the slowest possible implementation with no optimization. I’m importing the all.min.css file Font Awesome provides.

As we saw above, the gzipped file weighs in at 33.4KB, which isn’t bad at all. Unfortunately, when we peek into DevTool’s Font tab, things get a little worse.

Yikes. 757KB just for font files. For 54 icons.

While font files are not as expensive a resource for your browser to handle as JavaScript, those are still bytes your browser needs to pull down, just for some little icons. Consider that some of your users might be browsing your site on mobile, away from a strong or fast internet connection.

First attempt using PurifyCSS

Font Awesome’s main stylesheet contains definitions for literally thousands of icons. But what if we only need a few dozen at most? Surely we could trim out the unneeded stuff?

There are many tools out there that will analyze your code, and remove unused styles from a stylesheet. I happen to be using PurifyCSS. While this library isn’t actively maintained anymore, the idea is the same, and in the end, this isn’t the solution we’re looking for. But let’s see what happens when we trim our CSS down to only what’s needed, which we can do with this script:

const purify = require("purify-css"); const content = ["./dist/**/*.js"]; // Vite-built content purify(content, ["./css/fontawesome/css/all.css"], { minify: true, output: "./css/fontawesome/css/font-awesome-minimal-build.css" });

And when we load that newly built CSS file, our CSS bytes over the wire drop quite a bit, from 33KB to just 7.1KB!

But unfortunately, our other Font Awesome font files are unchanged.

What happened? PurifyCSS did its job. It indeed removed the CSS rules for all the unused icons. Unfortunately, it wasn’t capable of reaching into the actual font files to trim down the glyphs, in addition to the CSS rules.

If only there was a tool like PurifyCSS that handles font files…

Subsetters to the rescue!

There are, of course, tools that are capable of removing unused content from font files, and they’re called subsetters. A subsetter analyzes your webpage, looks at your font files, and trims out the unused characters. There are a bunch of tools for subsetting fonts out there, like Zach Leatherman’s Glyphhanger. As it turns out, subsetting Font Awesome is pretty straightforward because it ships its own built-in subsetters. Let’s take a look.

Subsetting fonts automatically

The auto subsetting and manual subsetting tools I’m about to show you require a paid Font Awesome Pro subscription.

Font Awesome allows you to set up what it calls kits, which are described in the Font Awesome docs as a “knapsack that carries all the icons and awesomeness you need in a neat little lightweight bundle you can sling on the back of your project with ease.” So, rather than importing any and every CSS file, a kit gives you a single script tag you can add to your HTML file’s <head>, and from there, the kit only sends down the font glyphs you actually need from the font file.

Creating a kit takes about a minute. You’re handed script tag that looks something like this:

<script src="" crossorigin="anonymous"></script>

When the script loads, we now have no CSS files at all, and the JavaScript file is a mere 4KB. Let’s look again at the DevTools Fonts tab to see which font files are loaded now that we’ve done some subsetting.

We’ve gone from 757KB down to 331KB. That’s a more than 50% reduction. But we can still do better than that, especially if all we’re rendering is 54 icons. That’s where Font Awesome’s manual font subsetter comes into play.

Subsetting fonts manually

Wouldn’t it be nice if Font Awesome gave us a tool to literally pick the exact icons we wanted, and then provide a custom build for that? Well, they do. They don’t advertise this too loudly for some reason, but they actually have a desktop application exactly for subsetting fonts manually. The app is available to download from their site — but, like the automatic subsetter, this app requires a paid Font Awesome subscription to actually use.

Search the icons, choose the family, add what you want, and then click the big blue Build button. That’s really all it takes to generate a custom subset of Font Awesome icons.

Once you hit the button, Font Awesome will ask where it should save your custom build, then it dumps a ZIP file that contains everything you need. In fact, the structure you’ll get is exactly the same as the normal Font Awesome download, which makes things especially simple. And naturally, it lets you save the custom build as a project file so you can open it back up later to add or remove icons as needed.

We’ll open up DevTools to see the final size of the icons we’re loading, but first, let’s look at the actual font files themselves. The custom build creates many different types, depending on what your browser uses. Let’s focus on the .woff2 files, which is what Chrome loads. The same light, regular, duotone, solid, and brand files that were there before are still in place, except this time no file is larger than 5KB… and that’s before they’re gzipped!

And what about the CSS file? It slims down to just 8KB. With gzip, it’s only 2KB!

Here’s the final tally in DevTools:

Before we go, take a quick peek at those font filenames. The fa-light-300.woff2 font file is still there, but the others look different. That’s because I’m using Vite here, and it decided to automatically inline the font files into the CSS, since they’re so tiny.

That’s why our CSS file looks a little bigger in the DevTools Network tab than the 2KB we saw before on disk. The tradeoff is that most of those font “files” from above aren’t files at all, but rather Base64-encoded strings embedded right in this CSS file, saving us additional network requests.

All that said, Vite is inlining many different font formats that the browser will never use. But overall it’s a pretty small number of bytes, especially compared to what we were seeing before.

Before leaving, if you’re wondering whether that desktop font subsetting GUI tool comes in a CLI that can integrate with CI/CD to generate these files at build time, the answer is… not yet. I emailed the Font Awesome folks, and they said something is planned. That’ll allow users to streamline their build process if and when it ships.

As you’ve seen, using something like Font Awesome for icons is super cool. But the default usage might not always be the best approach for your project. To get the smallest file size possible, subsetting fonts is something we can do to trim what we don’t need, and only serve what we do. That’s the kind of performance we want, especially when it comes to loading fonts, which have traditionally been tough to wrangle.

Subsetting Font Awesome to Improve Performance originally published on CSS-Tricks. You should get the newsletter.

Top Things You Didn’t Know You Could Do With Netlify CLI

Css Tricks - Thu, 02/17/2022 - 3:00am

(This is a sponsored post.)

First things first, if you didn’t know Netlify had a CLI, they do.  One of my favorite things about it running the command netlify dev on nearly any static-site generator project is seeing it detect what it should be doing and spinning the site up in a dev server for you. But not just any dev server, a dev server that replicates the Netlify environment, meaning things like running your serverless functions and making your environment variables available.

Here are five more things you can do with it that you might not realize.

1) Create a new site from a template

That’s right, spin up a new site by typing a single command and walking through the steps. Try it:

netlify sites:create-template

There is a shorthand to the CLI as well! Try the above as ntl sites:create-template

As Charlie Gerard writes in a blog post about this:

At the moment, our templates include a Gatsby and Hugo starter with the Netlify CMS, as well as a Next.js starter. 

2) Manage your environment variables

The netlify env command, now in Beta, allows you to control environment variables. You can list them out with netlify env:list, get and set (and unset) them. My favorite: move a whole set of them from one site to another like netlify env:migrate --to <to-site-id>.

3) Test serverless functions

By virtue of spinning up your site locally with the Netlify CLI, your serverless functions will run. You can test that they are working and inspect the network traffic and such that way. But the CLI can help you as well, the netlify functions command is capable of testing functions at the command line level. For example, netlify functions:invoke can trigger a function with simulated data.

4) Live stream your Dev environment

Here’s Melanie Crissey on the Netlify Blog about this:

While Netlify’s collaborative Deploy Previews are our go-to for asynchronous feedback, sometimes you need to drop everything and pair on an issue together. That’s when Netlify Live really shines.

For example, just last week, our team was working quickly to debug some funky edge case issues with authentication for the Your Year on Netlify project. Zach Leatherman, who was working on the fix, spun up a local version of the app with Netlify Live. Within minutes, he was able to see the logs, identify the issue, and make a few changes. Meanwhile, I was able to test out the fix before it was ever deployed—without pulling down a copy of his latest version from a repo. Netlify CLI to the rescue and problem solved!

Remember how I mentioned you spin up a dev environment locally with netlify dev? The trick here is to do netlify dev --live. So rather than a localhost URL that only you would be able to see, you’ll get a special URL that the world can see.

5) Run netlify switch to switch between different Netlify accounts, like from your personal side project to a work project

You literally auth with the CLI (netlify login, imagine that), so that you can act on behalf of your own Netlify account. Deploy sites and whatnot. But it’s perfectly reasonable that you have multiple Netlify accounts (like work and personal). Running netlify switch makes it trivial to move between accounts.


This video is 50 seconds long and shows how you can go from having some static files locally to a deployed with the CLI:

Top Things You Didn’t Know You Could Do With Netlify CLI originally published on CSS-Tricks. You should get the newsletter.

Add-to-Calendar Button UI Widget

Css Tricks - Wed, 02/16/2022 - 3:14pm

A useful little UI widget thingy here from Jens Kuerschner. Click the add-to-calendar button, get a list of calendar apps, the user selects which one they actually use, and they get what they need for that calendar. Could be a specialized URL they get sent to, or even an .ics file that gets downloaded.

It’s pretty easy to use. Here’s me using the library off of CDNs for both the JavaScript and CSS:

CodePen Embed Fallback

Let’s do a thought dump!

The configuration as “a big chunk of JSON sitting in the HTML as a string” is a little weird to me.

I see the hack where it uses display: none; on the parent to hide that text from rendering, but I think I like the setup where that’s put into a <script type="application/ld+json"> tag much better.

The fallback for these, assuming JavaScript doesn’t load or execute correctly, is nothing.

I’m torn there. Maybe it’s fine? This seems like bonus functionality anyway. And it’s presumably sitting next to actual content about the event that a user could add to their calendar however they want. I certainly wouldn’t want to see non-interactive text saying “Add to Calendar” because that’s worse than nothing. But maybe there could be some kind of generically useful hyperlink that can act as the fallback?

An add-to-calendar button seems like a good use case for a web component.

Why not an <add-to-calendar> element? That way, the script and styles could be isolated and probably a bit safer for general usage. But how do you do JSON config for a web component? Maybe every single property becomes an attribute? Maybe something like: <add-to-calendar options="Apple, Google", startTime="10:15" />

The biggest problem to address up front, though, is that it looks like the interactive element is a <div> with all JavaScript handlers.

You can’t Tab to it at all, so there is no way to activate it. There are no CSS states — it’s all classes updated by JavaScript. I’d definitely get this thing updated to be a <button>. And maybe it’s good timing to make use of a <dialog> element for the options and use dialog::backdrop for that fancy backdrop-filter background.

Just some constructive criticism, Jens — keep on keepin’ on.

To Shared LinkPermalink on CSS-Tricks

Add-to-Calendar Button UI Widget originally published on CSS-Tricks. You should get the newsletter.

An Auto-Filling CSS Grid With Max Columns of a Minimum Size

Css Tricks - Wed, 02/16/2022 - 5:06am

Within Drupal 10 core, we’re implementing a new auto-filling CSS Grid technique that I think is cool enough to share with the world.

The requirements are:

  • The user specifies a maximum number of columns. This is the auto-filling grid’s “natural” state.
  • If a grid cell goes under a user-specified width, the auto-filling grid will readjust itself and decrease the number of columns.
  • The grid cells should always stretch to fit the auto-filling grid container’s width, no matter the column count.
  • All of this should work independent of viewport width and should not require JavaScript.
The auto-filling CSS Grid in action

Here’s how the resulting auto-filling CSS grid behaves when it is compressed by the draggable div element to its left.

Here’s the code

If you’re not looking for the theory behind the auto-filling grid, and just want to copy/paste code, here you go!

.grid-container { /** * User input values. */ --grid-layout-gap: 10px; --grid-column-count: 4; --grid-item--min-width: 100px; /** * Calculated values. */ --gap-count: calc(var(--grid-column-count) - 1); --total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap)); --grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count)); display: grid; grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item--min-width), var(--grid-item--max-width)), 1fr)); grid-gap: var(--grid-layout-gap); } CodePen Embed Fallback Theory and tools behind the auto-filling CSS Grid

The code above uses several modern CSS tools including CSS Grid’s repeat(), auto-fill(), and minmax() functions, as well as the CSS max(), and calc() functions. Here’s how it works.

CSS Grid’s auto-fill() function

The key to all of this is auto-fill(). We need each row to fill up with as many columns as possible. For more info on auto-fill, check out Sara Soueidan’s awesome article on the difference between auto-fill and auto-fit, which includes this helpful video showing how it works.

But how to we make sure that it doesn’t fill in too many columns?

The CSS max() function

That’s where the max() function comes in! We want each grid cell’s width to max out at a certain percentage, say 25% for a four-column grid. But, we can’t have it go below the user-specified minimum width.

So, assuming a four-column grid and minimum cell width of 100px, the max() function would look something like: max(25%, 100px).

However, the 25% value is still not quite correct because it doesn’t take the grid gaps into account. What we really need is something like this instead:

max(calc(25% - <grid-gap-for-one-cell>), 100px)

We can calc()-ulate this in CSS! (Who says CSS isn’t programming?)

--gap-count: calc(var(--grid-column-count) - 1); --total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap)); --grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count));

Now we have another key to making this work! This will tell the grid cell to go to its maximum width — which takes into account the user-specified columns) — but will never go under 100px.

max(100px, var(--grid-item--max-width))

Learn more about the max() function with Chris Coyier’s article on the CSS min(),max(), and clamp() functions.

CSS Grid’s minmax() function

We’re getting close, but there’s one key ingredient that’s missing: The grid doesn’t always stretch to its parent’s container’s width.

This is exactly what the minmax() function is designed to do. The following CSS will set the minimum width to the <grid-item-width>, and if it has room, it’ll stretch all the cells out equally to fit the parent’s width!

minmax(<grid-item-width>, 1fr) Let’s put it all together and make some magic!

Using the tools above, we can put together this magic bit of code that does exactly what we want!

--gap-count: calc(var(--grid-column-count) - 1); --total-gap-width: calc(var(--gap-count) * var(--grid-layout-gap)); --grid-item--max-width: calc((100% - var(--total-gap-width)) / var(--grid-column-count)); grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item--min-width), var(--grid-item--max-width)), 1fr)); CSS is fun!

CSS has really come a long way. I had a lot of fun working on this, and I’m so happy that use-cases like this are now possible without the use of JavaScript.

Special thanks to Andy Blum, who suggested auto-fill() over auto-fit(). Also, an extremely special thanks to all of the implementors and spec writers who make advanced functions like this standardized and possible.

An Auto-Filling CSS Grid With Max Columns of a Minimum Size originally published on CSS-Tricks. You should get the newsletter.

GSAP Flip Plugin for Animation

Css Tricks - Tue, 02/15/2022 - 11:41am

Greensock made the GSAP Flip plugin free in the 3.9 release. FLIP is an animation concept that helps make super performance state-change animations. Ryan Mulligan has a good blog post:

FLIP, coined by Paul Lewis, is an acronym for First, Last, Invert, and Play. The Flip plugin harnesses this technique so that web developers can effortlessly and smoothly transition elements between states.

Examples using the GSAP Flip plugin

Taking advantage of FLIP “by hand” is certainly possible, but tricky. It’s an absolutely perfect thing for an animation library to do for us. Greenstock nailed it, as Ryan says:

1. Get the current state
2. Make your state changes
3. Call Flip.from(state, options)

Deliciously simple. Ryan made an “add to cart” effect with it:

CodePen Embed Fallback

I used it just the other day to make a “mini photo gallery” that could rotate which image was the big one on top:

CodePen Embed Fallback

Which, coincidently, is exactly why I ended up blogging “How to Cycle Through Classes on an HTML Element” the other day.

To Shared LinkPermalink on CSS-Tricks

GSAP Flip Plugin for Animation originally published on CSS-Tricks. You should get the newsletter.

6 Creative Ideas for CSS Link Hover Effects

Css Tricks - Tue, 02/15/2022 - 5:37am

Creating CSS link hover effects can add a bit of flair to an otherwise bland webpage. If you’ve ever found yourself stumped trying to make a slick hover effect, then I have six CSS effects for you to take and use for your next project.

Let’s get right to it!

I know we’re talking about :hover and all, but it can sometimes (but maybe not always) be a good idea lump :focus in as well, as not all interactions are directly from a mouse, but perhaps a tap or keystroke.

The Sliding Highlight Link Hover Effect

This effect applies a box shadow to the inline link, altering the color of the link text in the process. We start with padding all around the link, then add a negative margin of the same value to prevent the padding from disrupting the text flow.

We will use box-shadow instead of the background property since it allows us to transition.

a { box-shadow: inset 0 0 0 0 #54b3d6; color: #54b3d6; margin: 0 -.25rem; padding: 0 .25rem; transition: color .3s ease-in-out, box-shadow .3s ease-in-out; } a:hover { box-shadow: inset 100px 0 0 0 #54b3d6; color: white; } CodePen Embed Fallback The Text Swappin’ Link Hover Effect

Here’s a fun one where we swap the text of the link with some other text on hover. Hover over the text and the linked text slides out as new text slides in.

Easier to show than tell.

There’s quite a bit of trickery happening in this link hover effect. But the magic sauce is using a data-attribute to define the text that slides in and call it with the content property of the link’s ::after pseudo-element.

First off, the HTML markup:

<p>Hover <a href="#" data-replace="get a link"><span>get a link</span></a></p>

That’s a lot of inline markup, but you’re looking at a paragraph tag that contains a link and a span.

Let’s give link some base styles. We need to give it relative positioning to hold the pseudo-elements — which will be absolutely positioned — in place, make sure it’s displayed as inline-block to get box element styling affordances, and hide any overflow the pseudo-elements might cause.

a { overflow: hidden; position: relative; display: inline-block; }

The ::before and ::after pseudo-elements should have some absolute positioning so they stack with the actual link. We’ll make sure they are set to the link’s full width with a zero offset in the left position, setting them up for some sliding action.

a::before, a::after { content: ''; position: absolute; width: 100%; left: 0; }

The ::after pseudo-element gets the content from the link’s data-attribute that’s in the HTML markup:

a::after { content: attr(data-replace); }

Now we can transform: translate3d() the ::after pseudo-element element to the right by 200%. We move it back into position on :hover. While we’re at it, we can give this a zero offset n the top direction. This’ll be important later when we use the ::before pseudo-element like an underline below the text.

a::after { content: attr(data-replace); top: 0; transform-origin: 100% 50%; transform: translate3d(200%, 0, 0); } a:hover::after, a:focus::after { transform: translate3d(0, 0, 0); }

We’re also going to transform: scale() the ::before pseudo-element so it’s hidden by default, then scale it back up on :hover. We’ll make it small, like 2px in height, and pin it to the bottom so it looks like an underline on the text that swaps in with ::after.

a::before { background-color: #54b3d6; height: 2px; bottom: 0; transform-origin: 100% 50%; transform: scaleX(0); } a:hover::before, a:focus::before { transform-origin: 0% 50%; transform: scaleX(1); }

The rest is all preference! We drop in a transition on the transform effects, some colors, and whatnot to get the full effect. Those values are totally up to you.

CodePen Embed Fallback View full CSS a { overflow: hidden; position: relative; display: inline-block; } a::before, a::after { content: ''; position: absolute; width: 100%; left: 0; } a::before { background-color: #54b3d6; height: 2px; bottom: 0; transform-origin: 100% 50%; transform: scaleX(0); transition: transform .3s cubic-bezier(0.76, 0, 0.24, 1); } a::after { content: attr(data-replace); height: 100%; top: 0; transform-origin: 100% 50%; transform: translate3d(200%, 0, 0); transition: transform .3s cubic-bezier(0.76, 0, 0.24, 1); color: #54b3d6; } a:hover::before { transform-origin: 0% 50%; transform: scaleX(1); } a:hover::after { transform: translate3d(0, 0, 0); } a span { display: inline-block; transition: transform .3s cubic-bezier(0.76, 0, 0.24, 1); } a:hover span { transform: translate3d(-200%, 0, 0); } The Growing Background Link Hover Effect

This is a pretty popular effect I’ve seen used in quite a few places. The idea is that you use the link’s ::before pseudo-element as a thick underline that sits slightly behind the actual text of the link. Then, on hover, the pseudo-element expands to cover the whole thing.

OK, some base styles for the link. We want no text-decoration since ::before will act like one, then some relative positioning to hold ::before in place when we give that absolute positioning.

a { text-decoration: none; position: relative; }

Now let’s set up ::before by making it something like 8px tall so it looks like a thick underline. We’ll also give it absolute positioning so we have control to make it the full width of the actual link while offsetting it so it’s at the left and is just a smidge off the bottom so it looks like it’s subtly highlighting the link. May as well give it z-index: -1 so we’re assured it sits behind the link.

a::before { content: ''; background-color: hsla(196, 61%, 58%, .75); position: absolute; left: 0; bottom: 3px; width: 100%; height: 8px; z-index: -1; }

Nice, nice. Let’s make it appear as though ::before is growing when the link is hovered. All we need is to change the height from 3px to 100%. Notice that I’m also dropping the bottom offset back to zero so the background covers more space when it grows.

a:hover::before { bottom: 0; height: 100%; }

Now for slight transition on those changes:

a::before { content: ''; background-color: hsla(196, 61%, 58%, .75); position: absolute; left: 0; bottom: 3px; width: 100%; height: 8px; z-index: -1; transition: all .3s ease-in-out; } CodePen Embed Fallback View full CSS a { text-decoration: none; color: #18272F; font-weight: 700; position: relative; } a::before { content: ''; background-color: hsla(196, 61%, 58%, .75); position: absolute; left: 0; bottom: 3px; width: 100%; height: 8px; z-index: -1; transition: all .3s ease-in-out; } a:hover::before { bottom: 0; height: 100%; } The Right-to-Left Color Swap Link Hover Effect

I personally like using this effect for links in a navigation. The link starts in one color without an underline. Then, on hover, a new color slides in from the right while an underline slides in from the left.

Neat, right? There’s a lot of motion happening in there, so you might consider the accessibility implications and wrap it all in a prefers-reduced-motion query to replace it with something more subtle for those with motion sensitivities.

Here’s how it works. We give the link a linear background gradient with a hard stop between two colors at the halfway mark.

a { background-image: linear-gradient( to right, #54b3d6, #54b3d6 50%, #000 50% ); }

We make the background double the link’s width, or 200%, and position it all the way over to the left. That way, it’s like only one of the gradients two colors is showing.

a { background-image: linear-gradient( to right, #54b3d6, #54b3d6 50%, #000 50% ); background-size: 200% 100%; background-position: -100%; }

The magic happens when we reach for a couple of non-standard -webkit-prefixed properties. One strips the color out of the text to make it transparent. The other clips the background gradient to the text so it appears the text is actually the color of the background.

a { background-image: linear-gradient( to right, #54b3d6, #54b3d6 50%, #000 50% ); background-size: 200% 100%; background-position: -100%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }

Still with me? Now let’s make the link’s faux underline by putting ::before to use. We’ll give it the same color we gave the on the hidden portion of the link’s background gradient and position it under the actual link so it looks like a proper text-decoration: underline.

a:before { content: ''; background: #54b3d6; display: block; position: absolute; bottom: -3px; left: 0; width: 0; height: 3px; }

On hover, we slide ::before into place, coming in from the left:

a:hover { background-position: 0; }

Now, this is a little tricky. On hover, we make the link’s ::before pseudo-element 100% of the link’s width. If we were to apply this directly to the link’s hover, we’d make the link itself full-width, which moves it around the screen. Yikes!

a:hover::before { width: 100%; }

Add a little transition to smooth things out:

a { background-image: linear-gradient( to right, #54b3d6, #54b3d6 50%, #000 50% ); background-size: 200% 100%; background-position: -100%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; transition: all 0.3s ease-in-out; } CodePen Embed Fallback View full CSS a { background-image: linear-gradient( to right, #54b3d6, #54b3d6 50%, #000 50% ); background-size: 200% 100%; background-position: -100%; display: inline-block; padding: 5px 0; position: relative; -webkit-background-clip: text; -webkit-text-fill-color: transparent; transition: all 0.3s ease-in-out; } a:before { content: ''; background: #54b3d6; display: block; position: absolute; bottom: -3px; left: 0; width: 0; height: 3px; transition: all 0.3s ease-in-out; } a:hover { background-position: 0; } a:hover::before { width:100%; } The Rainbow Underline Link Hover Effect

We can’t do text-decoration-color: rainbow, but we can fake it with a little background magic mixed with linear gradients.

First, we remove the link’s text-decoration:

a { text-decoration: none; }

Now for those gradients. We chain two linear gradients together on the same background property. One gradient is the initial color before hover. The second is the rainbow on hover.

a { background: linear-gradient( to right, rgba(100, 200, 200, 1), rgba(100, 200, 200, 1) ), linear-gradient( to right, rgba(255, 0, 0, 1), rgba(255, 0, 180, 1), rgba(0, 100, 200, 1) ); }

Let’s make the background size a mere 3px tall so it looks like, you know, an underline. We can size both gradients together on the background-size property so that the initial gradient is full width and 3px tall, and the rainbow is zero width.

a { background: linear-gradient( to right, rgba(100, 200, 200, 1), rgba(100, 200, 200, 1) ), linear-gradient( to right, rgba(255, 0, 0, 1), rgba(255, 0, 180, 1), rgba(0, 100, 200, 1) ); background-size: 100% 3px, 0 3px; }

Now we can position the background gradients — at the same time on the background-position property — so that the first gradient is fully in view and the rainbow is pushed out of view. Oh, and let’s make sure the background isn’t repeating while we’re at it.

a { background: linear-gradient( to right, rgba(100, 200, 200, 1), rgba(100, 200, 200, 1) ), linear-gradient( to right, rgba(255, 0, 0, 1), rgba(255, 0, 180, 1), rgba(0, 100, 200, 1) ); background-size: 100% 3px, 0 3px; background-position: 100% 100%, 0 100%; background-repeat: no-repeat; }

Let’s update the background-size on hover so that the gradients swap values:

a:hover { background-size: 0 3px, 100% 3px; }

And, finally, a little transition when the hover takes place:

a { background: linear-gradient( to right, rgba(100, 200, 200, 1), rgba(100, 200, 200, 1) ), linear-gradient( to right, rgba(255, 0, 0, 1), rgba(255, 0, 180, 1), rgba(0, 100, 200, 1) ); background-size: 100% 3px, 0 3px; background-position: 100% 100%, 0 100%; background-repeat: no-repeat; transition: background-size 400ms; }


CodePen Embed Fallback The Passing Underline Link Hover Effect

Geoff Graham actually covered this same one recently when he dissected Adam Argyle’s slick hover effect. In his demo, a background color enters from the left behind the link, then exits to the right on mouse out.

My version pares down the background so it’s more of an underline.

a { position: relative; } a::before { content: ''; position: absolute; width: 100%; height: 4px; border-radius: 4px; background-color: #18272F; bottom: 0; left: 0; transform-origin: right; transform: scaleX(0); transition: transform .3s ease-in-out; } a:hover::before { transform-origin: left; transform: scaleX(1); } CodePen Embed Fallback

That’s not the only way to accomplish this! Here’s another one by Justin Wong using background instead:

CodePen Embed Fallback

Geoff also has a roundup of CSS link hover effects, ranging from neat to downright absurd. Worth checking out!

Have a blast linking!

There are a lot of options when it comes to creating your own hover effect for in-line links with CSS. You can even play with these effects and create something new. I hope you liked the article. Keep experimenting!

6 Creative Ideas for CSS Link Hover Effects originally published on CSS-Tricks. You should get the newsletter. Has a New Home on YouTube

Css Tricks - Tue, 02/15/2022 - 5:35am

(This is a sponsored post.)

✋ High fives to WordPress for releasing version 5.9 on January 29! This was the long-awaited introduction of the Site Editor and the reverberations are still being felt across the 43% slice of the web that is powered by WordPress.

The Site Editor is more than a neat feature: it’s a completely new approach to theming in WordPress. What makes it a big deal is that it lowers what was once a pretty high barrier to entry for anyone who wants to create or customize a WordPress theme, thanks to a visual interface that takes the PHP out of everything. If you’re interested more in this transition, check out Ganesh Dahal’s Deep Introduction to WordPress Block Themes.

Need a new template? All it takes is a click and dropping some blocks into place. Learn the Site Editor on’s YouTube Page

The Site Editor, like many things about WordPress, is intuitive as heck. But it’s still such a new concept that it might be worth getting a few pointers on how to use it.

That’s why the team set up a brand spankin’ new YouTube channel full of fresh videos that walk you through it, including how full-site editing works, how to set up a homepage, and much more.

The idea is that this YouTube channel can be your go-to for all sorts of educational resources to support your ongoing website-building needs. There’s already a good amount of content in there with plans for more videos released regularly.

And just because the videos center around, anyone running a WordPress site, self-hosted or not, will benefit from these step-by-step tutorials.

Subscribe on YouTube Has a New Home on YouTube originally published on CSS-Tricks. You should get the newsletter.

Why are hyperlinks blue?

Css Tricks - Mon, 02/14/2022 - 10:13am

Last year, Elise Blanchard did some great historical research and discovered that blue hyperlinks replaced black hyperlinks in 1993. They’ve been blue for so long now that the general advice I always hear is to keep them that way. There is powerful societal muscle memory for “blue text is a clickable link.”


On a hot tip, Elise kept digging and published a follow-up and identified the source of blue hyperlinks:

[…] it is Prof. Ben Shneiderman whom we can thank for the modern blue hyperlink.

But it didn’t start on the web. It was more about operating systems in the very early 1990s that started using blue for interactive components and highlighted text.

The decision to make hyperlinks blue in Mosaic, and the reason why we see it happening in Cello at the same time, is that by 1993, blue was becoming the industry standard for interaction for hypertext. It had been eight years since the initial research on blue as a hyperlink color. This data had been shared, presented at conferences, and printed in industry magazines. Hypertext went on to be discussed in multiple forums. Diverse teams’ research came to the same conclusion – color mattered. If it didn’t inspire Marc Andreessen and Eric Bina directly, it inspired those around them and those in their industry.

Because research:

[…] the blue hyperlink was indeed inspired by the research done at the University of Maryland.

To Shared LinkPermalink on CSS-Tricks

Why are hyperlinks blue? originally published on CSS-Tricks. You should get the newsletter.

Getting Started With the File System Access API

Css Tricks - Mon, 02/14/2022 - 6:01am

The File System Access API is a web API that allows read and write access to a user’s local files. It unlocks new capabilities to build powerful web applications, such as text editors or IDEs, image editing tools, improved import/export, all in the frontend. Let’s look into how to get started using this API.

Reading files with the File System Access API

Before diving into the code required to read a file from the user’s system, an important detail to keep in mind is that calling the File System Access API needs to be done by a user gesture, in a secure context. In the following example, we’ll use a click event.

Reading from a single file

Reading data from a file can be done in less than 10 lines of code. Here’s an example code sample:

let fileHandle; document.querySelector(".pick-file").onclick = async () => { [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const content = await file.text(); return content; };

Let’s imagine we have a button in our HTML with the class .pick-file. When clicking on this button, we launch the file picker by calling window.showOpenFilePicker(), and we store the result from this query in a variable called fileHandle. 

What we get back from calling showOpenFilePicker() is an array of FileSystemFileHandle objects representing each file we selected. As this example is for a single file, we destructure the result. I’ll show how to select multiple files a bit later.

These objects contain a kind and name property. If you were to use console.log(fileHandle), you would see the following object:

FileSystemFileHandle {kind: 'file', name: 'data.txt'}

The kind can either be file or directory.

On fileHandle, we can then call the getFile() method to get details about our file. Calling this method returns an object with a few properties, including a timestamp of when the file was last modified, the name of the file, its size, and type.

Finally, we can call text() on the file to get its content.

Reading from multiple files

To read from multiple files, we need to pass an options object to showOpenFilePicker().

For example:

let fileHandles; const options = { multiple: true, }; document.querySelector(".pick-file").onclick = async () => { fileHandles = await window.showOpenFilePicker(options); // The rest of the code will be shown below };

By default, the multiple property is set to false. Other options can be used to indicate the types of files that can be selected.

For example, if we only wanted to accept .jpeg files, the options object would include the following:

const options = { types: [ { description: "Images", accept: { "image/jpeg": ".jpeg", }, }, ], excludeAcceptAllOption: true, };

In this example, fileHandles is an array containing multiple files, so getting their content would be done in the following way:

let fileHandles; const options = { multiple: true, }; document.querySelector(".pick-file").onclick = async () => { fileHandles = await window.showOpenFilePicker(options); const allContent = await Promise.all( (fileHandle) => { const file = await fileHandle.getFile(); const content = await file.text(); return content; }) ); console.log(allContent); }; Writing to a file with the File System Access API

The File System Access API also allows you to write content to files. First, let’s look into how to save a new file.

Writing to a new file

Writing to a new file can also be done in a very short amount of code!

document.querySelector(".save-file").onclick = async () => { const options = { types: [ { description: "Test files", accept: { "text/plain": [".txt"], }, }, ], }; const handle = await window.showSaveFilePicker(options); const writable = await handle.createWritable(); await writable.write("Hello World"); await writable.close(); return handle; };

If we imagine a second button with the class save-file, on click, we open the file picker with the method showSaveFilePicker() and we pass in an option object containing the type of file to be saved, here a .txt file.

Calling this method will also return a FileSystemFileHandle object like in the first section. On this object, we can call the createWritable() method that will return a FileSystemWritableFileStream object. We can then write some content to this stream with the write() method in which we need to pass the content.

Finally, we need to call the close() method to close the file and finish writing the content to disk.

If you wanted to write some HTML code to a file for example, you would only need to change what’s in the options object to accept "text/html": [".html"]  and pass some HTML content to the write() method.

Editing an existing file

If you’d like to import a file and edit it with the File System Access API,  an example code sample would look like:

let fileHandle; document.querySelector(".pick-file").onclick = async () => { [fileHandle] = await window.showOpenFilePicker(); const file = await fileHandle.getFile(); const writable = await fileHandle.createWritable(); await writable.write("This is a new line"); await writable.close(); };

If you’ve been following the rest of this post, you might recognize that we start with the showOpenFilePicker() and getFile() methods to read a file and we then use createWritable(), write() and close() to write to that same file.

If the file you’re importing already has content, this code sample will replace the current content with the new one passed into the write() method.

Additional File System Access API features

Without going into too much detail, the File System Access API also lets you list files in directories and delete files or directories.

Read directories

Reading directories can be done with a tiny bit of code:

document.querySelector(".read-dir").onclick = async () => { const directoryHandle = await window.showDirectoryPicker(); for await (const entry of directoryHandle.values()) { console.log(entry.kind,; } };

If we add a new button with the class .read-dir, on click, calling the showDirectoryPicker() method will open the file picker and, when selecting a directory on your computer, this code will list the files found in that directory.

Delete files

Deleting a file in a directory can be done with the following code sample:

document.querySelector(".pick-file").onclick = async () => { const [fileHandle] = await window.showOpenFilePicker(); await fileHandle.remove(); };

If you want to delete a folder, you only need to make a small change to the code sample above:

document.querySelector(".read-dir").onclick = async () => { const directoryHandle = await window.showDirectoryPicker(); await directoryHandle.remove(); };

Finally, if you want to remove a specific file when selecting a folder, you could write it like this:

// Delete a single file named data.txt in the selected folder document.querySelector(".pick-folder").onclick = async () => { const directoryHandle = await window.showDirectoryPicker(); await directoryHandle.removeEntry("data.txt"); };

And if you want to remove an entire folder, you would need the following lines:

// Recursively delete the folder named "data" document.querySelector(".pick-folder").onclick = async () => { const directoryHandle = await window.showDirectoryPicker(); await directoryHandle.removeEntry('data', { recursive: true }); }; File System Access API browser support

At the moment, IE and Firefox don’t seem to be supporting the File System Access API. However, there exists a ponyfill called browser-fs-access.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

DesktopChromeFirefoxIEEdgeSafari101NoNo98TPMobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS SafariNoNoNo15.4 Wrapping up

If you’d like to try the File System Access API, check out this live demo text editor built by Google engineers. Otherwise, if you’d like to learn more about this API and all its features, here are some resources:

Getting Started With the File System Access API originally published on CSS-Tricks. You should get the newsletter.

Your CSS reset needs text-size-adjust (probably)

Css Tricks - Fri, 02/11/2022 - 2:46pm

Kilian Valkhof:

[…] Mobile Safari increases the default font-size when you switch a website from portrait to landscape. On phones that is, it doesn’t do it on iPad. Safari has been doing this for a long time, as a way to improve readability on non-mobile optimized websites. While undoubtedly useful in a time when literally no website was optimized for mobile, it’s significantly less helpful nowadays. […] Text size increasing randomly in a single situation is exactly the type of thing you want to guard for with a CSS reset.

This is very literally what text-size-adjust does. MDN:

When an element containing text uses 100% of the screen’s width, the algorithm increases its text size, but without modifying the layout. The text-size-adjust property allows web authors to disable or modify this behavior, as web pages designed with small screens in mind do not need it.

You can see Apple’s own docs showing off this is exactly what they do (on iPhones). There is an ancient bug where this would prevent zooming, but probably not a huge concern anymore.

Kilian’s recommendation:

html { -moz-text-size-adjust: none; -webkit-text-size-adjust: none; text-size-adjust: none; }

Firefox doesn’t even support it, so I’d maybe lose that vendor prefix, but otherwise I’d say I’m on board. I’d like to think I can handle my own text sizing.

Reminds me of how Mobile Safari does that zooming thing with text inputs under 16px, so watch out for that too.

To Shared LinkPermalink on CSS-Tricks

Your CSS reset needs text-size-adjust (probably) originally published on CSS-Tricks. You should get the newsletter.

9 New React and JavaScript Links for February 2022

Css Tricks - Fri, 02/11/2022 - 1:11pm

Every now and then, I find that I’ve accumulated a bunch of links about various things I find interesting. Like React and JavaScript! Here’s a list of nine links to other articles about them that I’ve been saving up and think are worth sharing.

Source: “Good advice on JSX conditionals” by Vladimir Klepov
  • Seed Funding for Remix
    Remix went open source after taking funding which seems like a solid move. It’s a for-now-React-only framework, so I think it’s fair that everyone asks how does it compare to Next.js. Which they answered. Probably worth noting again for us CSS folks, Kent mentioned: “Because Remix allows me to easily control which of my CSS files is on the page at any given time, I don’t have all the problems that triggered the JavaScript community to invent workarounds like CSS-in-JS.”
  • React Router v6
    Speaking of that gang, they released React Router v6, which looks like a positive move — all hooks based, 50% smaller than v5 — but is yet another major version with API changes. React Router has a history of API changes like this and they trigger plenty of grumbling in the community. There is plenty of that again.
  • React Aria
    “A library of React Hooks that provides accessible UI primitives for your design system” from… Adobe. Interesting. Looks like some pretty hard problems being solved here, like FocusScope (“When the contain prop is set, focus is contained within the scope.”) and interesting color inputs, like useColorField, useColorSlider, and useColorWheel. There are 59 hooks in all, ranging from interactions and forms to overlays and internationalization, with plenty of others in between.
  • Front End Tables: Sorting, Filtering, and Pagination
    Tania Rascia: “One thing I’ve had to do at every job I’ve had is implement a table on the front end of an application that has sorting, filtering, and pagination.” No shame in reaching for a big library with all these features, but sometimes it’s best to DIY.
  • Good advice on JSX conditionals
    Vladimir Klepov covers the (weirdly) many ways fairly simple conditionals can go wrong, like the number 0 leaking into your markup, and how to manage update versus remount in conditionals.
  • useProseMirror
    I’ve found ProseMirror to be a pretty nice rich text editor in the past. The library itself isn’t actually in React, so I think it’s a smart call here to make a modern React wrapper for it.
  • Spead up sluggish inputs with useDeferredValue
    You can introduce gnarly input delay the more work that an onChange function has to do on a text input. useDeferredValue gives us a way to separate high priority updates from low priority updates for cases like this.”
  • &#x1f3a5; A Cartoon Intro to WebAssembly
    If you don’t have a good understanding of what WebAssembly is, then Lin Clark will get you there in this video from JSConf EU 2017. So, no, not a new link or anything, but it’s new to me!
  • &#x1f3a5; Turborepo Demo and Walkthrough
    Vercel bought Turborepo. Turborepo is specifically focused on making monorepos better. As someone who’s main codebase is a monorepo with Lerna and Yarn Workspaces such that we can have multiple different sites all share things like a design system, this is right up our alley. This video is with the Turborepo creator Jared Palmer and Lee Robinson, head of developer relations at Vercel. In this video, you get to see it all work.

9 New React and JavaScript Links for February 2022 originally published on CSS-Tricks. You should get the newsletter.

Multi-Value CSS Properties With Optional Custom Property Values

Css Tricks - Fri, 02/11/2022 - 5:30am

Imagine you have an element with a multi-value CSS property, such as transform: optional custom property values:

.el { transform: translate(100px) scale(1.5) skew(5deg); }

Now imagine you don’t always want all the transform values to be applied, so some are optional. You might think of CSS optional custom property values:

.el { /* |-- default ---| |-- optional --| */ transform: translate(100px) var(--transform); }

But surprisingly using optional custom property values like this does not work as intended. If the --transform variable is not defined the whole property will not be applied. I’ve got a little “trick” to fix this and it looks like this:

.el { transform: translate(100px) var(--transform, ); }

Notice the difference? There is a fallback defined in there that is set to an empty value: (, )

That’s the trick, and it’s very useful! Here’s what the specification has to say:

In an exception to the usual comma elision rules, which require commas to be omitted when they’re not separating values, a bare comma, with nothing following it, must be treated as valid in var(), indicating an empty fallback value.

This is somewhat spiritually related to the The CSS Custom Property Toggle Trick that takes advantage of a custom property having the value of an empty space.


Like I said, this is useful and works for any multi-value CSS property. The following demo shows it using text-shadow, background, and filter in addition to the transform example we just discussed.

See the Pen CSS var – Fallback To Nothing by Yair Even Or (@vsync) on CodePen.

Some properties that accept multiple values, like text-shadow, require special treatment because they only work with a comma delimiter. In those cases, when the CSS custom property is defined, you (as the code author) know it is only to be used in a situation where a value is already defined where the custom property is used. Then a comma should be inserted directly in the custom property before the first value, like this:

--text-shadow: ,0 0 5px black;

This, of course, inhibits the ability to use this variable in places where it’s the only value of some property. That can be solved, though, by creating “layers” of variables for abstraction purposes, i.e. the custom property is pointing to lower level custom properties.

Beware of Sass compiler

While exploring this trick, I uncovered a bug in the Sass compiler that strips away the empty value (,) fallback, which goes against the spec. I’ve reported the bug and hope it will be fixed up soon.

As a temporary workaround, a fallback that causes no rendering can be used, such as:

transform: translate(100px) var(--transform, scale(1));

Multi-Value CSS Properties With Optional Custom Property Values originally published on CSS-Tricks. You should get the newsletter.

A Whistle-Stop Tour of 4 New CSS Color Features

Css Tricks - Thu, 02/10/2022 - 1:01pm

I was just writing in my “What’s new in since CSS3?” article about recent and possible future changes to CSS colors. It’s weirdly a lot. There are just as many or more new and upcoming ways to define colors than what we have now. I thought we’d take a really quick look.

First, a major heads up. This stuff is so complicated. I barely understand it. But here are some aspects:

  • Before all this upcoming change, we only had RGB as a color model, and everything dealt with that.
  • We had different “color spaces” that handled it differently (e.g. the rgb() function mapped that RGB color model as a cube with linear coordinates, the hsl() function mapped that RGB color model as a cylinder) but it was all sRGB gamut.
  • With the upcoming changes, we’re getting new color models and (!) we’re getting new functions that map that color model differently. So I think it’s kind of a double-triple whammy.

I can’t personally educate you on all the nitty-gritty details — I’m writing this because I bet there are a lot of you like me, wondering why you should care at all about this, and this is my attempt to understand why I should care about all of it.

Display-P3 is one that opens up a ton of more vibrant color that was able to be expressed before. body { background: color(display-p3 1 0.08 0); /* super red! */ }

It turns out that modern monitors can display way more colors, particularly extra vibrant ones, but we just have no way of defining those colors with classic CSS color syntaxes, like HEX, RGB, and HSL. Super weird, right?! But if you use Display-P3, you get a wider range of access to these vibrant colors.

That white line in Safari DevTools is showing us the “extra” range of Display-P3

The dev shop Panic latched onto this early on and started using these colors as a “secret weapon”:

&#x1f308; Along with WebGL, p3 colors are now a big part of the Panic website secret weapon pile. Shh, don’t tell anyone, but you should see this page on an iMac Pro screen!

— Panic (@panic) May 24, 2019

Jen Simmons also covers how to use them, including a fallback for non-supporting browsers:

Display P3 color. Designing in the browser. Amazing.

Let me show you how to switch over to P3, find a color, and then find a fallback color for older browsers. All while working inside Safari Web Inspector. (Turn sound on to hear me explain!)

— Jen Simmons (@jensimmons) January 5, 2022 Resources HWB is the one that is more “for humans” except that’s a bit debatable and it’s still based on sRGB.

I had no idea hwb() was a thing — shout out to Stefan Judis for blogging about it.

I normally think of HSL as the CSS color format that is “for humans” (and good for programmatic control) because, well, manipulating 360° of Hue and 0-100% of both Saturation and Lightness make some kind of obvious sense.

But in hwb(), we’ve got Hue (the same as HSL, I think), then Whiteness and Blackness. Stefan:

Adding White and Black to a color affects its saturation. Suppose you add the same amount of White and Black to a color, the color tone stays the same, but color loses saturation. This works up to 50% White and 50% Black (hwb(0deg 50% 50%)), which results in an achromatic color.

Stefan expressed some doubt that this is any easier to understand than HSL, and I tend to agree. I probably just need to get more used to it, but it seems to be more abstract than simply changing the lightness or saturation.

HWB is limited to the same color gamut (sRGB) as all the old color formats all. No new colors are unlocked here.

Resources LAB is like rgb() of a much wider gamut div { background: lab(150% -400 400); }

I liked Eric Portis’ explanation of LAB when I went around asking about it:

LAB is like RGB in that there are three linear components. Lower numbers mean less of the thing, bigger numbers mean more of the thing. So you could use LAB to specify the brightest, greenest green that ever bright-greened, and it’ll be super bright and green for everybody, but brighter and greener on monitors with wider gamuts.

So, we get all the extra color, which is awesome, but sRGB had this other problem (aside from being limited in color expression), that it isn’t perceptually uniform. Brian Kardell:

The sRGB space is not perceptually uniform. The same mathematical movement has different degrees of perceived effect depending on where you are at in the color space. If you want to read a designer’s experience with this, here’s an interesting example which does a good job struggling to do well.

The classic example here is how, in HSL, colors with the exact same “Lightness” really don’t feel the same at all.

HSL vs LAB:: lightness &#x1f4a1;

Same colors from our tricky color poll, but this time I’ve shown LAB’s version of the same color over top. Notice how much closer LAB’s lightness value is to the results of our poll!

&#x1f3a8; color spaces aren’t all the same y’all!

— Adam Argyle (@argyleink) December 3, 2019

But in LAB, apparently, it is perceptually uniform, meaning that programmatically manipulating colors is a much more sane task. And another bonus is that LAB colors are specced as being device-independent. Here’s Michelle Barker:

LAB and LCH are defined in the specification as device-independent colors. LAB is a color space that can be accessed in software like Photoshop and is recommended if you want a color to look the same on-screen as, say, printed on a t-shirt.

Resources LCH is like hsl() of a much wider gamut

Remember how I said HSL is “for humans” in that it is easier understand than RGB? Changing the Hue, Saturation, and Lightness makes a lot of logical sense. Similar here with lch() where we’ve got Lightness, Chroma, and Hue. Back to my conversation with Eric Portis:

LCH is more like HSL: a polar space. H = hue = a circle. So doing math to pick complementary colors (or whatever transforms you’re after) becomes trivial (just add 180 — or whatever!)

I suppose you’d pick LCH just because you like the syntax of it or because it makes some complicated programmatic thing you’re trying to do easier — and you get the fact that it can express 50% more colors for free.

We get the perceptual uniformity here, too. Here’s Lea Verou who seems excited that lightness will actually mean something:

In HSL, lightness is meaningless. Colors can have the same lightness value, with wildly different perceptual lightness. […] With LCH, any colors with the same lightness are equally perceptually light, and any colors with the same chroma are equally perceptually saturated.

Another benefit of the new model is that we can wipe our hands clean of the “gray dead zone” in CSS color gradients. I think because of this perceptual uniformity stuff, two rich colors won’t get cheeky and gradient themselves through non-rich territory.

There will always be tradeoffs in color models, especially with gradients. (Demo)

Here’s a small personal prediction: I’d say that lch() is probably going to be a designer favorite. Soon there are going to be a ton of new color choices and it’s too difficult and weird to always be picking different ones. LCH seems to have the most bang for the mental buck.

Resources “OK”

LAB ‘n’ friends seems so new because it is new… to CSS. But LAB was invented in the 1940s. In a conversation with Adam Argyle, he used a memorable phrase: All the color spaces have an Achilles’ heel. That is, something they kinda suck at. For sRGB, it’s the grey dead zone thing, as well as the limited color gamut. LAB is great and all, but it certainly has its own weaknesses. For example, a blue-to-white gradient in LAB travels pretty awkwardly through purpletown.

In December 2020, Björn Ottosson is all like “Hey, a new color space just dropped,” and now OKLAB exists. Apparently the CSS powers-that-be see enough value in that color space that both oklab() and oklch() are already specced. I guess we should care because they are just generally better, but don’t quote me on that.

Why is it Display P3 uses the color() function but the other’s don’t?

I don’t really know. I think the CSS color() function is a bit newer and that’s just how Safari dunked it in there to start. I have no idea if Display P3 will get its own dedicated function, or if we all should just start using CSS color(), or what.

/* This is how you use Display P3 */ color(display-p3 1 0.08 0); /* But this doesn't work */ color(oklch 42.1% 0.192 328.6); /* You gotta do this instead &#x1f937;‍♀️ */ oklch(42.1% 0.192 328.6); /* But you can use the color space within a gradient... */ background-image: linear-gradient( to right in oklch, lch(50% 100 100), lch(50% 100 250) ); The relative color syntax is super useful.

There is this really cool ability called “relative color syntax” where you can basically deconstruct a CSS color while moving it into another format. Say you have the (obviously) most famous CSS HEX color ever, fog dog, and you wanna kick it into HSL instead:

body { background: hsl(from #f06d06 h s l); }

Maybe that’s not all that useful immediately, but hey, now we’re able to add alpha to it! There is literally no other way to apply alpha to an existing HEX color, so that’s kinda huge:

body { background: hsl(from #f06d06 h s l / 0.5); }

But I can also mess with it. Say I wanna saturate fog dog a bit before I add opacity because the lower opacity will naturally dull it out and I wanna combat that. I can use calc() on the implied variables there:

body { background: hsl(from #f06d06 h calc(s + 20%) l / 0.5); }

That’s so cool. I’m sure we’ll see some amazing things come from this. And it certainly isn’t limited to HSL. I was just using HSL because it’s what is comfortable to me right now. I could start with the named color red and mess with it in LCH if I want:

body { background: lch(from red l calc(c + 15) h / 0.25); }

This stuff is going to be most useful when liberally combined with custom properties.

There are no special functions just for alpha anymore.

Just to be clear: no commas preceding the alpha value in a CSS color function — just a forward slash instead:

/* Old! */ rgb(255, 0, 0); rgba(255, 0, 0, 0.5); /* New! */ rgb(255 0 0); rgb(255 0 0 / 0.5); hsl(0deg 40% 40%) hsl(0deg 40% 40% / 90%) /* can be percentage rather than 0.9 or whatever */ /* The New color stuff ONLY has the single base function, no alpha secondardy function */ lab(49% 39 80) lab(49% 39 80 / 0.25) /* Display P3, with the color function, essentially works the same way with the slash */ color(display-p3 1 0.08 0 / 0.25); You can even define your own CSS color space.

But I literally can’t even think about that. It blows my mind, sorry.

A Whistle-Stop Tour of 4 New CSS Color Features originally published on CSS-Tricks. You should get the newsletter.

Developers Speculating About the Long-Distant Future: 2022

Css Tricks - Thu, 02/10/2022 - 11:45am

This is a wonderful roundup from Jeremy, who I picture circling January 1, 2022, in red marker on a giant paper calendar back in 2008 and patiently counting the days.

See, there was a little smattering of internet drama back in 2008 (weird, right?) where Hixie kind of “officially speculated” that HTML5 would take 19 years to make it to full “recommended” status (2003-2022). Seems like most web developers at the time were quite certain HTML, and perhaps the internet as we know it, would be essentially obsolete by 2022. They were not right.

To Shared LinkPermalink on CSS-Tricks

Developers Speculating About the Long-Distant Future: 2022 originally published on CSS-Tricks. You should get the newsletter.

Helpful Tips for Starting a Next.js Chrome Extension

Css Tricks - Thu, 02/10/2022 - 5:24am

I recently rewrote one of my projects — Minimal Theme for Twitter — as a Next.js Chrome extension because I wanted to use React for the pop-up. Using React would allow me to clearly separate my extension’s pop-up component and its application logic from its content scripts, which are the CSS and JavaScript files needed to execute the functionality of the extension.

As you may know, there are several ways to get started with React, from simply adding script tags to using a recommended toolchain like Create React App, Gatsby, or Next.js. There are some immediate benefits you get from Next.js as a React framework, like the static HTML feature you get with next export. While features like preloading JavaScript and built-in routing are great, my main goal with rewriting my Chrome extension was better code organization, and that’s really where Next.js shines. It gives you the most out-of-the-box for the least amount of unnecessary files and configuration. I tried fiddling around with Create React App and it has a surprising amount of boilerplate code that I didn’t need.

I thought it might be straightforward to convert over to a Next.js Chrome extension since it’s possible to export a Next.js application to static HTML. However, there are some gotchas involved, and this article is where I tell you about them so you can avoid some mistakes I made.

First, here’s the GitHub repo if you want to skip straight to the code.

New to developing Chrome extensions? Sarah Drasner has a primer to get you started.

Folder structure

next-export is a post-processing step that compiles your Next.js code, so we don’t need to include the actual Next.js or React code in the extension. This allows us to keep our extension at its lowest possible file size, which is what we want for when the extension is eventually published to the Chrome Web Store.

So, here’s how the code for my Next.js Chrome extension is organized. There are two directories — one for the extension’s code, and one containing the Next.js app.

&#x1f4c2; extension &#x1f4c4; manifest.json &#x1f4c2; next-app &#x1f4c2; pages &#x1f4c2; public &#x1f4c2; styles &#x1f4c4; package.json The build script

To use next export in a normal web project, you would modify the default Next.js build script in package.json to this:

"scripts": { "build": "next build && next export" }

Then, running npm run build (or yarn build) generates an out directory.

In this case involving a Chrome extension, however, we need to export the output to our extension directory instead of out. Plus, we have to rename any files that begin with an underscore (_), as Chrome will fire off a warning that “Filenames starting with “_” are reserved for use by the system.”

What we need is a way to customize those filenames so Chrome is less cranky.

This leads us to have a new build script like this:

"scripts": { "build": "next build && next export && mv out/_next out/next && sed -i '' -e 's/\\/_next/\\.\\/next/g' out/**.html && mv out/index.html ../extension && rsync -va --delete-after out/next/ ../extension/next/" }

sed on works differently on MacOS than it does on Linux. MacOS requires the '' -e flag to work correctly. If you’re on Linux you can omit that additional flag.


If you are using any assets in the public folder of your Next.js project, we need to bring that into our Chrome extension folder as well. For organization, adding a next-assets folder inside public ensures your assets aren’t output directly into the extension directory.

The full build script with assets is this, and it’s a big one:

"scripts": { "build": "next build && next export && mv out/_next out/next && sed -i '' -e 's/\\/_next/\\.\\/next/g' out/**.html && mv out/index.html ../extension && rsync -va --delete-after out/next/ ../extension/next/ && rm -rf out && rsync -va --delete-after public/next-assets ../extension/" } Chrome Extension Manifest

The most common pattern for activating a Chrome extension is to trigger a pop-up when the extension is clicked. We can do that in Manifest V3 by using the action keyword. And in that, we can specify default_popup so that it points to an HTML file.

Here we are pointing to an index.html from Next.js:

{ "name": "Next Chrome", "description": "Next.js Chrome Extension starter", "version": "0.0.1", "manifest_version": 3, "action": { "default_title": "Next.js app", "default_popup": "index.html" } }

The action API replaced browserAction and pageAction` in Manifest V3.

Next.js features that are unsupported by Chrome extensions

Some Next.js features require a Node.js web server, so server-related features, like next/image, are unsupported by a Chrome extension.

Start developing

Last step is to test the updated Next.js Chrome extension. Run npm build (or yarn build) from the next-app directory, while making sure that the manifest.json file is in the extension directory.

Then, head over to chrome://extensions in a new Chrome browser window, enable Developer Mode*,* and click on the Load Unpacked button. Select your extension directory, and you should be able to start developing!

Wrapping up

That’s it! Like I said, none of this was immediately obvious to me as I was getting started with my Chrome extension rewrite. But hopefully now you see how relatively straightforward it is to get the benefits of Next.js development for developing a Chrome extension. And I hope it saves you the time it took me to figure it out!

Helpful Tips for Starting a Next.js Chrome Extension originally published on CSS-Tricks. You should get the newsletter.

A Chrome Extension for Cloudinary That Helps You Pluck Out Useful Media URLs From Your Library Quickly

Css Tricks - Thu, 02/10/2022 - 5:23am

(This is a sponsored post.)

Cloudinary is a host for your digital assets like images and video. If you don’t already know them, you should, because you can build it into the asset management you almost certainly need to do if you run any size of website. Cloudinary helps you serve the assets as efficiently as technologically possible, meaning optimization, resizing, CDN hosting, and goes further in allowing interesting transforms on those assets.

If you already use it, unless you use it entirely through the APIs, you’ll know Cloudinary has a Media Library that gives you a UI dashboard for everything you’ve ever uploaded to Cloudinary. This is where you find your assets and open them up to play with the settings and transformations and such (e.g. blur it — then serve in the best possible format with automatic quality adjustments). You can always pop over to to use this. But wouldn’t it be nice if this process was made a bit easier?

That clutch moment where you get the URL of the image you need.

There are all sorts of moments while bopping the web around doing our jobs as developers where you might need to get your fingers on an asset URL.

Gimme that URL!

Here’s a personal example: we have a little custom CMS thing for building our weekly email The CodePen Spark. It expects a URL to an image.

This is the exact kind of moment that the brand new Chrome Media Library Extension could help. Essentially it gives you a context menu you can use right in the browser to snag a URL to an asset. Right click, Insert and Asset URL.

It pops up a UI right inline (where you are on the web) of your Media Library, and you pick an image from there. Find the one you want, open it up, and you can either “edit” it to customize it to your liking, or just Insert it straight away.

Then it plops the URL right onto the site (probably an input) where you need it.

You can set up defaults to your liking, but I really like how the defaults are f_auto and q_auto which are Cloudinary classics that you’ll almost surely want. They mean “serve in the best possible format” and “optimize it intelligently”.

Sharon Yelenik introduced it on the Cloudinary blog:

Say your team creates social posts on a browser tab on an automated marketing application. To locate a media asset, you must open another tab to search for the asset within the Media Library, copy the related URL, and paste it into the app. In some cases, you even have to download an asset and then upload it into the app.

Talk about a classic example of menial, mundane, and repetitive chores!

Exactly. I like the idea of having tools to optimize workflows that should be easy. I’d also call Cloudinary a bit of a technical/developer tool, and there is an aspect to this that could be set up on anyone’s machine that would allow them to pick assets from your Media Library easily, without any access control worries.

If all this appeals to you:

Get the Chrome Extension

Or see more at Cloudinary Labsdocumentation, and blog post.

A Chrome Extension for Cloudinary That Helps You Pluck Out Useful Media URLs From Your Library Quickly originally published on CSS-Tricks. You should get the newsletter.

SVGcode for “Live Tracing” Raster Images

Css Tricks - Wed, 02/09/2022 - 11:39am

Say you have a bitmap graphic — like a JPG, PNG, or GIF — and you wish it was vector, like SVG. What do you do? You could trace it yourself in some kind of design software. Or tools within design software can help.

(I don’t wanna delay the lede here, there is a free online tool for it now called SVGcode.)

I remember when Adobe Illustrator CS2 dropped in 2005 it had a feature called “Live Trace” and I totally made it my aesthetic. I used to make business cards for my folk band and they all had the look of a photograph-gone-vector. These days they apparently call it Image Trace.

SVGcode does exactly this, for free

Adobe software costs money though, so what other options are out there? I imagine they are out there, but now there is a wonderfully single-purpose web app called SVGcode for it by Thomas Steiner! He’s written about it in a couple of places:

I think it’s so cool both in what it does (super useful!) but also in the approach (so impressive what web apps can do these days!):

It uses the File System Access API, the Async Clipboard API, the File Handling API, and Window Controls Overlay customization. […]

Credit where credit is due: I didn’t invent this. With SVGcode, I just stand on the shoulders of a command line tool called Potrace by Peter Selinger that I have converted to Web Assembly, so it can be used in a Web app.

My just-out-of-college aesthetic is gonna live on people!

Thomas joined me and Dave over on ShopTalk episode #497 if you’re interested in hearing straight from Thomas about not just this, but the whole world of capable web apps. That episode was sort of designed as a follow-up to an article I wrote that asks: “Why would a business push a native app over a website?”

SVGcode for “Live Tracing” Raster Images originally published on CSS-Tricks. You should get the newsletter.

How to Make CSS Slanted Containers

Css Tricks - Wed, 02/09/2022 - 5:19am

I was updating my portfolio and wanted to use the forward slash (/) as a visual element for the site’s main layout. I hadn’t attempted to create a slanted container in CSS before, but it seemed like it would be easy at first glance. As I began digging into it more, however, there were actually a few very interesting challenges to get a working CSS slanted container that supports text and media.

Here’s what was going for and where I finally landed:

CodePen Embed Fallback

I started by looking around for examples of non-rectangular containers that allowed text to flow naturally inside of them. I assumed it’d be possible with CSS since programs like Adobe Illustrator and Microsoft Word have been doing it for years.

Step 1: Make a CSS slanted container with transforms

I found the CSS Shapes Module and that works very well for simple text content if we put the shape-outside property to use. It can even fully justify the text. But what it doesn’t do is allow content to scroll within the container. So, as the user scrolls down, the entire slanted container appears to move left, which isn’t the effect I wanted. Instead, I took a simpler approach by adding transform: skew() to the container.

.slant-container { transform: skew(14deg); }

That was a good start! The container was definitely slanted and scrolling worked as expected while pure CSS handled the resizing for images. The obvious problem, however, is that the text and images became slanted as well, making the content more difficult to read and the images distorted.

Step 2: Reverse the font

I made a few attempts to solve the issues with slanted text and images with CSS but eventually came up with an even simpler solution: create a new font using FontForge to reverse the text’s slant.

FontForge is an open-source font editor. I’d chosen Roboto Condensed Light for the site’s main content, so I downloaded the .ttf file and opened it up in FontForge. From there, I selected all the glyphs and applied a skew of 14deg to compensate for the slanting caused by the CSS transform on the container. I saved the new font file as Roboto-Rev-Italic.ttf and called it from my stylesheet.

There we go. Now the font is slanted in the opposite direction by the same amount of the container’s slant, offsetting things so that the content appears like the normal Roboto font I was originally using.

Step 3: Refine images and videos

That worked great for the text! Selecting the text even functioned normally. From there, all I needed to do was reverse the slant for block-level image and video elements using a negative skew() value that offsets the value applied to the container:

img, video { transform: skew(-14deg); }

I did wind up wrapping images and videos in extra divs, though. That way, I could give them nice backgrounds that appear to square nicely with the container. What I did was hook into the ::after pseudo-element and position it with a background that extends beyond the slanted container’s left and right edges.

img::after, video::after { content: ''; display: block; background: rgba(0, 0, 0, 0.5); position: absolute; top: 0; left: 0; width: 200%; height: 100%; } It’s subtle, but notice that the top-right and bottom-left corners of the image are filled in by the background of its ::after pseudo-element, making things feel more balanced. Final demo

Here’s that final demo again:

CodePen Embed Fallback

I’m using this effect right now on my personal website and love it so far. But have you done something similar with a different approach? Definitely let me know in the comments so we can compare notes!

How to Make CSS Slanted Containers originally published on CSS-Tricks. You should get the newsletter.

No Motion Isn’t Always prefers-reduced-motion

Css Tricks - Tue, 02/08/2022 - 10:55am

There is a code snippet that I see all the time when the media query prefers-reduced-motion is talked about. Here it is:

@media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }

This is CSS that attempts to obliterate any motion on a website under the condition that the user has specified a preference for reduced motion in the accessibility preferences of their operating system.

Why prefers-reduced-motion matters

The reason this setting exists is that on-screen movement is an accessibility concern. Here’s Eric Bailey:

Vestibular disorders can cause your vestibular system to struggle to make sense of what is happening, resulting in loss of balance and vertigo, migraines, nausea, and hearing loss. Anyone who has spun around too quickly is familiar with a confused vestibular system.

Vestibular disorders can be caused by both genetic and environmental factors. It’s part of the larger spectrum of conditions that make up accessibility concerns and it affects more than 70 million people.

Here he is again in a follow-up article:

If you have a vestibular disorder or have certain kinds of migraine or seizure triggers, navigating the web can be a lot like walking through a minefield — you’re perpetually one click away from activating an unannounced animation. And that’s just for casual browsing.

Reduced motion vs. nuked motion

Knowing this, the temptation might be high to go nuclear on the motion and wipe it out entirely when a user has specified a reduced motion preference. The trouble with that is — to quote Eric again — “animation isn’t unnecessary.” Some of it might be, but animation can also help accessibility. For example, a “transitional interface” (e.g. a list that animates an opening for a new item to slide into it) can be very helpful:

Animation can be a great tool to help combat some forms of cognitive disability by using it to break down complicated concepts, or communicate the relationship between seemingly disparate objects. Val Head’s article on A List Apart highlights some other very well-researched benefits, including helping to increase problem-solving ability, recall, and skill acquisition, as well as reducing cognitive load and your susceptibility to change blindness.

In this case, you would lose the helpful contextual movement if you just nuked it all. You just might want to take a different approach when in a prefers-reduced-motion scenario. Perhaps less, slower, or removed motion while leaning harder on color and fading transitions.

Ban Nadel recently wrote “Applying Multiple Animation @keyframes To Support Prefers-Reduced-Motion In CSS” and covered a similar example. A modal entrance animation uses both a fade-in and scale-in effect by default. Then, in a prefers-reduced-motion scenario, it uses the fade-in but not the scaling. The scaling causes movement in a way the fading doesn’t.

/* By default, we'll use the REDUCED MOTION version of the animation. */ @keyframes modal-enter { from { opacity: 0 ; } to { opacity: 1 ; } } /* Then, if the user has NO PREFERENCE for motion, we can OVERRIDE the animation definition to include both the motion and non-motion properties. */ @media ( prefers-reduced-motion: no-preference ) { @keyframes modal-enter { from { opacity: 0 ; transform: scale(0.7) ; } to { opacity: 1 ; transform: scale(1.0) ; } } }

See the GIF demo on Ben’s site if you’d like to see a quick comparison.

I like how this style of approach is think about the problem and come up with a reduced motion solution, rather than screw it all, no movement period!!

But not all motion is driven by CSS

While we’re on the topic of that screw-all-motion CSS snippet, note that it’s only effective at doing what it sets out to do on sites where all the movement is entirely CSS-driven. If you’re using JavaScript-powered animations beware that this nuclear snippet might… well here’s Josh Comeau:

If your animations are entirely driven by CSS, this works great… But I’ve had weird issues when running animations in JS. Specifically, I’ve seen this reset have the opposite effect, and make animations super fast and dizzying.

That’s right: It might do quite literally the opposite of what you are trying to do.

No Motion Isn’t Always prefers-reduced-motion originally published on CSS-Tricks. You should get the newsletter.

Replace JavaScript Dialogs With the New HTML Dialog Element

Css Tricks - Tue, 02/08/2022 - 5:09am

You know how there are JavaScript dialogs for alerting, confirming, and prompting user actions? Say you want to replace JavaScript dialogs with the new HTML dialog element.

Let me explain.

I recently worked on a project with a lot of API calls and user feedback gathered with JavaScript dialogs. While I was waiting for another developer to code the <Modal /> component, I used alert(), confirm() and prompt() in my code. For instance:

const deleteLocation = confirm('Delete location'); if (deleteLocation) { alert('Location deleted'); }

Then it hit me: you get a lot of modal-related features for free with alert(), confirm(), and prompt() that often go overlooked:

  • It’s a true modal. As in, it will always be on top of the stack — even on top of that <div> with z-index: 99999;.
  • It’s accessible with the keyboard. Press Enter to accept and Escape to cancel.
  • It’s screen reader-friendly. It moves focus and allows the modal content to be read aloud.
  • It traps focus. Pressing Tab will not reach any focusable elements on the main page, but in Firefox and Safari it does indeed move focus to the browser UI. What’s weird though is that you can’t move focus to the “accept” or “cancel” buttons in any browser using the Tab key.
  • It supports user preferences. We get automatic light and dark mode support right out of the box.
  • It pauses code-execution., Plus, it waits for user input.

These three JavaScripts methods work 99% of the time when I need any of these functionalities. So why don’t I — or really any other web developer — use them? Probably because they look like system errors that cannot be styled. Another big consideration: there has been movement toward their deprecation. First removal from cross-domain iframes and, word is, from the web platform entirely, although it also sounds like plans for that are on hold.

With that big consideration in mind, what are alert(), confirm() and prompt() alternatives do we have to replace them? You may have already heard about the <dialog> HTML element and that’s what I want to look at in this article, using it alongside a JavaScript class.

It’s impossible to completely replace Javascript dialogs with identical functionality, but if we use the showModal() method of <dialog> combined with a Promise that can either resolve (accept) or reject (cancel) — then we have something almost as good. Heck, while we’re at it, let’s add sound to the HTML dialog element — just like real system dialogs!

If you’d like to see the demo right away, it’s here.

A dialog class

First, we need a basic JavaScript Class with a settings object that will be merged with the default settings. These settings will be used for all dialogs, unless you overwrite them when invoking them (but more on that later).

export default class Dialog { constructor(settings = {}) { this.settings = Object.assign( { /* DEFAULT SETTINGS - see description below */ }, settings ) this.init() }

The settings are:

  • accept: This is the “Accept” button’s label.
  • bodyClass: This is a CSS class that is added to <body> element when the dialog is open and <dialog> is unsupported by the browser.
  • cancel: This is the “Cancel” button’s label.
  • dialogClass: This is a custom CSS class added to the <dialog> element.
  • message: This is the content inside the <dialog>.
  • soundAccept: This is the URL to the sound file we’ll play when the user hits the “Accept” button.
  • soundOpen: This is the URL to the sound file we’ll play when the user opens the dialog.
  • template: This is an optional, little HTML template that’s injected into the <dialog>.
The initial template to replace JavaScript dialogs

In the init method, we’ll add a helper function for detecting support for the HTML dialog element in browsers, and set up the basic HTML:

init() { // Testing for <dialog> support this.dialogSupported = typeof HTMLDialogElement === 'function' this.dialog = document.createElement('dialog') this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog' this.dialog.role = 'dialog' // HTML template this.dialog.innerHTML = ` <form method="dialog" data-ref="form"> <fieldset data-ref="fieldset" role="document"> <legend data-ref="message" id="${(Math.round(}"> </legend> <div data-ref="template"></div> </fieldset> <menu> <button data-ref="cancel" value="cancel"></button> <button data-ref="accept" value="default"></button> </menu> <audio data-ref="soundAccept"></audio> <audio data-ref="soundOpen"></audio> </form>` document.body.appendChild(this.dialog) // ... } Checking for support

The road for browsers to support <dialog> has been long. Safari picked it up pretty recently. Firefox even more recently, though not the <form method="dialog"> part. So, we need to add type="button" to the “Accept” and “Cancel” buttons we’re mimicking. Otherwise, they’ll POST the form and cause a page refresh and we want to avoid that.

<button${this.dialogSupported ? '' : ` type="button"`}...></button> DOM node references

Did you notice all the data-ref-attributes? We’ll use these for getting references to the DOM nodes:

this.elements = {} this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)

So far, this.elements.accept is a reference to the “Accept” button, and this.elements.cancel refers to the “Cancel” button.

Button attributes

For screen readers, we need an aria-labelledby attribute pointing to the ID of the tag that describes the dialog — that’s the <legend> tag and it will contain the message.


That id? It’s a unique reference to this part of the <legend> element:

The “Cancel” button

Good news! The HTML dialog element has a built-in cancel() method making it easier to replace JavaScript dialogs calling the confirm() method. Let’s emit that event when we click the “Cancel” button:

this.elements.cancel.addEventListener('click', () => { this.dialog.dispatchEvent(new Event('cancel')) })

That’s the framework for our <dialog> to replace alert(), confirm(), and prompt().

Polyfilling unsupported browsers

We need to hide the HTML dialog element for browsers that do not support it. To do that, we’ll wrap the logic for showing and hiding the dialog in a new method, toggle():

toggle(open = false) { if (this.dialogSupported && open) this.dialog.showModal() if (!this.dialogSupported) { document.body.classList.toggle(this.settings.bodyClass, open) this.dialog.hidden = !open /* If a `target` exists, set focus on it when closing */ if ( && !open) { } } } /* Then call it at the end of `init`: */ this.toggle() Keyboard navigation

Next up, let’s implement a way to trap focus so that the user can tab between the buttons in the dialog without inadvertently exiting the dialog. There are many ways to do this. I like the CSS way, but unfortunately, it’s unreliable. Instead, let’s grab all focusable elements from the dialog as a NodeList and store it in this.focusable:

getFocusable() { return [...this.dialog.querySelectorAll('button,[href],select,textarea,input:not([type=&quot;hidden&quot;]),[tabindex]:not([tabindex=&quot;-1&quot;])')] }

Next, we’ll add a keydown event listener, handling all our keyboard navigation logic:

this.dialog.addEventListener('keydown', e => { if (e.key === 'Enter') { if (!this.dialogSupported) e.preventDefault() this.elements.accept.dispatchEvent(new Event('click')) } if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel')) if (e.key === 'Tab') { e.preventDefault() const len = this.focusable.length - 1; let index = this.focusable.indexOf(; index = e.shiftKey ? index-1 : index+1; if (index < 0) index = len; if (index > len) index = 0; this.focusable[index].focus(); } })

For Enter, we need to prevent the <form> from submitting in browsers where the <dialog> element is unsupported. Escape will emit a cancel event. Pressing the Tab key will find the current element in the node list of focusable elements, this.focusable, and set focus on the next item (or the previous one if you hold down the Shift key at the same time).

Displaying the <dialog>

Now let’s show the dialog! For this, we need a small method that merges an optional settings object with the default values. In this object — exactly like the default settings object — we can add or change the settings for a specific dialog.

open(settings = {}) { const dialog = Object.assign({}, this.settings, settings) this.dialog.className = dialog.dialogClass || '' /* set innerText of the elements */ this.elements.accept.innerText = dialog.accept this.elements.cancel.innerText = dialog.cancel this.elements.cancel.hidden = dialog.cancel === '' this.elements.message.innerText = dialog.message /* If sounds exists, update `src` */ this.elements.soundAccept.src = dialog.soundAccept || '' this.elements.soundOpen.src = dialog.soundOpen || '' /* A target can be added (from the element invoking the dialog */ = || '' /* Optional HTML for custom dialogs */ this.elements.template.innerHTML = dialog.template || '' /* Grab focusable elements */ this.focusable = this.getFocusable() this.hasFormData = this.elements.fieldset.elements.length > 0 if (dialog.soundOpen) { } this.toggle(true) if (this.hasFormData) { /* If form elements exist, focus on that first */ this.focusable[0].focus() this.focusable[0].select() } else { this.elements.accept.focus() } }

Phew! That was a lot of code. Now we can show the <dialog> element in all browsers. But we still need to mimic the functionality that waits for a user’s input after execution, like the native alert(), confirm(), and prompt() methods. For that, we need a Promise and a new method I’m calling waitForUser():

waitForUser() { return new Promise(resolve => { this.dialog.addEventListener('cancel', () => { this.toggle() resolve(false) }, { once: true }) this.elements.accept.addEventListener('click', () => { let value = this.hasFormData ? this.collectFormData(new FormData(this.elements.form)) : true; if (this.elements.soundAccept.src) this.toggle() resolve(value) }, { once: true }) }) }

This method returns a Promise. Within that, we add event listeners for “cancel” and “accept” that either resolve false (cancel), or true (accept). If formData exists (for custom dialogs or prompt), these will be collected with a helper method, then returned in an object:

collectFormData(formData) { const object = {}; formData.forEach((value, key) => { if (!Reflect.has(object, key)) { object[key] = value return } if (!Array.isArray(object[key])) { object[key] = [object[key]] } object[key].push(value) }) return object }

We can remove the event listeners immediately, using { once: true }.

To keep it simple, I don’t use reject() but rather simply resolve false.

Hiding the <dialog>

Earlier on, we added event listeners for the built-in cancel event. We call this event when the user clicks the “cancel” button or presses the Escape key. The cancel event removes the open attribute on the <dialog>, thus hiding it.

Where to :focus?

In our open() method, we focus on either the first focusable form field or the “Accept” button:

if (this.hasFormData) { this.focusable[0].focus() this.focusable[0].select() } else { this.elements.accept.focus() }

But is this correct? In the W3’s “Modal Dialog” example, this is indeed the case. In Scott Ohara’s example though, the focus is on the dialog itself — which makes sense if the screen reader should read the text we defined in the aria-labelledby attribute earlier. I’m not sure which is correct or best, but if we want to use Scott’s method. we need to add a tabindex="-1" to the <dialog> in our init method:

this.dialog.tabIndex = -1

Then, in the open() method, we’ll replace the focus code with this:


We can check the activeElement (the element that has focus) at any given time in DevTools by clicking the “eye” icon and typing document.activeElement in the console. Try tabbing around to see it update:

Clicking the “eye” icon Adding alert, confirm, and prompt

We’re finally ready to add alert(), confirm() and prompt() to our Dialog class. These will be small helper methods that replace JavaScript dialogs and the original syntax of those methods. All of them call the open()method we created earlier, but with a settings object that matches the way we trigger the original methods.

Let’s compare with the original syntax.

alert() is normally triggered like this: window.alert(message);

In our Dialog, we’ll add an alert() method that’ll mimic this:

/* dialog.alert() */ alert(message, config = { target: }) { const settings = Object.assign({}, config, { cancel: '', message, template: '' }) return this.waitForUser() }

We set cancel and template to empty strings, so that — even if we had set default values earlier — these will not be hidden, and only message and accept are shown.

confirm() is normally triggered like this: window.confirm(message);

In our version, similar to alert(), we create a custom method that shows the message, cancel and accept items:

/* dialog.confirm() */ confirm(message, config = { target: }) { const settings = Object.assign({}, config, { message, template: '' }) return this.waitForUser() } prompt() is normally triggered like this: window.prompt(message, default);

Here, we need to add a template with an <input> that we’ll wrap in a <label>:

/* dialog.prompt() */ prompt(message, value, config = { target: }) { const template = ` <label aria-label="${message}"> <input name="prompt" value="${value}"> </label>` const settings = Object.assign({}, config, { message, template }) return this.waitForUser() }

{ target: } is a reference to the DOM element that calls the method. We’ll use that to refocus on that element when we close the <dialog>, returning the user to where they were before the dialog was fired.

We ought to test this

It’s time to test and make sure everything is working as expected. Let’s create a new HTML file, import the class, and create an instance:

<script type="module"> import Dialog from './dialog.js'; const dialog = new Dialog(); </script>

Try out the following use cases one at a time!

/* alert */ dialog.alert('Please refresh your browser') /* or */ dialog.alert('Please refresh your browser').then((res) => { console.log(res) }) /* confirm */ dialog.confirm('Do you want to continue?').then((res) => { console.log(res) }) /* prompt */ dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })

Then watch the console as you click “Accept” or “Cancel.” Try again while pressing the Escape or Enter keys instead.


We can also use the async/await way of doing this. We’re replacing JavaScript dialogs even more by mimicking the original syntax, but it requires the wrapping function to be async, while the code within requires the await keyword:

document.getElementById('promptButton').addEventListener('click', async (e) => { const value = await dialog.prompt('The meaning of life?', 42); console.log(value); }); Cross-browser styling

We now have a fully-functional cross-browser and screen reader-friendly HTML dialog element that replaces JavaScript dialogs! We’ve covered a lot. But the styling could use a lot of love. Let’s utilize the existing data-component and data-ref-attributes to add cross-browser styling — no need for additional classes or other attributes!

We’ll use the CSS :where pseudo-selector to keep our default styles free from specificity:

:where([data-component*="dialog"] *) { box-sizing: border-box; outline-color: var(--dlg-outline-c, hsl(218, 79.19%, 35%)) } :where([data-component*="dialog"]) { --dlg-gap: 1em; background: var(--dlg-bg, #fff); border: var(--dlg-b, 0); border-radius: var(--dlg-bdrs, 0.25em); box-shadow: var(--dlg-bxsh, 0px 25px 50px -12px rgba(0, 0, 0, 0.25)); font-family:var(--dlg-ff, ui-sansserif, system-ui, sans-serif); min-inline-size: var(--dlg-mis, auto); padding: var(--dlg-p, var(--dlg-gap)); width: var(--dlg-w, fit-content); } :where([data-component="no-dialog"]:not([hidden])) { display: block; inset-block-start: var(--dlg-gap); inset-inline-start: 50%; position: fixed; transform: translateX(-50%); } :where([data-component*="dialog"] menu) { display: flex; gap: calc(var(--dlg-gap) / 2); justify-content: var(--dlg-menu-jc, flex-end); margin: 0; padding: 0; } :where([data-component*="dialog"] menu button) { background-color: var(--dlg-button-bgc); border: 0; border-radius: var(--dlg-bdrs, 0.25em); color: var(--dlg-button-c); font-size: var(--dlg-button-fz, 0.8em); padding: var(--dlg-button-p, 0.65em 1.5em); } :where([data-component*="dialog"] [data-ref="accept"]) { --dlg-button-bgc: var(--dlg-accept-bgc, hsl(218, 79.19%, 46.08%)); --dlg-button-c: var(--dlg-accept-c, #fff); } :where([data-component*="dialog"] [data-ref="cancel"]) { --dlg-button-bgc: var(--dlg-cancel-bgc, transparent); --dlg-button-c: var(--dlg-cancel-c, inherit); } :where([data-component*="dialog"] [data-ref="fieldset"]) { border: 0; margin: unset; padding: unset; } :where([data-component*="dialog"] [data-ref="message"]) { font-size: var(--dlg-message-fz, 1.25em); margin-block-end: var(--dlg-gap); } :where([data-component*="dialog"] [data-ref="template"]:not(:empty)) { margin-block-end: var(--dlg-gap); width: 100%; }

You can style these as you’d like, of course. Here’s what the above CSS will give you:

alert() confirm() prompt()

To overwrite these styles and use your own, add a class in dialogClass,

dialogClass: 'custom'

…then add the class in CSS, and update the CSS custom property values:

.custom { --dlg-accept-bgc: hsl(159, 65%, 75%); --dlg-accept-c: #000; /* etc. */ } A custom dialog example

What if the standard alert(), confirm() and prompt() methods we are mimicking won’t do the trick for your specific use case? We can actually do a bit more to make the <dialog> more flexible to cover more than the content, buttons, and functionality we’ve covered so far — and it’s not much more work.

Earlier, I teased the idea of adding a sound to the dialog. Let’s do that.

You can use the template property of the settings object to inject more HTML. Here’s a custom example, invoked from a <button> with id="btnCustom" that triggers a fun little sound from an MP3 file:

document.getElementById('btnCustom').addEventListener('click', (e) => {{ accept: 'Sign in', dialogClass: 'custom', message: 'Please enter your credentials', soundAccept: '', soundOpen: '', target:, template: ` <label>Username<input type="text" name="username" value="admin"></label> <label>Password<input type="password" name="password" value="password"></label>` }) dialog.waitForUser().then((res) => { console.log(res) }) }); Live demo

Here’s a Pen with everything we built! Open the console, click the buttons, and play around with the dialogs, clicking the buttons and using the keyboard to accept and cancel.

CodePen Embed Fallback

So, what do you think? Is this a good way to replace JavaScript dialogs with the newer HTML dialog element? Or have you tried doing it another way? Let me know in the comments!

Replace JavaScript Dialogs With the New HTML Dialog Element originally published on CSS-Tricks. You should get the newsletter.

Syndicate content
©2003 - Present Akamai Design & Development.