Tag: more

More Real-World Uses for :has()

The :has() pseudo-class is, hands-down, my favorite new CSS feature. I know it is for many of you as well, at least those of you who took the State of CSS survey. The ability to write selectors upside down gives us more superpowers I’d never thought possible.

I say “more superpowers” because there have already been a ton of really amazing clever ideas published by a bunch of super smart people, like:

This article is not a definitive guide to :has(). It’s also not here to regurgitate what’s already been said. It’s just me (hi 👋) jumping on the bandwagon for a moment to share some of the ways I’m most likely to use :has() in my day-to-day work… that is, once it is officially supported by Firefox which is imminent.

When that does happen, you can bet I’ll start using :has() all over the place. Here are some real-world examples of things I’ve built recently and thought to myself, “Gee, this’ll be so much nicer once :has() is fully supported.”

Avoid having to reach outside your JavaScript component

Have you ever built an interactive component that sometimes needs to affect styles somewhere else on the page? Take the following example, where <nav> is a mega menu, and opening it changes the colors of the <header> content above it.

I feel like I need to do this kind of thing all the time.

This particular example is a React component I made for a site. I had to “reach outside” the React part of the page with document.querySelector(...) and toggle a class on the <body>, <header>, or another component. That’s not the end of the world, but it sure feels a bit yuck. Even in a fully React site (a Next.js site, say), I’d have to choose between managing a menuIsOpen state way higher up the component tree, or do the same DOM element selection — which isn’t very React-y.

With :has(), the problem goes away:

header:has(.megamenu--open) {   /* style the header differently if it contains      an element with the class ".megamenu--open"   */ }

No more fiddling with other parts of the DOM in my JavaScript components!

Better table striping UX

Adding alternate row “stripes” to your tables can be a nice UX improvement. They help your eyes keep track of which row you’re on as you scan the table.

But in my experience, this doesn’t work great on tables with just two or three rows. If you have, for example, a table with three rows in the <tbody> and you’re “striping” every “even” row, you could end up with just one stripe. That’s not really worth a pattern and might have users wondering what’s so special about that one highlighted row.

Using this technique where Bramus uses :has() to apply styles based on the number of children, we can apply tble stripes when there are more than, say, three rows:

What to get fancier? You could also decide to only do this if the table has at least a certain number of columns, too:

table:has(:is(td, th):nth-child(3)) {   /* only do stuff if there are three or more columns */ }

Remove conditional class logic from templates

I often need to change a page layout depending on what’s on the page. Take the following Grid layout, where the placement of the main content changes grid areas depending on whether there’s a sidebar present.

Layout with left sidebar above a layout with no sidebar.

That’s something that might depend on whether there are sibling pages set in the CMS. I’d normally do this with template logic to conditionally add BEM modifier classes to the layout wrapper to account for both layouts. That CSS might look something like this (responsive rules and other stuff omitted for brevity):

/* m = main content */ /* s = sidebar */ .standard-page--with-sidebar {   grid-template-areas: 's s s m m m m m m m m m'; } .standard-page--without-sidebar {   grid-template-areas: '. m m m m m m m m m . .'; }

CSS-wise, this is totally fine, of course. But it does make the template code a little messy. Depending on your templating language it can get pretty ugly to conditionally add a bunch of classes, especially if you have to do this with lots of child elements too.

Contrast that with a :has()-based approach:

/* m = main content */ /* s = sidebar */ .standard-page:has(.sidebar) {   grid-template-areas: 's s s m m m m m m m m m'; } .standard-page:not(:has(.sidebar)) {   grid-template-areas: '. m m m m m m m m m . .'; }

Honestly, that’s not a whole lot better CSS-wise. But removing the conditional modifier classes from the HTML template is a nice win if you ask me.

It’s easy to think of micro design decisions for :has()like a card when it has an image in it — but I think it’ll be really useful for these macro layout changes too.

Better specificity management

If you read my last article, you’ll know I’m a stickler for specificity. If, like me, you don’t want your specificity scores blowing out when adding :has() and :not() throughout your styles, be sure to use :where().

That’s because the specificity of :has() is based on the most specific element in its argument list. So, if you have something like an ID in there, your selector is going to be tough to override in the cascade.

On the other hand, the specificity of :where() is always zero, never adding to the specificity score.

/* specificity score: 0,1,0.   Same as a .standard-page--with-sidebar    modifier class */ .standard-page:where(:has(.sidebar)) {   /* etc */ }

The future’s bright

These are just a few things I can’t wait to be able to use in production. The CSS-Tricks Almanac has a bunch of examples, too. What are you looking forward to doing with :has()? What sort of some real-world examples have you run into where :has() would have been the perfect solution?

More Real-World Uses for :has() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, ,

More Than “Slapping Paint on a Website”

I’m a sucker for anything about front-end job titles.

Anselm Hannemann:

CSS evolved and we’re beyond the point where everyone can just do it as a side interest. We all can learn it and build amazing stuff with it, but using it wisely and correctly in a large-scale context isn’t an easy job anymore. It deserves people whose work is to focus on that part of the code.

Anselm is partly in responding to Sacha Greif’s “Is There Too Much CSS Now?” and the overall sentiment that CSS has a much higher barrier to entry for those learning it today than it did, say, in the CSS3 days. Back then, there was a super direct path to see the magic of CSS. Rachel Andrew perfectly captures that magic feeling in a prescient post from 2019:

There is something remarkable about the fact that, with everything we have created in the past 20 years or so, I can still take a complete beginner and teach them to build a simple webpage with HTML and CSS, in a day. […] We just need a text editor and a few hours. This is how we make things show up on a webpage.

That’s the real entry point here […]

“HTML, CSS and our vanishing industry entry points”

Rachel is speaking to the abstraction of frameworks on top of vanilla CSS (and HTML) but you might as well tack big, shiny, and fairly new features on there, like CSS grid, flexbox, container queries, cascade layers, custom properties, and relational pseudo-classes, to name a few. Not that those are abstractions, of course. There’s just a lot to learn right now, whether you’ve been writing CSS for 20 days or 20 years.

But back to Anselm’s post. Do we need to think about CSS as more than just, you know, styling things? I often joke that my job is slapping paint on websites to make them pretty. But, honestly, I know it’s a lot more than that. We all know it’s more than that.

Maybe CSS is an industry in itself. Think of all the possible considerations that have to pass through your brain when writing CSS rules. Heck, Ahmad Shadeed recently shared all the things his brain processes just to style a Hero component. CSS touches so much of the overall user experience — responsiveness, accessibility, performance, cross-browser, etc. — that it clearly goes well beyond “slapping paint on websites”. So far beyond that each of those things could be someone’s full-time gig, depending on the project.

So, yes, CSS has reached a point where I could imagine seeing “CSS Engineer” on some job board. As Anselm said, “[CSS] deserves people whose work is to focus on that part of the code.” Seen that way, it’s not so hard to imagine front-end development as a whole evolving into areas of specialization, just like many other industries.

More Than “Slapping Paint on a Website” originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , , ,

More Details on `details`

A lot of chatter around the ol’ <details> and <summary> elements lately! I saw Lea Verou recently tweet an observation about the element’s display behavior and that sorta splintered into more observations and usage notes from folks, including a revived discussion on whether <summary> should be allowed to contain interactive elements or not.

There are a lot of dots to connect and I’ll do my best here to do exactly that.

Can we change the display of elements nested in the <details> element?

Super weird! If we crack open DevTools, the user agent stylesheet tells us <details> is a displayed as a block element.

Notice the required <summary> element and the two additional <div>s in there. We can override the display, right?

What we might expect is that <details> now has an explicit height of 40vh and three rows where the third row takes up the remaining space leftover from the first two. Like this:

Open details element with a summary of foo and two child elements, one yellow and one blue. The blue element takes up the rest of the space left by summary and the first child.

Ugh, but the third row doesn’t… do… that.

Open details element with a summary of foo and two child elements, one yellow and one blue. The summary and two child elements are all the same height.

Apparently what we’re dealing with is a grid container that is unable to apply grid behavior to its grid items. But the HTML spec tells us:

The details element is expected to render as a block box. The element is also expected to have an internal shadow tree with two slots.

(Emphasis mine)

And a little later:

The details element’s second slot is expected to have its style attribute set to “display: block; content-visibility: hidden;” when the details element does not have an open attribute. When it does have the open attribute, the style attribute is expected to be removed from the second slot.

(Emphasis mine, again)

So, the spec says the second slot — the two additional <div>s from the example — are only coerced into being block elements when <details> is closed. When it’s open — <details open> — they should conform to the grid display that overrides the user agent styling… right?

That’s the debate. I get that slots are set to display: contents by default, but jamming nested elements into slots and removing the ability to style them seems off. Is it a spec issue that the contents are slots, or a browser issue that we cannot override their display even though they are in the box tree? Smarter people can enlighten me but it seems like an incorrect implementation.

Is <details> a container or an interactive element?

Lots of folks are using <details> to toggle menus open and closed. It’s a practice popularized by GitHub.

DevTools open with the details element highlighted in orange.

Seems reasonable. The spec sure allows it:

The details element represents a disclosure widget from which the user can obtain additional information or controls.

(Emphasis mine)

Alright, so we might expect that <details> is the container (it has an implicit role=group) and <summary> is an interactive element that sets the container’s open state. Makes sense since <summary> has an implcit button role in some contexts (but no corresponding WAI-ARIA role).

But Melanie Sumner did some digging that not only seems to contradict that, but leads to the conclusion that using <details> as a menu probably ain’t the best thing. See what happens when <details> is rendered without the <summary> element:

It does exactly what the spec suggests when it’s missing a <summary> — it makes its own:

The first summary element child of the element, if anyrepresents the summary or legend of the details. If there is no child summary element, the user agent should provide its own legend (e.g. “Details”).

(Emphasis mine)

DevTools open with the summary markup highlighted in orange.

Melanie ran that through an HTML validator and — surprise! — it’s invalid:

Error, element details is missing a required instance of child element summary.

So, <details> requires the <summary>. And when <summary> is missing, <details> creates it’s own, though it’s relayed as invalid markup. It’s all hunky-dory and valid when <summary> is there:

Success message from the W3C HTML validator with the markup for a details element and summary that contains a link element.

All of which leads to a new question: why is <summary> given an implcit button role when <details> is what appears to be the interactive element? Perhaps this is another case where the browser implementation is incorrect? Then again, the spec does categorize both as interactive elements. You can see how utterly confusing all of this becomes.

Either way, Melanie’s ultimate conclusion that we ought to avoid using <details> for menus is based on how assistive tech reads and announces <details> that contain interactive elements. The element is announced, but there is no mention of interactive controls beyond that until you, er, interact with <details>. Only then will something like a list of links be announced.

Besides, content inside a collapsed <details> is excluded from in-page searching (except in Chromium browsers, which can access the collapsed content at the time of writing), making things even more difficult to find.

Should <summary> allow interactive elements?

That’s the question posed in this open thread. The idea is that something like this would be invalid:

<details>   <summary><a href="...">Link element</a></summary> </details>  <!-- or -->  <details>   <summary><input></summary> </details>

Scott O’Hara sums up nicely why this is an issue:

The link is not discoverable at all to JAWS when navigating with its virtual cursor. If navigating to the summary element via the Tab key, JAWS announces “example text, button” as the name and role of the element. If hitting Tab key again, JAWS again announces “example text, button” even though keyboard focus is on the link.


There is more I could go on about with the various problems different AT have with the content model for summary… but that would just extend this comment out beyond what is necessary. tldr; the summary content model produces very inconsistent and sometimes just flat out broken experiences for people using AT.

Scott opened tickets to correct this behavior in Chromium and WebKit. Thanks, Scott!

Yet, it’s valid HTML:

Success message from the W3C validator with details markup.

Scott goes further in a separate blog post. For example, he explains how slapping role=button on <summary> might seem like a reasonable fix to ensure it is consistently announced by assistive tech. That would also settle the debate over whether <summary> should allow interactive elements because buttons cannot contain interactive elements. The only problem then is that Safari then treats <summary> as a standard button, which loses it’s expanded and collapsed states. So, the correct role is announced, but now it’s state is not. 🙃

Where do we go now?

Are you scared to use <details>/<summary> with all of these issues and inconsistencies? I sure am, but only insofar as to make sure that what’s in it provides the right sort of experience and expectations for users.

I’m just glad these conversations are happening and that they’re taking place in the open. Because of that, you can comment on Scott’s three proposed solutions for how the content model for <summary> is defined, upvote his tickets, and report your own issues and use cases while you’re at it. Hopefully, the better we understand how the elements are used and what we expect them to do, the better they are implemented.

More Details on `details` originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



Syntax Highlighting (and More!) With Prism on a Static Site

So, you’ve decided to build a blog with Next.js. Like any dev blogger, you’d like to have code snippets in your posts that are formatted nicely with syntax highlighting. Perhaps you also want to display line numbers in the snippets, and maybe even have the ability to call out certain lines of code.

This post will show you how to get that set up, as well as some tips and tricks for getting these other features working. Some of it is tricker than you might expect.


We’re using the Next.js blog starter as the base for our project, but the same principles should apply to other frameworks. That repo has clear (and simple) getting started instructions. Scaffold the blog, and let’s go!

Another thing we’re using here is Prism.js, a popular syntax highlighting library that’s even used right here on CSS-Tricks. The Next.js blog starter uses Remark to convert Markdown into markup, so we’ll use the remark-Prism.js plugin for formatting our code snippets.

Basic Prism.js integration

Let’s start by integrating Prism.js into our Next.js starter. Since we already know we’re using the remark-prism plugin, the first thing to do is install it with your favorite package manager:

npm i remark-prism

Now go into the markdownToHtml file, in the /lib folder, and switch on remark-prism:

import remarkPrism from "remark-prism";  // later ...  .use(remarkPrism, { plugins: ["line-numbers"] })

Depending on which version of the remark-html you’re using, you might also need to change its usage to .use(html, { sanitize: false }).

The whole module should now look like this:

import { remark } from "remark"; import html from "remark-html"; import remarkPrism from "remark-prism";  export default async function markdownToHtml(markdown) {   const result = await remark()     .use(html, { sanitize: false })     .use(remarkPrism, { plugins: ["line-numbers"] })     .process(markdown);    return result.toString(); }

Adding Prism.js styles and theme

Now let’s import the CSS that Prism.js needs to style the code snippets. In the pages/_app.js file, import the main Prism.js stylesheet, and the stylesheet for whichever theme you’d like to use. I’m using Prism.js’s “Tomorrow Night” theme, so my imports look like this:

import "prismjs/themes/prism-tomorrow.css"; import "prismjs/plugins/line-numbers/prism-line-numbers.css"; import "../styles/prism-overrides.css";

Notice I’ve also started a prism-overrides.css stylesheet where we can tweak some defaults. This will become useful later. For now, it can remain empty.

And with that, we now have some basic styles. The following code in Markdown:

```js class Shape {   draw() {     console.log("Uhhh maybe override me");   } }  class Circle {   draw() {     console.log("I'm a circle! :D");   } } ```

…should format nicely:

Adding line numbers

You might have noticed that the code snippet we generated does not display line numbers even though we imported the plugin that supports it when we imported remark-prism. The solution is hidden in plain sight in the remark-prism README:

Don’t forget to include the appropriate css in your stylesheets.

In other words, we need to force a .line-numbers CSS class onto the generated <pre> tag, which we can do like this:

And with that, we now have line numbers!

Note that, based on the version of Prism.js I have and the “Tomorrow Night” theme I chose, I needed to add this to the prism-overrides.css file we started above:

.line-numbers span.line-numbers-rows {   margin-top: -1px; }

You may not need that, but there you have it. We have line numbers!

Highlighting lines

Our next feature will be a bit more work. This is where we want the ability to highlight, or call out certain lines of code in the snippet.

There’s a Prism.js line-highlight plugin; unfortunately, it is not integrated with remark-prism. The plugin works by analyzing the formatted code’s position in the DOM, and manually highlights lines based on that information. That’s impossible with the remark-prism plugin since there is no DOM at the time the plugin runs. This is, after all, static site generation. Next.js is running our Markdown through a build step and generating HTML to render the blog. All of this Prism.js code runs during this static site generation, when there is no DOM.

But fear not! There’s a fun workaround that fits right in with CSS-Tricks: we can use plain CSS (and a dash of JavaScript) to highlight lines of code.

Let me be clear that this is a non-trivial amount of work. If you don’t need line highlighting, then feel free to skip to the next section. But if nothing else, it can be a fun demonstration of what’s possible.

Our base CSS

Let’s start by adding the following CSS to our prism-overrides.css stylesheet:

:root {   --highlight-background: rgb(0 0 0 / 0);   --highlight-width: 0; }  .line-numbers span.line-numbers-rows > span {   position: relative; }  .line-numbers span.line-numbers-rows > span::after {   content: " ";   background: var(--highlight-background);   width: var(--highlight-width);   position: absolute;   top: 0; }

We’re defining some CSS custom properties up front: a background color and a highlight width. We’re setting them to empty values for now. Later, though, we’ll set meaningful values in JavaScript for the lines we want highlighted.

We’re then setting the line number <span> to position: relative, so that we can add a ::after pseudo element with absolute positioning. It’s this pseudo element that we’ll use to highlight our lines.

Declaring the highlighted lines

Now, let’s manually add a data attribute to the <pre> tag that’s generated, then read that in code, and use JavaScript to tweak the styles above to highlight specific lines of code. We can do this the same way that we added line numbers before:

This will cause our <pre> element to be rendered with a data-line="3,8-10" attribute, where line 3 and lines 8-10 are highlighted in the code snippet. We can comma-separate line numbers, or provide ranges.

Let’s look at how we can parse that in JavaScript, and get highlighting working.

Reading the highlighted lines

Head over to components/post-body.tsx. If this file is JavaScript for you, feel free to either convert it to TypeScript (.tsx), or just ignore all my typings.

First, we’ll need some imports:

import { useEffect, useRef } from "react";

And we need to add a ref to this component:

const rootRef = useRef<HTMLDivElement>(null);

Then, we apply it to the root element:

<div ref={rootRef} className="max-w-2xl mx-auto">

The next piece of code is a little long, but it’s not doing anything crazy. I’ll show it, then walk through it.

useEffect(() => {   const allPres = rootRef.current.querySelectorAll("pre");   const cleanup: (() => void)[] = [];    for (const pre of allPres) {     const code = pre.firstElementChild;     if (!code || !/code/i.test(code.tagName)) {       continue;     }      const highlightRanges = pre.dataset.line;     const lineNumbersContainer = pre.querySelector(".line-numbers-rows");      if (!highlightRanges || !lineNumbersContainer) {       continue;     }      const runHighlight = () =>       highlightCode(pre, highlightRanges, lineNumbersContainer);     runHighlight();      const ro = new ResizeObserver(runHighlight);     ro.observe(pre);      cleanup.push(() => ro.disconnect());   }    return () => cleanup.forEach(f => f()); }, []);

We’re running an effect once, when the content has all been rendered to the screen. We’re using querySelectorAll to grab all the <pre> elements under this root element; in other words, whatever blog post the user is viewing.

For each one, we make sure there’s a <code> element under it, and we check for both the line numbers container and the data-line attribute. That’s what dataset.line checks. See the docs for more info.

If we make it past the second continue, then highlightRanges is the set of highlights we declared earlier which, in our case, is "3,8-10", where lineNumbersContainer is the container with the .line-numbers-rows CSS class.

Lastly, we declare a runHighlight function that calls a highlightCode function that I’m about to show you. Then, we set up a ResizeObserver to run that same function anytime our blog post changes size, i.e., if the user resizes the browser window.

The highlightCode function

Finally, let’s see our highlightCode function:

function highlightCode(pre, highlightRanges, lineNumberRowsContainer) {   const ranges = highlightRanges.split(",").filter(val => val);   const preWidth = pre.scrollWidth;    for (const range of ranges) {     let [start, end] = range.split("-");     if (!start || !end) {       start = range;       end = range;     }      for (let i = +start; i <= +end; i++) {       const lineNumberSpan: HTMLSpanElement = lineNumberRowsContainer.querySelector(         `span:nth-child($ {i})`       );       lineNumberSpan.style.setProperty(         "--highlight-background",         "rgb(100 100 100 / 0.5)"       );       lineNumberSpan.style.setProperty("--highlight-width", `$ {preWidth}px`);     }   } }

We get each range and read the width of the <pre> element. Then we loop through each range, find the relevant line number <span>, and set the CSS custom property values for them. We set whatever highlight color we want, and we set the width to the total scrollWidth value of the <pre> element. I kept it simple and used "rgb(100 100 100 / 0.5)" but feel free to use whatever color you think looks best for your blog.

Here’s what it looks like:

Syntax highlighting for a block of Markdown code.

Line highlighting without line numbers

You may have noticed that all of this so far depends on line numbers being present. But what if we want to highlight lines, but without line numbers?

One way to implement this would be to keep everything the same and add a new option to simply hide those line numbers with CSS. First, we’ll add a new CSS class, .hide-numbers:

```js[class="line-numbers"][class="hide-numbers"][data-line="3,8-10"] class Shape {   draw() {     console.log("Uhhh maybe override me");   } }  class Circle {   draw() {     console.log("I'm a circle! :D");   } } ```

Now let’s add CSS rules to hide the line numbers when the .hide-numbers class is applied:

.line-numbers.hide-numbers {   padding: 1em !important; } .hide-numbers .line-numbers-rows {   width: 0; } .hide-numbers .line-numbers-rows > span::before {   content: " "; } .hide-numbers .line-numbers-rows > span {   padding-left: 2.8em; }

The first rule undoes the shift to the right from our base code in order to make room for the line numbers. By default, the padding of the Prism.js theme I chose is 1em. The line-numbers plugin increases it to 3.8em, then inserts the line numbers with absolute positioning. What we did reverts the padding back to the 1em default.

The second rule takes the container of line numbers, and squishes it to have no width. The third rule erases all of the line numbers themselves (they’re generated with ::before pseudo elements).

The last rule simply shifts the now-empty line number <span> elements back to where they would have been so that the highlighting can be positioned how we want it. Again, for my theme, the line numbers normally adds 3.8em worth of left padding, which we reverted back to the default 1em. These new styles add the other 2.8em so things are back to where they should be, but with the line numbers hidden. If you’re using different plugins, you might need slightly different values.

Here’s what the result looks like:

Syntax highlighting for a block of Markdown code.

Copy-to-Clipboard feature

Before we wrap up, let’s add one finishing touch: a button allowing our dear reader to copy the code from our snippet. It’s a nice little enhancement that spares people from having to manually select and copy the code snippets.

It’s actually somewhat straightforward. There’s a navigator.clipboard.writeText API for this. We pass that method the text we’d like to copy, and that’s that. We can inject a button next to every one of our <code> elements to send the code’s text to that API call to copy it. We’re already messing with those <code> elements in order to highlight lines, so let’s integrate our copy-to-clipboard button in the same place.

First, from the useEffect code above, let’s add one line:

useEffect(() => {   const allPres = rootRef.current.querySelectorAll("pre");   const cleanup: (() => void)[] = [];    for (const pre of allPres) {     const code = pre.firstElementChild;     if (!code || !/code/i.test(code.tagName)) {       continue;     }      pre.appendChild(createCopyButton(code));

Note the last line. We’re going to append our button right into the DOM underneath our <pre> element, which is already position: relative, allowing us to position the button more easily.

Let’s see what the createCopyButton function looks like:

function createCopyButton(codeEl) {   const button = document.createElement("button");   button.classList.add("prism-copy-button");   button.textContent = "Copy";    button.addEventListener("click", () => {     if (button.textContent === "Copied") {       return;     }     navigator.clipboard.writeText(codeEl.textContent || "");     button.textContent = "Copied";     button.disabled = true;     setTimeout(() => {       button.textContent = "Copy";       button.disabled = false;     }, 3000);   });    return button; }

Lots of code, but it’s mostly boilerplate. We create our button then give it a CSS class and some text. And then, of course, we create a click handler to do the copying. After the copy is done, we change the button’s text and disable it for a few seconds to help give the user feedback that it worked.

The real work is on this line:

navigator.clipboard.writeText(codeEl.textContent || "");

We’re passing codeEl.textContent rather than innerHTML since we want only the actual text that’s rendered, rather than all the markup Prism.js adds in order to format our code nicely.

Now let’s see how we might style this button. I’m no designer, but this is what I came up with:

.prism-copy-button {   position: absolute;   top: 5px;   right: 5px;   width: 10ch;   background-color: rgb(100 100 100 / 0.5);   border-width: 0;   color: rgb(0, 0, 0);   cursor: pointer; }  .prism-copy-button[disabled] {   cursor: default; }

Which looks like this:

Syntax highlighting for a block of Markdown code.

And it works! It copies our code, and even preserves the formatting (i.e. new lines and indentation)!

Wrapping up

I hope this has been useful to you. Prism.js is a wonderful library, but it wasn’t originally written for static sites. This post walked you through some tips and tricks for bridging that gap, and getting it to work well with a Next.js site.

Syntax Highlighting (and More!) With Prism on a Static Site originally published on CSS-Tricks. You should get the newsletter.


, , , , ,

The Web is More Gooder, and Other Observations on Today’s Web Tech

I’m actually working on a talk (whew! been a while! kinda feels good!) about just how good the world of building websites has gotten. I plan to cover a wide swath of web tech, on purpose, because I feel like things have gotten good all around. CSS is doing great, but so is nearly everything else involved in making websites, especially if we take care in what we’re doing.

It also strikes me that updates to the web platform and the ecosystem around it are generally additive. If you feel like the web used to be simpler, well, perhaps it was—but it also still is. Whatever you could do then you can do now, if you want to, although, it would be a fair point if you’re job searching and the expectations to get hired involve a wheelbarrow of complicated tech.

This idea of the web getting better feels like it’s in the water a bit…

Chris Ferdinandi in “Web tech is better. Developer norms are worse.”:

What the modern web can actually do, easily and out-of-the-box, is amazing. My friend Sarah Dayan started her career at around the same time as me, and has a wonderful thread on how things have changed since then.

In particular, Sarah talks about the dramatically improved capabilities of the web and expectations from customers and the people who use it.

Modern web technology is lightyears ahead of the late 2000s.

Wes and Scott on Syntax.fm 410 also talk about all kinds of stuff that is great now, from HTML, CSS, and JavaScript to tooling and hosting.

Simeon Griggs in “There’s never been a better time to build websites” has a totally different take on what is great on the web these days than mine, but I appreciate that. The options around building websites have also widened, meaning there are approaches to things that just feel better to people who think and work in different ways.

While there’s absolutely a learning curve to getting started, once you’ve got momentum, modern web development feels like having rocket boosters. The distance between idea and execution is as short as it’s ever been.


, , , ,

Working With Web Feeds: It’s More Than RSS

Between Google Chrome experimenting with “following” sites, along with a growing frustration of how social media platforms limit a creator’s reach to their fans through algorithmic feeds, there’s been renewed interest in RSS feeds and they’re primed for a comeback as we get into 2022.

This research is brought to you by support from Frontend Masters, CSS-Tricks’ official learning partner.

Need front-end development training?

Frontend Masters is the best place to get it. They have courses on all the most important front-end technologies. Interested in going full-stack? Here’s your best bet:

You may have heard whispers that “RSS in dead” around the web, but the truth is that they are still widely used as virtually every podcast uses one. Maybe you used to be an RSS fan and need to get re-acquainted with it. Or maybe you’re like Chris here at CSS-Tricks and still love RSS. Whatever the case, RSS is a web technology like any other, and there are best practices for how to create and curate a feed.

That’s exactly what I’m going to walk you through in this article. Let’s talk about the different kinds of feeds, how to implement them, and what strategies you can use to get the most out of your feed content.

RSS vs. Atom vs. JSON

Believe it or not, RSS is just one format among other types of syndicated web feeds. The most common formats are:

  1. RSS
  2. Atom
  3. JSON Feed

I’ve used RSS to signify these formats since it’s a far more popular search term but I’ll refer to these technologies as web feeds in this article unless I’m referring to a specific format.

On Sept 26–Oct 2, 2021, RSS had 37 points, web 6, atom 2 and jsonfeed 0. S
Google Trends for Atom, JSON, RSS and web feeds. (Source: Google Web Trends)

While Atom, RSS and, JSON feeds accomplish the same thing, there are a few differences between them:

  • Atom and RSS are based on XML while a JSON feed is based on, well, JSON.
  • All of these formats can be extended in some way. In a JSON feed, this is done by adding an object with a key that starts with an underscore anywhere in a feed’s object. With Atom and RSS, you do this by declaring the namespace on the root element. One example of this is on podcast feeds which declare the iTunes podcast namespace, allowing for the use of <itunes:*> tags.
  • JSON feed is a newer feed format meaning that support for it might not be as broad as Atom or RSS. If you have a podcast, however, RSS is a must.
  • While all of the formats require a unique identifier for each entry/item, Atom takes it a step further as it requires a unique identifier for every feed.
  • All allow HTML markup, though they handle it differently. JSON uses the content_html key containing JSON escaped HTML. Atom uses the content tag with type=html containing XML escaped HTML. RSS uses the <description> tag (or the content extension) which either contains XML-escaped HTML or the HTML unescaped in a <![CDATA[]]> tag.

Other than these things, there are only minor differences between them. You might think that the size of the file could be a possible difference, but compression reduces them all to just a few kilobytes apiece. Unless your application has a specific use case that requires a specific format (like podcasts), it doesn’t hurt to provide multiple formats, but RSS and Atom have the most support.

What makes a “good” feed?

Let’s look at some best practices for making feeds. Like most things on the web, there are things we can do to optimize our work to get the most out of it.

1. It’s easy to find

It doesn’t help to have a feed if no one knows about it. And one way to make a feed discoverable is to link it up in the <head> of your site. This way, feed readers are able to crawl and recognize when a site offers a content feed.

Here’s an example showing all three formats linked up in the document head:

<head>   <link rel="alternate" type="application/rss+xml" href="https://codelab.farai.xyz/index.rss.xml" title="Farai's Codelab's RSS Feed" />   <link rel="alternate" type="application/feed+json" href="https://codelab.farai.xyz/index.feed.json" title="Farai's Codelab's JSON Feed" />   <link rel="alternate" type="application/atom+xml" href="https://codelab.farai.xyz/index.atom.xml" title="Farai's Codelab's ATOM Feed" />   <!-- etc. --> </head>

And, yes, it’s OK to use all three! You can specify as many links as you want, though some feed readers might only recognize the first one. The important thing is that one includes rel="alternate" and the feed’s MIME type. There’s also the option to add a title which never hurts.

What else can you do to make your feed easy to find? Advertise it! Place direct links to the feeds somewhere prominent on your site that people can use to copy and paste into their feed reader.

That’s what CSS Tricks does. This is the link to the site’s RSS and it’s available in the footer across the entire site. Some feed readers can pick up on these links, too, even though they are outside of the <head>.

Screenshot of the CSS-Tricks footer showing a column of links with the heading Follow, and links for social networks, including the site's RSS feed. The background is near black, the headings are orange, and the links are light gray.
Oh, you want to subscribe to the CSS-Tricks RSS feed? Please do! Your RSS feed is just as much a thing to follow as any social network.

As for what to name the feed itself, it doesn’t matter as long as it’s discoverable. Here’s a good look into how various sites name their feeds, but I’ve named mine feed.json, feed.rss.xml and feed.atom.xml for JSON feed, Atom, and RSS respectively.

2. It takes advantage of HTTP

There are certain basic features of the web that can be leveraged to make your feeds a little better.

For example, be sure to compress your feeds, as it greatly reduces the overall file size and the time to download it. Most servers can take care of this for you, using gzip, Brotli, or some other compression engine.

Likewise, it’s a good idea to support either ETags or If-Modified-Since as they both allow clients to cache feeds and informs the browser whether a newer version of the feed is ready before it is downloaded. Much like compression, your server may take care of this as well.

Another thing to do: enable permissive CORS. Otherwise, clients could be blocked from fetching the feed. And while you should consider the security implications of letting any old site fetch your feed, it’s highly unlikely that it becomes a major issue for most small sites and feeds. This one-liner is all you need to enable CORS:

Access-Control-Allow-Origin: *

3. It displays full content instead of summaries

This is totally a user experience thing. You may have even experienced this before, where you subscribe to an RSS feed and all you get is the first paragraph or a summary of the post. Why? The traditional thinking is that providing only a summary encourages users to click through to your site, thereby leading to more visits. And more visits equals more eyeballs, which equals more revenue, etc.

I suggest avoiding that and instead allow your feeds to send the entire content for each post/entry/item. Many users prefer reading content in a feed reader because of the emphasis they place on legibility.

If you’re concerned about some dishonest person scraping your content and displaying it on their own site because you’re feeding the full content, let me reassure you: it’s no harder to a web page than a syndicated feed.

And if you’re a publisher who relies on display ads, and are concerned about the impact that sending full content might have on your revenue: you can still add static ads directly into your feed content. Besides, some readers can parse the web page associated with a feed entry so it can be read in the reader as well.

Dark UI with blue links and light gray text showing an article from a blog.
N/N Group article rendered in the NetNewsWire reader

But let’s not be dogmatic and all, because there are situations where summaries make sense. One is when a feed has a bunch of long-form entries. Another is when you have rich content that can only be viewed in a particular way (think show notes for a podcast). In that case, try making a good summary. One example is Nielsen Norman Group’s RSS which has a summary and an excerpt up to the first <h2> tag.

If I ever decide to only show summaries in my feed, I’d make sure to include an image, an outline of the content’s main points, and a link to the canonical version in addition to the summary. It is a bit of work but it gives the reader an idea what to expect, unlike some feeds I’ve seen which awkwardly truncate content to just the first few words.

Showing a post title in white with the published date and time below it in light gray. Below that is an image of a tired looking man with dark slicked back hair holding a 10 of diamonds card. Below that is the first sentence of a post that breaks mid-sentence.
Yes? You want to finish that sentence?

4. It is designed for reading

When crafting content, consider how it might be seen outside the context of a web browser, in places where JavaScript and CSS are limited. Sara Soueidan has a bunch of tips and ideas that are worth checking out. The main idea: provide a good fallback experience for your content.

This is a mostly an issue when it comes to embedded elements. While some embeds contain fallback content in their markup (like Twitter’s embedded tweets and CodePen’s embedded pens), others might not. Certain embeds (including videos posted to Vimeo) can only be embedded on certain domains meaning those won’t show up in a feed reader. So you need provide a way to view it somehow, like an image or a link to a webpage.

Showing the same article rendered in both Microsoft Outlook (left) and NetNewsWire (right). The article includes three embeds, one from Twitter, one from YouTube, and the third from TinkerCad), where each includes some form of fallback content for a better reading experience.

There are plenty of ways to do fallbacks. Twitter’s embed falls back to a <blockquote> — which makes total sense as a tweet is sort of like a quote — and a link to the tweet itself, which allows some clients that do not support embeds, like Outlook, to effectively render the content in a way that is still accessible to the user.

Though NetNewsWire is good with embeds, YouTube sometimes prevents it from playing videos like here. So, instead, the embed falls back to a link that points the user to watch it on YouTube’s site. Outlook doesn’t support YouTube embeds (or any embeds at all), but a descriptive link to the video on YouTube is still available.

The moral of the story: know your readers and how they render content so you can provide the best fallback experience possible.

Beware of relative URLs

One big issue across feeds is resolving relative URLs for images and links. Resolving based off the feed’s canonical link might work, but what happens if that link is in a subdirectory? The XML formats could use the xml:base attribute which defines the base URL to use when resolving relative URLs, but that’s only supported by Atom and is ignored and deprecated by most readers.

The most robust solution is to use absolute URLs for every href and src in an entry’s content. Meaning that the markup looks something like this:

<p>Read <a href="https://css-tricks.com/archives/">all our articles</a>.</p>

…and neither this:

<p>Read <a href="/archives/">all our articles</a>.</p>

…nor this:

<p>Read <a href="archives/">all our articles</a>.</p>

This is hard to do automatically, moreso with statically-generated sites. One approach is to make relative URLs absolute after compiling the feed in a build pipeline. Another approach is to manipulate the way Markdown links and images are rendered by your static site generator so that the URLs are absolute. I hope that more static site generators allow the second option but, for now, Hugo is the only static site generator that supports this through Markdown render hooks.

But wait, there’s an exception to this rule. And it’s footnotes. Some readers can detect footnotes and handle them. Here’s some HTML that should work in any feed reader that supports relative jump links:

<p>They’d managed to place 27.9MB of images onto the Critical Path.  Almost 30MB of previously non-render blocking assets had just been  turned into blocking ones on purpose with no escape hatch. Start  render time was as high as 27.1s over a cable connection<sup id="fnref:1">   <a href="#fn:1" class="footnote">1</a></sup>.</p>  <div class="footnotes">   <ol>     <li id="fn:1">      <p>5Mb up, 1Mb down, 28ms RTT. <a href="#fnref:1" class="reversefootnote">↩</a></p>     </li>   </ol> </div>

How to handle ads in feeds

You’re unlikely to get JavaScript support inside of an RSS reader, and that means no ads connected to an ad server. In other words, ads will need to be part of your content rather than something that is dynamically injected into place.

Showing an advertisement for anima that displays a brightly colored image, text below the image that says our sponsor, followed by the post title in blue, a blurb from the article content, them a blue learn more link.
The Codrops newsletter is an example that does this well. The newsletter includes a sponsored image and text, and clearly indicates the content is sponsored.

PSA: Not all content needs to be included in a feed

I’ve seen feeds in which every piece of content published is packed in and made available all the way back to the very first entry in the feed. I also see plenty of feeds from publishers who post dozens of entries a day. In both cases, I suggest limiting both the amount of content that’s available from past archives and considering multiple feeds instead of one.

Perfect example. Check out MacRumors.com’s feed because it’s extremely active with dozens of new articles published daily. Can you imagine going back to an article from, say, 10 years ago in that feed? Likely not. Unless the feed is for a podcast where storing every episode makes sense, try limiting the number of entries stored in your feed, as users are likely more interested in newer content. This reduces bandwidth and reduces update times which especially counts since users have many feeds to refresh.

I am tempted to say that 10–15 posts is enough to store and display at a time, but like many things, “it depends.” While storing a few makes sense for a site that pushes new content a few times a month, other sites that post way more frequently might eclipse that in a day. So, really, the ideal number of posts is going to depend on the type of content you publish (is it timely or evergreen?) and both the volume and frequency of what you publish (is it a lot throughout the day or a few times a month?).

But what I’m really trying to get at is that you want to avoid overwhelming users by inundating them with a pile of articles to get through. A couple of ways to avoid that include:

  • displaying summaries instead of full content (see, another exception to a previous rule!), and
  • filtering content so that users can choose from multiple feeds for specific types of content. In other words, if you want to provide every single post (or a complete timeline on a particular topic), consider making a dedicated feed for that topic/category/tag/whatever.

The reason I’m so fussy about the size of a feed is that—like images, scripts, and other assets—the number and size of feeds affect the performance of a feed reader. The more feeds a user is subscribed to and the more entries that need to be fetched from those feeds add to the time it takes to refresh and display that content.

Moving feeds

Like websites might change domains, you may need to move a feed from its current address. It’s not terribly difficult to do, but there are important things to consider before making a move.

For instance, it’s a good idea to ensure that your feed’s items have a global unique identifier (GUID). This maps out to feed’s guid in RSS and its id in both Atom and JSON. The GUI prevents feed readers from fetching duplicate entries. This is all the more important (and challenging) if you’re working with a static site.

While it may be tempting to use the entry’s permalink as an identifier, remember, those can change. To make a GUID, I’d recommend looking into using a tag URI. A tag URI consists of:

  • an authority (i.e., the domain of the site)
  • a date (that indicates a point in time that the tagging entity controlled the authority name associated with the feed)
  • the specific URL to fetch
  • a fragment (which might be a sub resource or a timestamp)

The <specific> portion could be something like the relative portion of your site’s homepage URL (i.e. /) and the fragment can be the content’s published timestamp. For instance, a post here on CSS Tricks could have a tag URI that looks like this:


This way, the authority date ensures that even if the domain changed hands. Plus, it can be managed in a static site generator as you can track domain changes over time.

The biggest reason I suggest the tag URI scheme is that Atom requires a feed’s id to be in a URL format. Even though RSS and JSON don’t have the same constraint, the tag URI scheme works for them as well, meaning we have full support.

And, with a robust id in place, a feed can be safely moved without feed readers pulling in duplicate entries. To move the feed itself, set up a 301 redirect to the new location and you’re done.

You might come across a technique called the XML redirect in which a file containing the feed’s new location is placed at the old location. As great as this would work for times when you can’t manipulate HTTP codes, I couldn’t find any feed readers which implement this.

Validating a feed

Feeds, like HTML, need to be valid in order to properly work. The benefit of a validated feed is that you know your code is free from errors and that entries are properly flowing from your site to feed readers.

W3C’s feed validation service is one option for RSS and Atom feeds. You provide the URL to the feed or paste the feed’s actual code, and you’ll get a full report that shows whether you’re hitting all the best practices. You’re likely to get warnings. It happens. Most warnings are really just a heads up and might not have an impact on the feed.

That said, there are two things that should always be addressed when validating a feed:

  • item should contain a guid element: The unique identifier, as we saw, prevents a feed reader from showing the same entry twice when a feed moves.
  • element should contain absolute URL references—these are hard for readers to resolve, so avoid relative URLs where possible.

What about JSON? To validate a JSON feed, try either using validator.jsonfeed.org or verifying against the JSON Feed schema using any JSON schema validator.

Managing or restricting access to a feed

You know how you can subscribe to a paid podcast and you get access to a special feed URL that contains all the “premium” content you gain access to with your subscription? Well, that’s because we can control who has access to a particular feed while locking others out from receiving the content.

There are two techniques for managing access to a feed:

  • HTTP basic authentication requires a username and password, which are either prompted for the user to provide or inferred in the feed URL itself, e.g. https://username:password@domain.example/path.
  • Providing a token as a query parameter, e.g. http://domain.com/path?token=xyz

As long as the URL is HTTPS, they have the same security, as the URL paths and passwords are encrypted. As for handling authentication it on the server, that’s a whole other topic though there are quite a few articles on it right here on CSS-Tricks.

Join the RSS Club!

OK, so the first rule of the RSS Club is:

Don’t talk about it. Let people find it. Make it worthwhile.

But I’m going to talk about it because feeds that are part of the RSS Club are excellent examples of tailored feeds. That’s because the feed entries are only available in those feeds. In other words, the blog posts are published, but never display on the actual site — they’re only accessible by feed.

Dave Rupert founded the club a number of years ago and it’s a great way to make RSS a first-class citizen for consuming content within a small community.

Joining the club means having a dedicated feed for posts that are only available in that feed. For example, in WordPress, you could create a new “RSS Club” category and filter it out of the main post query. That way, you’re able to either provide a feed just for that category, or the full feed that still includes posts in that category.

(Sorry for spilling the beans, Dave!)

Web feeds beyond content

RSS can be used for more things than blog posts or articles. For instance, GitHub has Atom feeds for issues, commits, pull requests, and releases.

They can also be used to provide updates. Let’s say you wanted a feed that notifies you when there are changes to your website. That’s a great idea, right? Always nice to know what’s happening, especially when there’s more than one cook in the kitchen.

You could build some sort of system that polls your feed periodically for changes then trigger a new feed entry, but that requires a lot of resources. Another idea is to implement webhooks you tell where to look for changes. Then again, managing and sending out notifications can be a hassle, especially if all you want is to monitor content.

I think it’s worth checking out WebSub. You, as a publisher, tell a hub that the site has changed, and the hub notifies whatever system that’s subscribed to the site’s web feeds. You can publish your feed to an existing hub — like Google’s PubSubHubbub Hub — then specify the hub in your feeds. YouTube has implemented this.

WebSub Flow Diagram, Julien Genestoux
Copyright © 2018 World Wide Web Consortium, (MIT, ERCIM, Keio, Beihang).


What’s a tutorial like this without a few good examples? Let’s look at three real-world examples.

1. RSS Podcast

Did you know that CSS-Tricks has a podcast for an ongoing series that covers web history? Well, it does. And, yes, you can subscribe to it via RSS.

Podcasts must use RSS with the xmlns:content and xmlns:itunes extensions, which are needed to provide metadata about the podcast and its episodes. The audio file for each episode is specified in an enclosure along with its mime type and size. RSS is limited to one enclosure, but both Atom and JSON support multiple enclosures.

Here’s the feed. Notice the iTunes-specific tags as well as other bits of information that are provided for additional context:

<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">   <channel>     <atom:link href="https://adactio.s3.amazonaws.com/audio/narration/web_history/podcast.xml" rel="self" type="application/rss+xml" />     <title>Web History</title>     <link>https://css-tricks.com/category/history/</link>     <language>en</language>     <copyright>2020</copyright>     <description>Written by Jay Hoffmann and narrated by Jeremy Keith.</description>     <image>       <url>https://adactio.s3.amazonaws.com/audio/narration/web_history/WebHistoryPodcast.jpg</url>       <title>Web History</title>       <link>https://css-tricks.com/category/history/</link>     </image>     <itunes:author>Jay Hoffman</itunes:author>     <itunes:summary>The history of the web.</itunes:summary>     <itunes:explicit>no</itunes:explicit>     <itunes:type>episodic</itunes:type>     <itunes:owner>       <itunes:name>Jeremy Keith</itunes:name>       <itunes:email>jeremy@adactio.com</itunes:email>     </itunes:owner>     <itunes:image href="https://adactio.s3.amazonaws.com/audio/narration/web_history/WebHistoryPodcast.jpg"/>     <itunes:category text="Technology"></itunes:category>     <item>       <title>Chapter 10: Browser Wars</title>       <description>In June of 1995, representatives from Microsoft arrived at the Netscape offices. The stated goal was to find ways to work together—Netscape as the single dominant force in the browser market and Microsoft as a tech giant just beginning to consider the implications of the Internet. Both groups, however, were suspicious of ulterior motives.</description>       <pubDate>Mon, 8 Nov 2021 12:00:00 -0000</pubDate>       <link>https://css-tricks.com/chapter-10-browser-wars/</link>       <itunes:title>Chapter 10: Browser Wars</itunes:title>       <itunes:episode>10</itunes:episode>       <itunes:episodeType>full</itunes:episodeType>       <itunes:author>Jay Hoffman</itunes:author>       <itunes:summary>In June of 1995, representatives from Microsoft arrived at the Netscape offices. The stated goal was to find ways to work together—Netscape as the single dominant force in the browser market and Microsoft as a tech giant just beginning to consider the implications of the Internet. Both groups, however, were suspicious of ulterior motives.</itunes:summary>       <content:encoded>         <![CDATA[           <p>In June of 1995, representatives from Microsoft arrived at the Netscape offices. The stated goal was to find ways to work together—Netscape as the single dominant force in the browser market and Microsoft as a tech giant just beginning to consider the implications of the Internet. Both groups, however, were suspicious of ulterior motives.</p>         ]]>       </content:encoded>       <itunes:duration>00:40:40</itunes:duration>       <guid>https://adactio.s3.amazonaws.com/audio/narration/web_history/Chapter_10_Browser_Wars.mp3</guid>       <enclosure url="https://adactio.s3.amazonaws.com/audio/narration/web_history/Chapter_10_Browser_Wars.mp3" length="19608877" type="audio/mpeg"/>     </item> </channel> </rss>

2. RSS for posts

Let’s look to CSS-Tricks once again, this time for an example of what a pretty standard RSS feed of blog posts looks like.

The code for this particular RSS feed is a little more verbose than your typical feed, and that’s to do with the multiple extensions added to the <rss> tag. A number of them aren’t reachable but there are some that handle other things, like xmlns:wfw for comments, xmlns:dc for additional metadata, and xmlns:sy for information on how often the feed is refreshed.

<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:georss="http://www.georss.org/georss" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">   <channel>     <title>CSS-Tricks</title>     <atom:link href="https://css-tricks.com/feed/" rel="self" type="application/rss+xml" />     <link>https://css-tricks.com</link>     <description>Tips, Tricks, and Techniques on using Cascading Style Sheets.</description>     <lastBuildDate>Fri, 19 Nov 2021 15:13:49 +0000</lastBuildDate>     <language>en-US</language>     <sy:updatePeriod>   hourly  </sy:updatePeriod>     <sy:updateFrequency>   1 </sy:updateFrequency>     <generator>https://wordpress.org/?v=5.8.2</generator>     <image>       <url>https://i1.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1</url>       <title>CSS-Tricks</title>       <link>https://css-tricks.com</link>       <width>32</width>       <height>32</height>     </image>     <site xmlns="com-wordpress:feed-additions:1">45537868</site>     <item>       <title>Parallax Powered by CSS Custom Properties</title>       <link>https://css-tricks.com/parallax-powered-by-css-custom-properties/</link>       <comments>https://css-tricks.com/parallax-powered-by-css-custom-properties/#respond</comments>       <dc:creator>         <![CDATA[Jhey Tompkins]]>       </dc:creator>       <pubDate>Fri, 19 Nov 2021 15:13:46 +0000</pubDate>       <category>         <![CDATA[Article]]>       </category>       <category>         <![CDATA[animation]]>       </category>       <category>         <![CDATA[custom properties]]>       </category>       <category>         <![CDATA[GSAP]]>       </category>       <guid isPermaLink="false">https://css-tricks.com/?p=357192</guid>       <description>         <![CDATA[ ]]>       </content:encoded>       <wfw:commentRss>https://css-tricks.com/parallax-powered-by-css-custom-properties/feed/</wfw:commentRss>       <slash:comments>0</slash:comments>        <post-id xmlns="com-wordpress:feed-additions:1">357192</post-id>     </item>   </channel> </rss>

3. JSON feed

This is actually my personal feed and I just so happen to use JSON for it. It’s pretty bare bones and less cluttered than the other examples because, as far as I know, there are no JSON extensions like the RSS ones we saw in Example 2.

I find that JSON is much easier to read and understand all that’s needed is an object with the feed data rather than writing out the entire template.

{   "author": {     "name": "Farai Gandiya"   },   "feed_url": "https://codelab.farai.xyz/feed.json",   "home_page_url": "https://codelab.farai.xyz/",   "icon": "https://codelab.farai.xyz/fcl-logo.png",   "items": [     {       "content_html": "...",       "date_modified": "2021-11-13T05:26:07+02:00",       "date_published": "2021-11-13T05:26:07+02:00",       "id": "https://codelab.farai.xyz/1636773967",       "summary": "...",       "title": "Don't be afraid of the Big Long Page by Amy Hupe, content designer.",       "url": "https://codelab.farai.xyz/links/long-content-ok/"     }   ] }

Web Feed Implementations Across CMSs and Static Site Generators

Many CMSs and static site generators support web feeds, though it’s usually RSS as it has the widest support. Here are some CMSs that support web feeds:

And here’s some resources on adding web feeds (again mostly RSS) to various static site generators

Wrapping up

And there you have it! This is what I believe be nearly everything you need to consider when implementing a web feed. We looked at three different formats (RSS, Atom, JSON), covered best practices for creating a user-friendly feed reading experience, walked through validating a feed, covered the possibility of authenticating feeds, looked at three real-world examples of feeds in the wild, and provided some implementations across various technologies.

(Oh, and there was that thing where the first rule of the thing is not to talk about the thing.)

I hope these guidelines empower you to make resilient web feeds. If you have any questions on implementing a web feed, or you just feel like sharing your RSS feed, please do leave a comment!


, , , ,

My tiny side project has had more impact than my decade in the software industry

That’s a heartwrenching title from Michael Williamson. I believe it though. It’s kinda like a maximized version of the blogging phenomenon where if you work on a post for weeks it’ll flop compared to a post that’s some dumb 20-minute thought. Or how your off-handed remark to some developer at the perfect time might cause some huge pivot in what they are doing, changing the course of a project forever. For Mike, it was a 3,000 line-of-code side project that had more impact on the world than a career of work as a software developer.

I’ve tried to pick companies working on domains that seem useful: developer productivity, treating diseases, education. While my success in those jobs has been variable – in some cases, I’m proud of what I accomplished, in others I’m pretty sure my net effect was, at best, zero – I’d have a tough time saying that the cumulative impact was greater than my little side project.

Impact is fuzzy though, isn’t it? I don’t know Mike, but assuming he is a kind and helpful person, think of all the people he’s likely helped along the way. Not by just saving them minutes of toil, but helped. Helped grow, helped through hard times, helped guide to where they ought to go. Those things are immeasurable and awfully important.

Direct Link to ArticlePermalink

The post My tiny side project has had more impact than my decade in the software industry appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , , , , ,

Equal Columns With Flexbox: It’s More Complicated Than You Might Think

You get a nice-looking design handed to you and it has this nice big hero section, followed by one of those three-up columns right beneath it. You know, like almost every other website you’ve ever worked on.

You bang through the hero section and get to work on the three-column section. It’s time to pull out our trusty friend flexbox! Except, you write display: flex and you get this mess instead of getting the three equal columns you’d expect.

This happens because of how flexbox calculates the base size of an element. You’ve probably read lots of flexbox tutorials, and many of them (including my own) are an overly simplistic example of what flexbox does without really digging into the complex things that flexbox does for us that we take for granted.

I’m positive you’ve seen examples and tutorials that look at something like this, where three divs shrink down around the content that’s inside them:

In other words, we get some block-level elements shrinking down and slotting next to one another. It feels like flex wants things to be as small as possible. But in reality, flexbox actually wants things to be as big as possible.

Wait, what? Flex shrinks things by default — that can’t be right! Right?

As awesome as flexbox is, what it’s doing under the hood is actually a little strange because, by default, it is doing two things at once. It first looks at the content size which is what we would get if by declaring width: max-content on an element. But on top of that, flex-shrink is also doing some work allowing the items to be smaller, but only if needed.

To really understand what’s going on, let’s break those two down and see how they work together.

Diving into max-content

max-content is a pretty handy property value in certain situations, but we want to understand how it works in this particular situation where we are trying to get three seemingly simple equal columns. So let’s strip away flexbox for a moment and look at what max-content does on its own.

MDN does a good job of explaining it:

The max-content sizing keyword represents the intrinsic maximum width of the content. For text content this means that the content will not wrap at all even if it causes overflows.

Intrinsic might throw you off here, but it basically means we’re letting the content decide on the width, rather than us explicitly setting a set width. Uri Shaked aptly describes the behavior by saying “the browser pretends it has infinite space, and lays all the text in a single line while measuring its width.”

So, bottom line, max-content allows the content itself to define the width of the element. If you have a paragraph, the width of that paragraph will be the text inside it without any line breaks. If it’s a long enough paragraph, then you end up with some horizontal scrolling.

Let’s revisit that overly-simplistic example of three block-level elements that shrink down and slot next to one another. That isn’t happening because of flex-shrink; it’s happening because that’s the size of those elements when their declared width is max-content. That’s literally as wide as they go because that’s as wide as the combined content inside each element.

Here, take a look at those elements without flexbox doing it’s flexbox stuff, but with a width: max-content on there instead:

So, when there’s just a small amount of text, the intrinsic max-content shrinks things down instead of flex-shrink. Of course, flexbox also comes in with it’s default flex-direction: row behavior which turns the flex items into columns, putting them right next to one another. Here’s another look but with the free space highlighted.

Adding flex-shrink to the equation

So we see that declaring display: flex pushes that max-content intrinsic size on flex items. Those items want to be as big as their content. But there is another default that comes in here as well, which is flex-shrink.

flex-shrink is basically looking at all the flex items in a flexible container to make sure they don’t overflow the parent. If the flex items can all fit next to each other without overflowing the flexible container, then flex-shrink won’t do anything at all… it’s job is already done.

But if the flex items do overflow the container (thanks to that max-content intrinsic width thing), the flex items are allowed to shrink to prevent that overflow because flex-shrink is looking out for that.

This is why flex-shrink has a default value of 1. Without it, things would get messy pretty quickly.

Here’s why the columns aren’t equal

Going back to our design scenario where we need three equal columns beneath a hero, we saw that the columns aren’t equal widths. That’s because flexbox starts by looking at the content size of each flex item before even thinking about shrinking them.

Using Firefox’s DevTools (because it’s got some unique flexbox visualizations that others don’t), we can actually see how flexbox is calculating everything.

For simplicity’s sake, as we dive deeper into this, let’s work with some nice round numbers. We can do this by declaring widths on our flex items. When we declare a width on a flex item, we throw that intrinsic size out the window, as we’ve now declared an explicit value instead. This makes figuring out what’s really going on a lot easier.

In the Pen below, we have a parent that’s a 600px wide flexible container (display: flex). I’ve removed anything that might influence the numbers, so no gap or padding. I’ve also switched out the border for an outline so we can still visualize everything easily.

The first and third flex items have a width: 300px and the middle one a width: 600px. If we add that all up, it’s a total of 1200px. That’s bigger than the the 600px available within the parent, so flex-shrink kicks in.

flex-shrink is a ratio. If everything has the same flex-shrink (which is 1 by default), they all shrink at the same rate. That doesn’t mean they all shrink to the same size or by the same amount, but they all shrink at the same rate.

If we jump back into Firefox DevTools, we can see the base size, the flex-shrink and the final size. In this case, the two 300px elements are now 150px, and the 600px one is now 300px.

The two elements that have a base width of 300px become 150px.

The larger element with a base width of 600px becomes 300px.

If we add up all the base sizes of all three flex items (the actual widths we declared on them), the total comes out to 1200px. Our flex container is 600px wide. If we divide everything by 2, it fits! They are all shrinking by the same rate, dividing their own widths by 2.

It’s not often that we have nice round numbers like that in the real world, but I think this does a nice job illustrating how flexbox does what it does when figuring out how big to make things.

Getting the columns to be equal

There are a few different ways to get the three columns we want to be equal in width, but some are better than others. For all the approaches, the basic idea is that we want to get all the columns base size to be the same. If they have an equal base size, then they will shrink (or grow, if we use flex-grow) at an equal rate when flexbox does it’s flex things, and in theory, that should make them the same size.

There are a few common ways to do this, but as I discovered while diving into all of this, I have come to believe those approaches are flawed. Let’s look at two of the most common solutions that I see used in the wild, and I’ll explain why they don’t work.

Method 1: Using flex: 1

One way we can try to get all the flex items to have the same base size is by declaring flex: 1 on all of them:

.flex-parent { display: flex; } .flex-parent > * { flex: 1; }

In a tutorial I made once, I used a different approach, and I must have had 100 people asking why I wasn’t using flex: 1 instead. I replied by saying I didn’t want to dive into the flex shorthand property. But then I used flex: 1 in a new demo, and to my surprise, it didn’t work.

The columns weren’t equal.

The middle column here is larger than the other two. It’s not by a ton, but the whole design pattern I’m creating is just so you have perfectly equal columns every single time, regardless of the content.

So why didn’t it work in this situation? The culprit here is the padding on the component in the middle column.

And maybe you’ll say it’s silly to add padding to one of the items and not the others, or that we can nest things (we’ll get to that). In my opinion, though, I should be able to have a layout that works regardless of the content that we’re putting in it. The web is all about components that we can plug and play these days, after all.

When I first set this up, I was sure it would work, and seeing this issue pop up made me want to learn what was really going on here.

The flex shorthand does more than just set the flex-grow: 1. If you don’t already know, flex is shorthand for flex-grow, flex-shrink, and flex-basis.

The default values for those constituent properties are:

.selector {   flex-grow: 0;   flex-shrink: 1;   flex-basis: auto; }

I’m not going to deep dive flex-basis in this article, as that’s something Robin has already done well. For now, we can think of it like width to keep things simple since we aren’t playing with flex-direction.

We’ve already seen how flex-shrink works. flex-grow is the opposite. If the size of all the flex items along the main axis is smaller than the parent, they can grow to fill that space.

So by default, flex items:

  • don’t grow;
  • if they would otherwise overflow the parent, they are allowed to shrink;
  • their width acts like max-content.

Putting that all together, the flex shorthand defaults to:

.selector {   flex: 0 1 auto; }

The fun thing with the flex shorthand is you can omit values. So, when we declare flex: 1, it’s setting the first value, flex-grow, to 1, which basically turns on flex-grow.

The strange thing here is what happens to the values that you omit. You’d expect them to stay at their defaults, but they don’t. Before we get to what happens, though, let’s first dive into what flex-grow even does.

As we’ve seen, flex-shrink allows elements to shrink if their base sizes add up to a computed value that’s bigger than the available width of the parent container. flex-grow is the opposite. If the grand total of the element base sizes is smaller than the value of the parent container’s width, then they will grow to fill the available space.

If we take that super basic example where we have three small divs next to one another and add flex-grow: 1, they grow to fill that leftover space.

But if we have three divs with unequal widths — like those ones we started with — adding flex-grow to them won’t do anything at all. They won’t grow because they’re already taking up all the available space —so much space, in fact, that flex-shrink needs to kick in and shrink them down to fit!

But, as folks have pointed out to me, setting flex: 1 can work to create equal columns. Well, sort of, as we saw above! In simple situations it does work though, as you can see below.

When we declare flex: 1 it works because, not only does this set the flex-grow to 1, but it also changes the flex-basis!

.selector {   flex: 1;   /* flex-grow: 1; */   /* flex-shrink: 1; */   /* flex-basis: 0%; Wait what? */ }

Yup, setting flex: 1 sets the flex-basis to 0%. This overwrites that intrinsic sizing we had before that behaved like max-content. All of our flex-items now want to have a base size of 0!

Looking at the expanded view of the flex: 1 shorthand in DevTools shows us that the flex-basis has changed to 0

So their base sizes are 0 now, but because of the flex-grow, they can all grow to fill up the empty space. And really, in this case, flex-shrink is no longer doing anything, as all the flex items now have a width of 0, and are growing to fill the available space.

FireFox’s DevTools showing an element with flex: 1 has a content size of 0 and is growing to fill the available space.

Just like the shrink example before, we’re taking the space that’s available, and letting all the flex items grow at an equal rate. Since they are all a base width of 0, growing at an equal rate means the available space is equally divided between them and they all have the same final size!

Except, as we saw, that’s not always the case…

The reason for this is because, when flexbox does all this stuff and distributes the space, whether it’s shrinking or growing a flex item, it’s looking at the content size of the element. If you remember back to the box model, we have the content size itself, then the padding, border, and margin outside of that.

And no, I didn’t forget * { box-sizing: border-box; }.

This is one of those strange quirks of CSS but it does make sense. If the browser looked at the width of those elements and included their padding and borders in the calculations, how could it shrink things down? Either the padding would also have to shrink or things are going to overflow the space. Both of those situations are terrible, so instead of looking at the box-size of elements when calculating the base size of them, it only looks at the content-box!

So, setting flex: 1 causes a problem in cases where you have borders or padding on some of your elements. I now have three elements that have a content-size of 0, but my middle one has padding on it. If we didn’t have that padding, we’d have the same math we did when we looked at how flex-shrink works.

A parent that is 600px and three flex items with a width of 0px. They all have a flex-grow: 1 so they grow at an equal rate, and they each end up 200px wide. But the padding mucks it all up. Instead, I end up with three divs with a content size of 0, but the middle one has padding: 1rem. That means it has a content size of 0, plus 32px padding as well.

We have 600 - 32 = 568px to divide equally, instead of 600px. All the divs want to grow at an equal rate, so 568 / 3 = 189.3333px.

And that’s what happens!

But… remember, that’s their content size, not the total width! That leaves us with two divs with a width of 189.333px, and another with a which of 189.333px + 32 = 221.333px. That’s a pretty substantial difference!

Method 2: flex-basis: 100%

I have always handled this like this:

.flex-parent {   display: flex; }  .flex-parent > * {   flex-basis: 100%; }

I thought this worked for the longest time. Actually, this was supposed to be my final solution after showing that flex: 1 doesn’t work. But while I was writing this article, I realized it also falls into the same problem, but it’s a lot less obvious. Enough so that I didn’t notice it with my eyes.

The elements are all trying to be 100% width, meaning they all want to match the width of the parent, which, again, is 600px (and in normal circumstances, is often much bigger, which further reduces the perceivable difference).

The thing is that 100% includes the padding in the computed values (because of * { box-size: border-box; }, which for the sake of this article, I’m assuming everyone is using). So, the outer divs end up with a content size of 600px, whereas the middle div ends up with a content size of 600 - 32 = 568px.

When the browser is working out how to evenly divide the space, it isn’t looking at how to evenly squish 1800px into a 600px space, but rather it’s looking at how to squish 1768px. Plus, as we saw earlier, flex items don’t shrink by the same amount, but at an equal pace! So, the element with padding shrinks slightly less in total than the others do.

This results in the .card having a final width of 214.483px while the others clock in at 192.75px. Again, this leads to unequal width values, though the difference is smaller than we saw with the flex: 1 solution.

Why CSS Grid is the better choice here

While all this is a little frustrating (and even a little confusing), it all happens for a good reason. If margins, padding, or borders changed sizes when flex gets involved, it would be a nightmare.

And maybe this means that CSS Grid might be a better solution to this really common design pattern.

I’ve long thought that flexbox was easier to pick up and start using than grid, but grid gives you more ultimate control in the long run, but that it’s a lot harder to figure out. I’ve changed my mind on that recently though, and I think not only does grid give us better control in this type of situation, but it’s actually more intuitive as well.

Normally, when we use grid, we explicitly declare our columns using grid-template-columns. We don’t have to do that though. We can make it behave a lot like flexbox does by using grid-auto-flow: column.

.grid-container {   display: grid;   grid-auto-flow: column; }

Just like that, we end up with the same type of behavior as throwing display: flex on there. Like flex, the columns can potentially be unbalanced, but the advantage with grid is that the parent has total control over everything. So, rather than the content of the items having an impact like they would in flexbox, we only need one more line of code and we can solve the problem:

.grid-container {   display: grid;   grid-auto-flow: column;   grid-auto-columns: 1fr; }

I love that this is all on the parent selector, and that we don’t have to select the children to help get the layout that we are after!

The interesting thing here is how fr units work. They are literally called flex units in the spec, and they work just like flexbox does in dividing up space;. The big difference: they’re looking at the other tracks to determine how much space they have, paying no attention to the content inside those tracks.

That’s the real magic here. By declaring grid-auto-columns: 1fr, we are in essence saying, “by default, all my columns should have an equal width,” which is what we’ve been after from the start!

But what about at small screens?

What I love with this approach is we can keep it super simple:

.grid-container {   display: grid;   gap: 1em; }  @media (min-width: 35em) {   grid-auto-flow: column;   grid-auto-columns: 1fr; }

And just like that, it works perfectly. And by declaring display: grid from the start, we can include the gap to maintain equal spacing, whether the children are rows or columns.

I also find this to be a lot more intuitive than changing the flex-direction within a media query to get a similar result. As much as I love flexbox (and I really do still think it has great use cases), the fact that declaring flex-direction: column creates rows, and vice versa, is a little counter-intuitive at first glance.

And of course, if you prefer rolling without media queries, there is nothing stopping you from taking this to the next level with the help of auto-fit, which would be similar to setting something up with flex-wrap (though not exactly the same):

.grid-container {   display: grid;   gap: 1em;   grid-template-columns: repeat(auto-fit, minmax(10em, 25em)); }

Making it work with flexbox

I realize that we can get around this with flexbox by nesting the element with the padding on it. We’ve done this since we started making layouts using floats, and Bootstrap really hammered home this type of design pattern.

<div class="container">   <div class="row">     <div class="col"> <div class="">... </div>     <div class="col"> <div class="element-with-padding">...</div> </div>     <div class="col"> ... </div>   </div> </div>

And there is nothing wrong with that. It works! But floats work too, and we’ve stopped using them for layouts as better solutions have been released in recent years.

One of the reasons that I love grid is because we can simplify our markup quite a bit. Just like we ditched floats for a better solution, I’d at least like to people to keep an open mind that maybe, just maybe, grid could be a better, and more intuitive solution for this type of design pattern.

Flexbox still has it’s place, of course

I still love flexbox, but I think its real power comes from times that we want to rely on the intrinsic width of the flex items, such as with navigations, or groups of elements, such as buttons or other items of varying width that you want to go next to one another.

In those situations, that behavior is an asset that makes things like even spacing between unequal items such a breeze! Flexbox is wonderful, and I have no plans to stop using.

In other situations though, when you find yourself fighting with how flexbox is trying to work, maybe you could turn to grid instead, even if it’s not a typical “2d” grid where you’re told you should be using it for.

People often tell me that they struggle to figure out grid because it’s too complicated, and while it can be, as we saw here, it doesn’t have to be.

The post Equal Columns With Flexbox: It’s More Complicated Than You Might Think appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , , , , ,

Making Disabled Buttons More Inclusive

Let’s talk about disabled buttons. Specifically, let’s get into why we use them and how we can do better than the traditional disabled attribute in HTML (e.g. <button disabled> ) to mark a button as disabled.

There are lots of use cases where a disabled button makes a lot of sense, and we’ll get to those reasons in just a moment. But throughout this article, we’ll be looking at a form that allows us to add a number of tickets to a shopping cart.

This is a good baseline example because there’s a clear situation for disabling the “Add to cart” button: when there are no tickets to add to the cart.

But first, why disabled buttons?

Preventing people from doing an invalid or unavailable action is the most common reason we might reach for a disabled button. In the demo below, we can only add tickets if the number of tickets being added to the cart is greater than zero. Give it a try:

Allow me to skip the code explanation in this demo and focus our attention on what’s important: the “Add to cart” button.

<button type="submit" disabled="disabled">   Add to cart </button>

This button is disabled by the disabled attribute. (Note that this is a boolean attribute, which means that it can be written as disabled or disabled="disabled".)

Everything seems fine… so what’s wrong with it?

Well, to be honest, I could end the article right here asking you to not use disabled buttons because they suck, and instead use better patterns. But let’s be realistic: sometimes disabling a button is the solution that makes the most sense.

With that being said, for this demo purpose, we’ll pretend that disabling the “Add to cart” button is the best solution (spoiler alert: it’s not). We can still use it to learn how it works and improve its usability along the way.

Types of interactions

I’d like to clarify what I mean by disabled buttons being usable. You may think, If the button is disabled, it shouldn’t be usable, so… what’s the catch? Bear with me.

On the web, there are multiple ways to interact with a page. Using a mouse is one of the most common, but there are others, like sighted people who use the keyboard to navigate because of a motor impairment.

Try to navigate the demo above using only the Tab key to go forward and Tab + Shift to go backward. You’ll notice how the disabled button is skipped. The focus goes directly from the ticket input to the “dummy terms” link.

Using the Tab key, it changes the focus from the input to the link, skipping the “Add to cart” button.

Let’s pause for a second and recap the reason that lead us to disable the button in the first place versus what we had actually accomplished.

It’s common to associate “interacting” with “clicking” but they are two different concepts. Yes, click is a type of interaction, but it’s only one among others, like hover and focus.

In other words…

All clicks are interactions, but not all interactions are clicks.

Our goal is to prevent the click, but by using disabled, we are preventing not only the click, but also the focus, which means we might be doing as much harm as good. Sure, this behavior might seem harmless, but it causes confusion. People with a cognitive disability may struggle to understand why they are unable to focus the button.

In the following demo, we tweaked the layout a little. If you use a mouse, and hover over the submit button, a tooltip is shown explaining why the button is disabled. That’s great! But if you use only the keyboard, there’s no way of seeing that tooltip because the button cannot be focused with disabled. Ouch!

Using the mouse, the tooltip on the “Add to cart” button is visible on hover. But the tooltip is missing when using the Tab key.

Allow me to once again skip past the code explanation. I highly recommend reading “Inclusive Tooltips” by Haydon Pickering and “Tooltips in the time of WCAG 2.1” by Sarah Higley to fully understand the tooltip pattern.

ARIA to the rescue

The disabled attribute in a <button> is doing more than necessary. This is one of the few cases I know of where a native HTML attribute can do more harm than good. Using an ARIA attribute can do a better job, allowing us to add not only focus on the button, but do so consistently to create an inclusive experience for more people and use cases.

The disabled attribute correlates to aria-disabled="true". Give the following demo a try, again, using only the keyboard. Notice how the button, although marked disabled, is still accessible by focus and triggers the tooltip!

Using the Tab key, the “Add to cart” button is focused and it shows the tooltip.

Cool, huh? Such tiny tweak for a big improvement!

But we’re not done quite yet. The caveat here is that we still need to prevent the click programmatically, using JavaScript.

elForm.addEventListener('submit', function (event) {   event.preventDefault(); /* prevent native form submit */    const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';    if (isDisabled || isSubmitting) {     // return early to prevent the ticket from being added     return;   }    isSubmitting = true;   // ... code to add to cart...   isSubmitting = false; })

You might be familiar with this pattern as a way to prevent double clicks from submitting a form twice. If you were using the disabled attribute for that reason, I’d prefer not to do it because that causes the temporary loss of the keyboard focus while the form is submitting.

The difference between disabled and aria-disabled

You might ask: if aria-disabled doesn’t prevent the click by default, what’s the point of using it? To answer that, we need to understand the difference between both attributes:

Feature / Attribute disabled aria-disabled="true"
Prevent click
Prevent hover
Prevent focus
Default CSS styles

The only overlap between the two is semantics. Both attributes will announce that the button is indeed disabled, and that’s a good thing.

Contrary to the disabled attribute, aria-disabled is all about semantics. ARIA attributes never change the application behavior or styles by default. Their only purpose is to help assistive technologies (e.g. screen readers) to announce the page content in a more meaningful and robust way.

So far, we’ve talked about two types of people, those who click and those who Tab. Now let’s talk about another type: those with visual impairments (e.g. blindness, low vision) who use screen readers to navigate the web.

People who use screen readers, often prefer to navigate form fields using the Tab key. Now look an how VoiceOver on macOS completely skips the disabled button.

The VoiceOver screen reader skips the “Add to cart” button when using the Tab key.

Once again, this is a very minimal form. In a longer one, looking for a submit button that isn’t there right away can be annoying. Imagine a form where the submit button is hidden and only visible when you completely fill out the form. That’s what some people feel like when the disabled attribute is used.

Fortunately, buttons with disabled are not totally unreachable by screen readers. You can still navigate each element individually, one by one, and eventually you’ll find the button.

The VoiceOver screen reader is able to find and announce the “Add to cart” button.

Although possible, this is an annoying experience. On the other hand, with aria-disabled, the screen reader will focus the button normally and properly announce its status. Note that the announcement is slightly different between screen readers. For example, NVDA and JWAS say “button, unavailable” but VoiceOver says “button, dimmed.”

The VoiceOver screen reader can find the “Add to cart” button using Tab key because of aria-disabled.

I’ve mapped out how both attributes create different user experiences based on the tools we just used:

Tool / Attribute disabled aria-disabled="true"
Mouse or tap Prevents a button click. Requires JS to prevent the click.
Tab Unable to focus the button. Able to focus the button.
Screen reader Button is difficult to locate. Button is easily located.

So, the main differences between both attributes are:

  • disabled might skip the button when using the Tab key, leading to confusion.
  • aria-disabled will still focus the button and announce that it exists, but that it isn’t enabled yet; the same way you might perceive it visually.

This is the case where it’s important to acknowledge the subtle difference between accessibility and usability. Accessibility is a measure of someone being able to use something. Usability is a measure of how easy something is to use.

Given that, is disabled accessible? Yes. Does it have a good usability? I don’t think so.

Can we do better?

I wouldn’t feel good with myself if I finished this article without showing you the real inclusive solution for our ticket form example. Whenever possible, don’t use disabled buttons. Let people click it at any time and, if necessary, show an error message as feedback. This approach also solves other problems:

  • Less cognitive friction: Allow people to submit the form at any time. This removes the uncertainty of whether the button is even disabled in the first place.
  • Color contrast: Although a disabled button doesn’t need to meet the WCAG 1.4.3 color contrast, I believe we should guarantee that an element is always properly visible regardless of its state. But that’s something we don’t have to worry about now because the button isn’t disabled anymore.

Final thoughts

The disabled attribute in <button> is a peculiar case where, although highly known by the community, it might not be the best approach to solve a particular problem. Don’t get me wrong because I’m not saying disabled is always bad. There are still some cases where it still makes sense to use it (e.g. pagination).

To be honest, I don’t see the disabled attribute exactly as an accessibility issue. What concerns me is more of a usability issue. By swapping the disabled attribute with aria-disabled, we can make someone’s experience much more enjoyable.

This is yet one more step into my journey on web accessibility. Over the years, I’ve discovered that accessibility is much more than complying with web standards. Dealing with user experiences is tricky and most situations require making trade-offs and compromises in how we approach a solution. There’s no silver bullet for perfect accessibility.

Our duty as web creators is to look for and understand the different solutions that are available. Only then we can make the best possible choice. There’s no sense in pretending the problems don’t exist.

At the end of the day, remember that there’s nothing preventing you from making the web a more inclusive place.


Still there? Let me mention two last things about this demo that I think are worthy:

1. Live Regions will announce dynamic content

In the demo, two parts of the content changed dynamically: the form submit button and the success confirmation (“Added [X] tickets!”).

These changes are visually perceived, however, for people with vision impairments using screen readers, that just ain’t the reality. To solve it, we need to turn those messages into Live Regions. Those allow assistive technologies to listen for changes and announce the updated messages when they happen.

There is a .sr-only class in the demo that hides a <span> containing a loading message, but allows it to be announced by screen readers. In this case, aria-live="assertive" is applied to the <span> and it holds a meaningful message after the form is submitting and is in the process of loading. This way, we can announce to the user that the form is working and to be patient as it loads. Additionally, we do the same to the form feedback element.

<button type="submit" aria-disabled="true">   Add to cart   <span aria-live="assertive" class="sr-only js-loadingMsg">      <!-- Use JavaScript to inject the the loading message -->   </span> </button>  <p aria-live="assertive" class="formStatus">   <!-- Use JavaScript to inject the success message --> </p>

Note that the aria-live attribute must be present in the DOM right from the beginning, even if the element doesn’t hold any message yet, otherwise, Assistive Technologies may not work properly.

Form submit feedback message being announced by the screen reader.

There’s much more to tell you about this little aria-live attribute and the big things it does. There are gotchas as well. For example, if it is applied incorrectly, the attribute can do more harm than good. It’s worth reading “Using aria-live” by Ire Aderinokun and Adrian Roselli’s “Loading Skeletons” to better understand how it works and how to use it.

2. Do not use pointer-events to prevent the click

This is an alternative (and incorrect) implementation that I’ve seen around the web. This uses pointer-events: none; in CSS to prevent the click (without any HTML attribute). Please, do not do this. Here’s an ugly Pen that will hopefully demonstrate why. I repeat, do not do this.

Although that CSS does indeed prevent a mouse click, remember that it won’t prevent focus and keyboard navigation, which can lead to unexpected outcomes or, even worse, bugs.

In other words, using this CSS rule as a strategy to prevent a click, is pointless (get it?). 😉

The post Making Disabled Buttons More Inclusive appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , ,

Use CSS Clamp to create a more flexible wrapper utility

I like Andy’s idea here:

.wrapper {   width: clamp(16rem, 90vw, 70rem);   margin-left: auto;   margin-right: auto;   padding-left: 1.5rem;   padding-right: 1.5rem; }

Normally I’d just set a max-width there, but as Andy says:

This becomes a slight issue in mid-sized viewports, such as tablets in portrait mode, in long-form content, such as this article because contextually, the line-lengths feel very long.

So, on super large screens, you’ll get capped at 70rem (or whatever you think a good maximum is), and on small screens you’ll get full width, which is fine. But it’s those in-betweens that aren’t so great. I made a little demo to get a feel for it. This video makes it clear I think:

Direct Link to ArticlePermalink

The post Use CSS Clamp to create a more flexible wrapper utility appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,