Front End Web Development

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.

Hot new fonts

Typography - Wed, 02/09/2022 - 2:30am

Read the book, Typographic Firsts

This month’s hot new fonts include a voluptuous space-aged sans from Jean François Porchez, plus two new gems from Beasts of England that take us from the roaring twenties through the psychedelic 70s, and into the present day with aplomb and pizazz. Oh, and the launch of ILT’s premium collectibles!

The post Hot new fonts appeared first on I Love Typography.

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.

Netlify Has Scheduled Functions

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

(This is a sponsored post.)

Hey! Scheduled Functions are cool! Think of them like a CRON job. I want this code to run every Monday at 2pm. I want this code run every hour on the hour. That kind of thing. Why would you want to do that? There are tons of reasons! Perhaps something like “send my newsletter” where you write it on your site in Markdown, it gets processed into an email template and sent out via a Netlify Function. Now you could make that happen on a set schedule. Or something like “send all my new blog posts out, if there are any.”

This is pretty near and dear to me, because I’ve reached for paid outside services to do this for me in the past!

See, I have a little mini site right here on CSS-Tricks that is very time-based in that it lists upcoming conferences. It’s a totally static site, so once a date is passed, it, uh, kinda doesn’t matter, the site just stays how it is. But there is code that during the build process, it only builds out conferences in the future, not the past. So the trick is to run the build process every day.

Before Scheduled Functions, I used Zapier to do this, which has been humming along doing this just fine for years:

But the knowlege of how that works is basically locked up in my head. Plus, I’m doing it on a non-free third-party service, and there is always a little bit of Rube Goldberg-y technical debt to that.

I’m literally switching up how I’m doing it right this second as I type out this blog post. I’m just going to write the dumbest function ever that kicks a POST request to the URL that Netlify gives me to trigger builds and do it once a day. That’s it.

Might as well keep that URL as an “Enviornment Variable” like process.env.BUILD_SECRET or whatever

With this in place, I’m gonna switch off my Zap and just rest easy knowing all this functionality is now shored up in one place.

This is a Beta feature, for the record. Netlify doesn’t recommend it for production just quiiiiite yet, as per the Labs documentation. But my thing isn’t super mission-critical so I’m giving it a shot.

What else might you use them for? The blog post about the new feature has some ideas:

• Invoke a set of APIs to collate data for a report at the end of every week

• Back up data from one data store to another at the end of every night

• Build and deploy all your static content every hour instead of for every authored or merged pull request, or

• Anything else you can imagine you might want to invoke on a regular basis!

Netlify Has Scheduled Functions originally published on CSS-Tricks. You should get the newsletter.

Using Different Color Spaces for Non-Boring Gradients

Css Tricks - Mon, 02/07/2022 - 11:46am

A little gradient generator tool from Tom Quinonero. You’d think fading one color to another would be an obvious, simple, solved problem — it’s actually anything but!

Tom’s generator does two things that help make a gradient better:

  1. You can pick an “interpolation space.” Gradients that use the sRGB color space (pretty much all the color stuff we have in CSS today) have a bad habit of going through a gray dead zone, and if you interpolate the gradient in another color space, it can turn out nicer (and yet convert it back to RGB to use today).
  2. Easing the colors, though the use of multiple color-stops, which can result in a less abrupt and more pleasing look.
See the gray in the middle there? Different gradient apps with different color spaces

Josh has another similar app, as does Erik Kennedy. So stinkin’ interesting how different gradients are in different color spaces. Think of the color spaces as a physical map where individual colors are points on the map. Gradients are dumb. They just walk straight from one point on the map to the next. The colors underneath their feet as they walk make a massive difference in how the gradient turns out.

Safari Tech Preview has experimental CSS gradient colorspaces and I had tons of fun playing around last night with it!

background: linear-gradient(
to right in var(–colorspace),
black, white

basic black to white can be so different!

— Adam Argyle (@argyleink) February 6, 2022

To Shared LinkPermalink on CSS-Tricks

Using Different Color Spaces for Non-Boring Gradients originally published on CSS-Tricks. You should get the newsletter.

CSS Scroll Snap Slide Deck That Supports Live Coding

Css Tricks - Mon, 02/07/2022 - 5:24am

Virtual conferences have changed the game in terms of how a presenter is able to deliver content to an audience. At a live event it’s likely you just have your laptop, but at home, you may have multiple monitors so that you can move around windows and make off-screen changes when delivering live coding demos. However, as some events go back to in-person, you may be in a similar boat as me wondering how to bring an equivalent experience to a live venue.

With a bit of creativity using native web functionality and modern CSS, like CSS scroll snap, we’ll be building a no-JavaScript slide deck that allows live editing of CSS demos. The final deck will be responsive and shareable, thanks to living inside of a CodePen.

To make this slide deck, we’ll learn about:

  • CSS scroll snap, counters, and grid layout
  • The contenteditable attribute
  • Using custom properties and HSL for theming
  • Gradient text
  • Styling the <style> element
Slide templates

When making a slide deck of a bunch of different slides, it’s likely that you’ll need different types of slides. So we’ll create these three essential templates:

  • Text: open for any text you need to include
  • Title: emphasizing a headline to break up sections of content
  • Demo: split layout with a code block and the preview
Text presentation slide Title presentation slide

Demo presentation slide HTML templates

Let’s start creating our HTML. We’ll use an ordered list with the ID of slides and go ahead and populate a text and title slide.

Each slide is one of the list elements with the class of slide, as well as a modifier class to indicate the template type. For these text-based slides, we’ve nested a <div> with the class of content and then added a bit of boilerplate text.

<ol id="slides"> <li class="slide slide--text"> <div class="content"> <h1>Presentation Title</h1> <p>Presented by Your Name</p> <p><a target="_blank" href="<>">@5t3ph</a></p> </div> </li> <li class="slide slide--title"> <div class="content"> <h2>Topic 1</h2> </div> </li> </ol>

We’re using target="_blank" on the link due to CodePen using iframes for the preview, so it’s necessary to “escape” the iframe and load the link.

Base styles

Next, we’ll begin to add some styles. If you are using CodePen, these styles assume you’re not loading one of the resets. Our reset wipes out margin and ensures the <body> element takes up the total available height, which is all we really need here. And, we’ll make a basic font stack update.

* { margin: 0; box-sizing: border-box; } body { min-height: 100vh; font-family: system-ui, sans-serif; font-size: 1.5rem; }

Next, we’ll define that all our major layout elements will use a CSS grid, remove list styling from #slides, and make each slide take up the size of the viewport. Finally, we’ll use the place-content shorthand to center the slide--text and slide--title slide content.

body, #slides, .slide { display: grid; } #slides { list-style: none; padding: 0; margin: 0; } .slide { width: 100vw; height: 100vh; } .slide--text, .slide--title { place-content: center; }

Then, we’ll add some lightweight text styles. Since this is intended to be a presentation with one big point being made at a time, as opposed to an article-like format, we’ll bump the base font-size to 2rem. Be sure to adjust this value as you test out your final slides in full screen. You may decide it feels too small for your content versus your presentation viewport size.

h1, h2 { line-height: 1.1; } a { color: inherit; } .content { padding: 2rem; font-size: 2rem; line-height: 1.5; } .content * + * { margin-top: 0.5em; } .slide--text .content { max-width: 40ch; }

At this point, we have some large text centered within a container the size of the viewport. Let’s add a touch of color by creating a simple theme system.

We’ll be using the hsl color space for the theme while setting a custom property of --theme-hue and --theme-saturation. The hue value of 230 corresponds to a blue. For ease of use, we’ll then combine those into the --theme-hs value to drop into instances of hsl.

:root { --theme-hue: 230; --theme-saturation: 85%; --theme-hs: var(--theme-hue), var(--theme-saturation); }

We can adjust the lightness values for backgrounds and text. The slides will feel cohesive since they will be tints of that base hue.

Back in our main <body> style, we can apply this idea to create a very light version of the color for a background, and a dark version for the text.

body { /* ... existing styles */ background-color: hsl(var(--theme-hs), 95%); color: hsl(var(--theme-hs), 25%); }

Let’s also give .slide--title a little bit of extra pizazz by adding a subtle gradient background.

.slide--title { background-image: linear-gradient(125deg, hsl(var(--theme-hs), 95%), hsl(var(--theme-hs), 75%) ); } Demo slide template

Our demo slide breaks the mold so far and requires two main elements:

  • a .style container around an inline <style> element with actual written styles that you intend to both be visible on screen and apply to the demo
  • a .demo container to hold the demo preview with whatever markup is appropriate for that

If you’re using CodePen to create this deck, you’ll want to update the “Behavior” setting to turn off “Format on Save.” This is because we don’t want extra tabs/spaces prior to the styles block. Exactly why will become clear in a moment.

Here’s our demo slide content:

<li class="slide slide--demo"> <div class="style"> <style contenteditable="true"> .modern-container { --container-width: 40ch; width: min( var(--container-width), 100% - 3rem ); margin-inline: auto; } </style> </div> <div class="demo"> <div class="modern-container"> <div class="box">container</div> </div> </div> </li>

Note that extra contenteditable="true" attribute on the <style> block . This is a native HTML feature that allows you to mark any element as editable. It is not a replacement for form inputs and textareas and typically requires JavaScript for more full-featured functionality. But for our purposes, it’s the magic that enables “live” coding. Ultimately, we’ll be able to make changes to the content in here and the style changes will apply immediately. Pretty fancy, hold tight.

However, if you view this so far, you won’t see the style block displayed. You will see the outcome of the .modern-container demo styles are being applied, though.

Another relevant note here is that HTML5 included validating a <style> block anywhere; not just in the <head>.

What we’re going to do next will feel strange, but we can actually use display properties on <style> to make it visible. We’ve placed it within another container to use a little extra positioning for it and make it a resizable area. Then, we’ve set the <style> element itself to display: block and applied properties to give it a code editor look and feel.

.style { display: grid; align-items: center; background-color: hsl(var(--theme-hs), 5%); padding-inline: max(5vw, 2rem) 3rem; font-size: 1.35rem; overflow-y: hidden; resize: horizontal; } style { display: block; outline: none; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; color: hsl(var(--theme-hs), 85%); background: none; white-space: pre; line-height: 1.65; tab-size: 2; hyphens: none; }

Then, we need to create the .slide--demo rule and use CSS grid to display the styles and demo, side-by-side. As a reminder, we’ve already set up the base .slide class to use grid, so now we’ll create a rule for grid-template-columns just for this template.

.slide--demo { grid-template-columns: fit-content(85ch) 1fr; }

If you’re unfamiliar with the grid function fit-content(), it allows an element to use its intrinsic width up until the maximum value defined in the function. So, this rule says the style block can grow to a maximum of 85ch wide. When your <style> content is narrow, the column will only be as wide as it needs to be. This is really nice visually as it won’t create extra horizontal space while still ultimately capping the allowed width.

To round out this template, we’ll add some padding for the .demo. You may have also noticed that extra class within the demo of .box. This is a convention I like to use for demos to provide a visual of element boundaries when the size and position of something are important.

.demo { padding: 2rem; } .box { background-color: hsl(var(--theme-hs), 85%); border: 2px dashed; border-radius: .5em; padding: 1rem; font-size: 1.35rem; text-align: center; }

Here’s the result of our code template:

Live-editing functionality

Interacting with the displayed styles will actually update the preview! Additionally, since we created the .style container as a resizable area, you can grab the resize handle in the lower right to grow and shrink the preview area as needed.

The one caveat for our live-editing ability is that browsers treat it differently.

  • Firefox: This provides the best result as it allows both changing the loaded styles and full functionality of adding new properties and even new rules.
  • Chromium and Safari: These allow changing values in loaded styles, but not adding new properties or new rules.

As a presenter, you’ll likely want to use Firefox. As for viewers utilizing the presentation link, they’ll still be able to get the intention of your slides and shouldn’t have issues with the display (unless their browser doesn’t support your demoed code). But outside of Firefox, they may be unable to manipulate the demos as fully as you may show in your presentation.

You may want to “Fork” your finished presentation pen and actually remove the editable behavior on <style> blocks and instead display final versions of your demos styles, as applicable.

Reminder: styles you include in demos can potentially affect slide layout and other demos! You may want to scope demo styles under a slide-specific class to prevent unintended style changes across your deck.

Code highlighting

While we won’t be able to achieve full syntax highlighting without JavaScript, we can create a method to highlight certain parts of the code block for emphasis.

To do this, we’ll pair up linear-gradient with the -webkit properties that enable using an element’s background as the text effect. Then, using custom properties, we can define how many “lines” of the style block to highlight.

First, we’ll place the required -webkit properties directly on the <style> element. This will cause the visible text to disappear, but we’ll make it visible in a moment by adding a background. Although these are -webkit prefixed, they are supported cross-browser.

style { /* ...existing styles */ -webkit-text-fill-color: transparent; -webkit-background-clip: text; }

The highlighting effect will work by creating a linear-gradient with two colors where the lighter color shows through as the text color for the lines to highlight. As a default, we’ll bookend the highlight with a darker color such that it appears that the first property is highlighted.

Here’s a preview of the initial effect:

To create this effect, we need to work out how to calculate the height of the highlight color. In our <style> element’s rules, we’ve already set the line-height to 1.65, which corresponds to a total computed line height of 1.65em. So, you may think that we multiply that by the number of lines and call it a day.

However, due to the visible style block being rendered using white-space: pre to preserve line breaks, there’s technically a sneaky invisible line before the first line of text. This is created from formatting the <style> tag on an actual line prior to the first line of CSS code. This is also why I noted that preventing auto-formatting in CodePen is important — otherwise, you would also have extra left padding.

With these caveats in mind, we’ll set up three custom properties to help compute the values we need and add them to the beginning of our .style ruleset. The final --lines height value first takes into account that invisible line and the selector.

style { --line-height: 1.65em; --highlight-start: calc(2 * var(--line-height)); --lines: calc(var(--highlight-start) + var(--num-lines, 1) * var(--line-height)); }

Now we can apply the values to create the linear-gradient. To create the sharp transitions we need for this effect, we ensure the gradient stops from one color to the next match.

style { background-image: linear-gradient( hsl(var(--theme-hs), 75%) 0 var(--highlight-start), hsl(var(--theme-hs), 90%) var(--highlight-start) var(--lines), hsl(var(--theme-hs), 75%) var(--lines) 100% ); }

To help visualize what’s happening, I’ve commented out the -webkit lines to reveal the gradient being created.

Within our --lines calculation, we also included a --num-lines property. This will let you adjust the number of lines to highlight per demo via an inline style. This example adjusts the highlight to three lines:

<style contenteditable="true" style="--num-lines: 3">

We can also pass a recalculated --highlight-start to change the initial line highlighted:

<style contenteditable="true" style="--num-lines: 3; --highlight-start: calc(4 * var(--line-height))">

Let’s look at the outcome of the previous adjustment:

Now, if you add or remove lines during your presentation, the highlighting will not adjust. But it’s still nice as a tool to help direct your viewers’ attention.

There are two utility classes we’ll add for highlighting the rule only or removing highlighting altogether. To use, apply directly to the <style> element for the demo.

.highlight--rule-only { --highlight-start: calc(1 * var(--line-height)) } .highlight--none { background-image: none; background-color: currentColor; } Slide motion with CSS scroll snap

Alright, we have some nice-looking initial slides. But it’s not quite feeling like a slide deck yet. We’ll resolve that in two parts:

  1. Reflow the slides horizontally
  2. Use CSS scroll snap to enforce scrolling one slide at a time

Our initial styles already defined the #slides ordered list as a grid container. To accomplish a horizontal layout, we need to add one extra property since the .slides have already included dimensions to fill the viewport.

#slides { /* ...existing styles */ grid-auto-flow: column; }

For CSS scroll snap to work, we need to define which axis allows overflow, so for horizontal scrolling, that’s x:

#slides { overflow-x: auto; }

The final property we need for scroll snapping for the #slides container is to define scroll-snap-type. This is a shorthand where we select the x axis, and the mandatory behavior, which means initiating scrolling should always trigger snapping to the next element.

#slides { scroll-snap-type: x mandatory; }

If you try it at this point, you won’t experience the scroll snapping behavior yet because we have two properties to add to the child .slide elements. Use of scroll-snap-align tells the browser where to “snap” to, and setting scroll-snap-stopto always prevents scrolling past one of the child elements.

.slide { /* ...existing styles */ scroll-snap-align: center; scroll-snap-stop: always; }

The scroll snapping behavior should work either by scrolling across your slide or using left and right arrow keys.

There are more properties that can be set for CSS scroll snap, you can review the MDN docs to learn what all is available. CSS scroll snap also has a bit different behavior cross-browser, and across input types, like touch versus mouse, or touchpad versus mouse wheel, or via scrollbar arrows. For our presentation, if you find that scrolling isn’t very smooth or “snapping” then try using arrow keys instead.

Currently, there isn’t a way to customize the CSS scroll snap sliding animation easing or speed. Perhaps that is important to you for your presentation, and you don’t need the other features we’ve developed for modifying the code samples. In that case, you may want to choose a “real” presentation application.

CSS scroll snap is very cool but also has some caveats to be aware of if you’re thinking of using it beyond our slide deck context. Check out another scroll snapping demo and more information on

Slide numbers

An optional feature is adding visible slide numbers. Using a CSS counter, we can get the current slide number and display it however we’d like as the value of a pseudo-element. And using data attributes, we can even append the current topic.

The first step is giving our counter a name, which is done via the counter-reset property. This is placed on the element that contains items to be counted, so we’ll add it to #slides.

#slides { counter-reset: slides; }

Then, on the elements to be counted (.slide), we add the counter-increment property and callback to the name of the counter we defined.

.slide { counter-increment: slides; }

To access the current count, we’ll set up a pseudo element. Within the content property, the counter() function is available. This function accepts the name of our counter and returns the current number.

.slide::before { content: counter(slides); }

The number is now appearing but not where we want it. Because our slide content is variable, we’ll use classic absolute positioning to place the slide number in the bottom-left corner. And we’ll add some visual styles to make it enclosed in a nice little circle.

.slide::before { content: counter(slides); position: absolute; left: 1rem; bottom: 1rem; width: 1.65em; height: 1.65em; display: grid; place-content: center; border-radius: 50%; font-size: 1.25rem; color: hsl(var(--theme-hs), 95%); background-color: hsl(var(--theme-hs), 55%); }

We can enhance our slide numbers by grabbing the value of a data attribute to also append a short topic title. This means first adding an attribute to each <li> element where we want this to happen. We’ll add data-topic to the <li> for the title and code demo slides. The value can be whatever you want, but shorter strings will display best.

<li class="slide slide--title" data-topic="CSS">

We’ll use the attribute as a selector to change the pseudo element. We can get the value by using the attr() function, which we’ll concatenate with the slide number and add a colon for a separator. Since the element was previously a circle, there are a few other properties to update.

[data-topic]::before { content: counter(slides) ": " attr(data-topic); padding: 0.25em 0.4em; width: auto; border-radius: 0.5rem; }

With that added, here’s the code demo slide showing the added topic of “CSS”:

Small viewport styles

Our slides are already somewhat responsive, but eventually, there will be problems with horizontal scrolling on smaller viewports. My suggestion is to remove the CSS scroll snap and let the slides flow vertically.

To accomplish this will just be a handful of updates, including adding a border to help separate slide content.

First, we’ll move the CSS scroll snap related properties for #slides into a media query to only apply above 120ch.

@media screen and (min-width: 120ch) { #slides { grid-auto-flow: column; overflow-x: auto; scroll-snap-type: x mandatory; } }

Next, we’ll move the CSS scroll snap and dimension properties for .slide into this media query as well.

@media screen and (min-width: 120ch) { .slide { width: 100vw; height: 100vh; scroll-snap-align: center; scroll-snap-stop: always; } }

To stack the demo content, we’ll move our entire rule for .slide--demo into this media query:

@media screen and (min-width: 120ch) { .slide--demo { grid-template-columns: fit-content(85ch) 1fr; } }

Now everything is stacked, but we want to bring back a minimum height for each slide and then add the border I mentioned earlier:

@media (max-width: 120ch) { .slide { min-height: 80vh; } .slide + .slide { border-top: 1px dashed; } }

Your content also might be at risk of overflow on smaller viewports, so we’ll do a couple of adjustments for .content to try to prevent that We’ll add a default width that will be used on small viewports, and move our previous max-width constraint into the media query. Also shown is a quick method updating our <h1> to use fluid type.

h1 { font-size: clamp(2rem, 8vw + 1rem, 3.25rem); } .content { /* remove max-width rule from here */ width: calc(100vw - 2rem); } @media screen and (min-width: 120ch) { .content { width: unset; max-width: 45ch; } }

Additionally, I found it helps to reposition the slide counter. For that, we’ll adjust the initial styles to place it in the top-left, then move it back to the bottom in our media query.

.slide { /* adjust default here, removing the old "bottom" value */ top: 0.25rem; left: 0.25rem; } @media (min-width: 120ch) { .slide::before { top: auto; bottom: 1rem; left: 1rem; } } Final slide deck

The embed will likely be showing the stacked small viewport version, so be sure to open the full version in CodePen, or jump to the live view. As a reminder, the editing ability works best in Firefox.

CodePen Embed Fallback

If you’re interested in seeing a fully finished deck in action, I used this technique to present my modern CSS toolkit.

CSS Scroll Snap Slide Deck That Supports Live Coding originally published on CSS-Tricks. You should get the newsletter.

Building a newbie-friendly codebase

Css Tricks - Thu, 02/03/2022 - 12:32pm

Pedro Santos suggests:

  1. Using naming conventions such that you can learn them once and apply them everywhere
  2. Unidirectional data flows. Make it easy to follow the app flow.
  3. No magic numbers. I’d add they are even worse in CSS as it’s both the confusion they cause and how they are often tied to awkward or incorrect assumptions.
  4. Using data structures. Like state machines.
  5. Testing everything
  6. Good code > good comments
  7. Avoiding acronyms
  8. Refactoring opportunistically

To Shared LinkPermalink on CSS-Tricks

Building a newbie-friendly codebase originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Syndicate content
©2003 - Present Akamai Design & Development.