Month: February 2022

Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think

We’ve discussed a lot about the internals of using CSS in this ongoing series on web components, but there are a few special pseudo-elements and pseudo-classes that, like good friends, willingly smell your possibly halitotic breath before you go talk to that potential love interest. You know, they help you out when you need it most. And, like a good friend will hand you a breath mint, these pseudo-elements and pseudo-classes provide you with some solutions both from within the web component and from outside the web component — the website where the web component lives.

I’m specifically referring to the ::part and ::slotted pseudo-elements, and the :defined, :host, and :host-context pseudo-classes. They give us extra ways to interact with web components. Let’s examine them closer.

Article series

The ::part pseudo-element

::part, in short, allows you to pierce the shadow tree, which is just my Lord-of-the-Rings-y way to say it lets you style elements inside the shadow DOM from outside the shadow DOM. In theory, you should encapsulate all of your styles for the shadow DOM within the shadow DOM, i.e. within a <style> element in your <template> element.

So, given something like this from the very first part of this series, where you have an <h2> in your <template>, your styles for that <h2> should all be in the <style> element.

<template id="zprofiletemplate">   <style>     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     /* other styles */   </style>   <div class="profile-wrapper">     <div class="info">       <h2>         <slot name="zombie-name">Zombie Bob</slot>       </h2>       <!-- other zombie profile info -->     </div> </template>

But sometimes we might need to style an element in the shadow DOM based on information that exists on the page. For instance, let’s say we have a page for each zombie in the undying love system with matches. We could add a class to profiles based on how close of a match they are. We could then, for instance, highlight a match’s name if he/she/it is a good match. The closeness of a match would vary based on whose list of potential matches is being shown and we won’t know that information until we’re on that page, so we can’t bake the functionality into the web component. Since the <h2> is in the shadow DOM, though, we can’t access or style it from outside the shadow DOM meaning a selector of zombie-profile h2 on the matches page won’t work.

But, if we make a slight adjustment to the <template> markup by adding a part attribute to the <h2>:

<template id="zprofiletemplate">   <style>     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     /* other styles */   </style>   <div class="profile-wrapper">     <div class="info">       <h2 part="zname">         <slot name="zombie-name">Zombie Bob</slot>       </h2>       <!-- other zombie profile info -->     </div> </template>

Like a spray of Bianca in the mouth, we now have the superpowers to break through the shadow DOM barrier and style those elements from outside of the <template>:

/* External stylesheet */ .high-match::part(zname) {   color: blue; } .medium-match::part(zname) {   color: navy; } .low-match::part(zname) {   color: slategray; }

There are lots of things to consider when it comes to using CSS ::part. For example, styling an element inside of a part is a no-go:

/* frowny-face emoji */ .high-match::part(zname) span { ... }

But you can add a part attribute on that element and style it via its own part name.

What happens if we have a web component inside another web component, though? Will ::part still work? If the web component appears in the page’s markup, i.e. you’re slotting it in, ::part works just fine from the main page’s CSS.

<zombie-profile class="high-match">   <img slot="profile-image" src="" />   <span slot="zombie-name">Leroy</span>   <zombie-details slot="zdetails">     <!-- Leroy's details -->   </zombie-details> </zombie-profile>

But if the web component is in the template/shadow DOM, then ::part cannot pierce both shadow trees, just the first one. We need to bring the ::part into the light… so to speak. We can do that with an exportparts attribute.

To demonstrate this we’ll add a “watermark” behind the profiles using a web component. (Why? Believe it or not this was the least contrived example I could come up with.) Here are our templates: (1) the template for <zombie-watermark>, and (2) the same template for <zombie-profile> but with added a <zombie-watermark> element on the end.

<template id="zwatermarktemplate">   <style>     div {     text-transform: uppercase;       font-size: 2.1em;       color: rgb(0 0 0 / 0.1);       line-height: 0.75;       letter-spacing: -5px;     }     span {       color: rgb( 255 0 0 / 0.15);     }   </style>   <div part="watermark">     U n d y i n g  L o v e  U n d y i n g  L o v e  U n d y i n g  L o v e  <span part="copyright">©2 0 2 7 U n d y i n g  L o v e  U n L t d .</span>   <!-- Repeat this a bunch of times so we can cover the background of the profile -->   </div>  </template> <template id="zprofiletemplate">   <style>     ::part(watermark) {       color: rgb( 0 0 255 / 0.1);     }     /* More styles */   </style>   <!-- zombie-profile markup -->   <zombie-watermark exportparts="copyright"></zombie-watermark> </template> <style>   /* External styles */   ::part(copyright) {     color: rgb( 0 100 0 / 0.125);   } </style>

Since ::part(watermark) is only one shadow DOM above the <zombie-watermark>, it works fine from within the <zombie-profile>’s template styles. Also, since we’ve used exportparts="copyright" on the <zombie-watermark>, the copyright part has been pushed up into the <zombie-profile>‘s shadow DOM and ::part(copyright) now works even in external styles, but ::part(watermark) will not work outside the <zombie-profile>’s template.

We can also forward and rename parts with that attribute:

<zombie-watermark exportparts="copyright: cpyear"></zombie-watermark>
/* Within zombie-profile's shadow DOM */  /* happy-face emoji */ ::part(cpyear) { ... }  /* frowny-face emoji */ ::part(copyright) { ... }

Structural pseudo-classes (:nth-child, etc.) don’t work on parts either, but you can use pseudo-classes like :hover. Let’s animate the high match names a little and make them shake as they’re lookin’ for some lovin’. Okay, I heard that and agree it’s awkward. Let’s… uh… make them more, shall we say, noticeable, with a little movement.

.high::part(name):hover {   animation: highmatch 1s ease-in-out; }

The ::slotted pseudo-element

The ::slotted CSS pseudo-element actually came up when we covered interactive web components. The basic idea is that ::slotted represents any content in a slot in a web component, i.e. the element that has the slot attribute on it. But, where ::part pierces through the shadow DOM to make a web component’s elements accessible to outside styles, ::slotted remains encapsulated in the <style> element in the component’s <template> and accesses the element that’s technically outside the shadow DOM.

In our <zombie-profile> component, for example, each profile image is inserted into the element through the slot="profile-image".

<zombie-profile>   <img slot="profile-image" src="photo.jpg" />    <!-- rest of the content --> </zombie-profile>

That means we can access that image — as well as any image in any other slot — like this:

::slotted(img) {   width: 100%;   max-width: 300px;   height: auto;   margin: 0 1em 0 0; }

Similarly, we could select all slots with ::slotted(*) regardless of what element it is. Just beware that ::slotted has to select an element — text nodes are immune to ::slotted zombie styles. And children of the element in the slot are inaccessible.

The :defined pseudo-class

:defined matches all defined elements (I know, surprising, right?), both built-in and custom. If your custom element is shuffling along like a zombie avoiding his girlfriend’s dad’s questions about his “living” situation, you may not want the corpses of the content to show while you’re waiting for them to come back to life errr… load.

You can use the :defined pseudo-class to hide a web component before it’s available — or “defined” — like this:

:not(:defined) {   display: none; }

You can see how :defined acts as a sort of mint in the mouth of our component styles, preventing any broken content from showing (or bad breath from leaking) while the page is still loading. Once the element’s defined, it’ll automatically appear because it’s now, you know, defined and not not defined.

I added a setTimeout of five seconds to the web component in the following demo. That way, you can see that <zombie-profile> elements are not shown while they are undefined. The <h1> and the <div> that holds the <zombie-profile> components are still there. It’s just the <zombie-profile> web component that gets display: none since they are not yet defined.

The :host pseudo-class

Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.

It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host.

In previous examples throughout this series, I set the <zombie-profile> width from the main page’s CSS, like this:

zombie-profile {   width: calc(50% - 1em); }

With :host, however, I can set that width from inside the web component, like this:

:host {   width: calc(50% - 1em); }

In fact, there was a div with a class of .profile-wrapper in my examples that I can now remove because I can use the shadow root as my wrapper with :host. That’s a nice way to slim down the markup.

You can do descendant selectors from the :host, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted).

Showing the parts of the HTML that are relevant to the :host pseudo-element.

That said, :host isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.

:host(.high) {   border: 2px solid blue; }

This allows you to make changes should certain classes be added to the custom element.

You can also pass pseudo-classes in there, like :host(:last-child) and :host(:hover).

The :host-context pseudo-class

Now let’s talk about :host-context. It’s like our friend :host(), but on steroids. While :host gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.

:host-context, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context is unsupported in Firefox or Safari. So use it for progressive enhancement.

Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch class. The second div will hold all the medium and low love matches with a .worstmatch class.

<div class="profiles bestmatch">   <zombie-profile class="high">     <!-- etc. -->   </zombie-profile>   <!-- more profiles --> </div>  <div class="profiles worstmatch">   <zombie-profile class="medium">     <!-- etc. -->   </zombie-profile>   <zombie-profile class="low">     <!-- etc. -->   </zombie-profile>   <!-- more profiles --> </div>

Let’s say we want to apply different background colors to the .bestmatch and .worstmatch classes. We are unable to do this with just :host:

:host(.bestmatch) {   background-color: #eef; } :host(.worstmatch) {   background-color: #ddd; }

That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context pokes past the custom element to match the, er, match classes we want to style.

:host-context(.bestmatch) {   background-color: #eef; } :host-context(.worstmatch) {   background-color: #ddd; }

Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)

How would you use ::part, ::slotted, :defined, :host, and :host-context in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)

Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter.


, , , , ,

Trailing Slashes on URLs: Contentious or Settled?

A fun deep dive from Zach. Do you have an opinion on which you should use?

1) 2)

The first option has a “trailing slash.” The second does not.

I’ve always preferred this thinking: you use a trailing slash if that page has child pages (as in, it is something of a directory page, even if has unique content of its own). If it’s the end-of-the-line (of content), no trailing slash.

I say that, but this very site doesn’t practice it. Blog posts on this site are like with a trailing slash and if you leave off the trailing slash, WordPress will redirect to include it. That’s part of the reason Zach is interested here. Redirects come with a performance penalty, so it’s ideal to have it happen as infrequently possible.

Performance is one thing, but SEO is another one. If you render the same content, both with and without a trailing slash, that’s theoretically a duplicate content penalty and a no-no. (Although that seems weird to me, I would think Google would smart enough not to be terribly concerned by this.)

Where resources resolve to seems like the biggest deal to me. Here’s Zach:

If you’re using relative resource URLs, the assets may be missing on Vercel, Render, and Azure Static Web Apps (depending on which duplicated endpoint you’ve visited).

<img src="image.avif"> on /resource/ resolves to /resource/image.avif

<img src="image.avif"> on /resource resolves to /image.avif

That’s a non-trivial difference and, to me, a reason the redirect is worth it. Can’t be having a page with broken resources for something this silly.

What complicates this is that the site-building framework might have opinions about this and a hosting provider might have opinions about this. As Zach notes, there are some disagreements among hosts, so it’s something to watch for.

Me, I’d go with the grain as much as I possibly could. As long as redirects are in place and I don’t have to override any config, I’m cool.

To Shared LinkPermalink on CSS-Tricks

Trailing Slashes on URLs: Contentious or Settled? originally published on CSS-Tricks. You should get the newsletter.


, , , ,

Manuel Matuzovic’s CSS Specificity Demo

If you’re looking for a primer on CSS specificity, we’ve got that. And if you’re trying to get ahead of the game, you should be aware of CSS Cascade Layers as well.

Screenshot of the CSS Specificity Demo.

One of the ways to help get a grasp of CSS specificity is thinking terms of “what beats what” or how strong the specificity is. Manuel Matuzovic has a helpful interactive step-by-step demo. You keep clicking the “Add selector” button, and the CSS shown (and applied to the page) changes with ever-increasingly-strong selectors applied to the body that change the background-color. At the end, it veers into not-really-selectors trickery, like using @keyframes to override things.

More specificity practice

If you enjoyed the trickery at the end, check out Francisco Dias’ A Specificity Battle!, an article we published a few years back that does a back-and-forth styling battle with nineteen steps “selecting” the same element to re-style it. CSS is cray sometimes.

To Shared LinkPermalink on CSS-Tricks

Manuel Matuzovic’s CSS Specificity Demo originally published on CSS-Tricks. You should get the newsletter.


, , ,

When to Avoid the text-decoration Shorthand Property

In my recent article about CSS underline bugs in Chrome, I discussed text-decoration-thickness and text-underline-offset, two relatively new and widely-supported CSS properties that give us more control over the styling of underlines.

Let me demonstrate the usefulness of text-decoration-thickness on a simple example. The Ubuntu web font has a fairly thick default underline. We can make this underline thinner like so:

:any-link {   text-decoration-thickness: 0.08em; }
Showing two links, a default and one that decreases the text-decoration-thickness.

/explanation Throughout this article, I will use the :any-link selector instead of the a element to match hyperlinks. The problem with the a tag as a selector is that it matches all <a> elements, even the ones that don’t have a href attribute and thus aren’t hyperlinks. The :any-link selector only matches <a> elements that are hyperlinks. Web browsers also use :any-link instead of a in their user agent stylesheets.

Hover underlines

Many websites, including Google Search and Wikipedia, remove underlines from links and only show them when the user hovers a link. Removing underlines from links in body text is not a good idea, but it can make sense in places where links are more spaced apart (navigation, footer, etc.). With that being said, here’s a simple implementation of hover underlines for links in the website’s header:

header :any-link {   text-decoration: none; }  header :any-link:hover {   text-decoration: underline; }

But there’s a problem. If we tested this code in a browser, we’d notice that the underlines in the header have the default thickness, not the thinner style that we declared earlier. Why did text-decoration-thickness stop working after we added hover underlines?

Let’s look at the full CSS code again. Can you think of a reason why the custom thickness doesn’t apply to the hover underline?

:any-link {   text-decoration-thickness: 0.08em; }  header :any-link {   text-decoration: none; }  header :any-link:hover {   text-decoration: underline; }

The reason for this behavior is that text-decoration is a shorthand property and text-decoration-thickness its associated longhand property. Setting text-decoration to none or underline has the side effect of re-initializing the other three text decoration components (thickness, style, and color). This is defined in the CSS Text Decoration module:

The text-decoration property is a shorthand for setting text-decoration-line, text-decoration-thickness, text-decoration-style, and text-decoration-color in one declaration. Omitted values are set to their initial values.

You can confirm this in the browser’s DevTools by selecting one of the hyperlinks in the DOM inspector and then expanding the text-decoration property in the CSS pane.

DevTools screenshot showing text-decoration styles on the :any-link pseudo-selector.

In order to get text-decoration-thickness to work on hover underlines, we’ll have to make a small change to the above CSS code. There are actually multiple ways to achieve this. We could:

  • set text-decoration-thickness after text-decoration,
  • declare the thickness in the text-decoration shorthand, or
  • use text-decoration-line instead of text-decoration.

Choosing the best text-decoration option

Our first thought might be to simply repeat the text-decoration-thickness declaration in the :hover state. It’s a quick and simple fix that indeed works.

/* OPTION A */  header :any-link {   text-decoration: none; }  header :any-link:hover {   text-decoration: underline;   text-decoration-thickness: 0.08em; /* set thickness again */ }

However, since text-decoration is a shorthand and text-decoration-thickness is its associated longhand, there really should be no need to use both at the same time. As a shorthand, text-decoration allows setting both the underline itself and the underline’s thickness, all in one declaration.

/* OPTION B */  header :any-link {   text-decoration: none; }  header :any-link:hover {   text-decoration: underline 0.08em; /* set both line and thickness */ }

If this code looks unfamiliar to you, that could be because the idea of using text-decoration as a shorthand is relatively new. This property was only subsequently turned into a shorthand in the CSS Text Decoration module. In the days of CSS 2, text-decoration was a simple property.

Unfortunately, Safari still hasn’t fully caught up with these changes. In the WebKit browser engine, the shorthand variant of text-decoration remains prefixed (-webkit-text-decoration), and it doesn’t support thickness values yet. See WebKit bug 230083 for more information.

This rules out the text-decoration shorthand syntax. The above code won’t work in Safari, even if we added the -webkit- prefix. Luckily, there’s another way to avoid repeating the text-decoration-thickness declaration.

When text-decoration was turned into a shorthand, a new text-decoration-line longhand was introduced to take over its old job. We can use this property to hide and show the underline without affecting the other three text decoration components.

/* OPTION C */  header :any-link {   text-decoration-line: none; }  header :any-link:hover {   text-decoration-line: underline; }

Since we’re only updating the line component of the text-decoration value, the previously declared thickness remains intact. I think that this is the best way to implement hover underlines.

Be aware of shorthands

Keep in mind that when you set a shorthand property, e.g., text-decoration: underline, any missing parts in the value are re-initialized. This is also why styles such as background-repeat: no-repeat are undone if you set background: url(flower.jpg) afterwards. See the article “Accidental CSS Resets” for more examples of this behavior.

When to Avoid the text-decoration Shorthand Property originally published on CSS-Tricks. You should get the newsletter.


, , ,

My white whale: A use case for will-change

 Nic Chan:

[…] the will-change property landed in major browsers in August 2015, and I’ve been on the lookout for when to use it ever since. It might seem self-evident to apply it to commonly animated properties such as transform or opacity, but the browser already classifies them as composite properties, thus, they are known as the few properties that you can already expect decent animation performance from. So, heeding the advice of the great developers who came before me, I was cautious and waited for the right opportunity to come along.

I was thinking-out-loud about this as well on ShopTalk not too long ago. I get the spirit behind will-change. It’s like responsive images or DNS prefetching: you give the browser extra information about what you’re about to do, and it can optimize it when it happens. But with will-changewhen? Why isn’t there a simple reduced test case demo to showcase something with bad performance, then will-change being applied, and it becomes good performance?

Well Nic found one little directly useful case where a hover-transformed pseudo-element leaves a little dingus of color behind in Safari, and that goes away if you use will-change. I tested it in the latest versions of Safari and found it to be true. Alrighty then, one use case!

I’d love to see a more obvious direct use case. I imagine the sweet spot is on lower-power devices (that still have GPUs) but are new enough to know what will-change is.

To Shared LinkPermalink on CSS-Tricks

My white whale: A use case for will-change originally published on CSS-Tricks. You should get the newsletter.


, , ,

Superior Image Optimization: An Ideal Solution Using Gatsby & ImageEngine

(This is a sponsored post.)

In recent years, the Jamstack methodology for building websites has become increasingly popular. Performance, scalable, and secure, it’s easy to see why it’s becoming an attractive way to build websites for developers.

GatsbyJS is a static site generator platform. It’s powered by React, a front-end JavaScript library, for building user interfaces. And uses GraphQL, an open-source data query and manipulation language, to pull structured data from other sources, typically a headless CMS like Contentful.

While GatsbyJS and similar platforms have revolutionized much about the web development process, one stubborn challenge remains: image optimization. Even using a modern front-end development framework like GatsbyJS, it tends to be a time-intensive and frustrating exercise.

For most modern websites, it doesn’t help much if you run on a performant technology but your images aren’t optimized. Today, images are the largest contributor to page weight, and growing, and have been singled out by Google as presenting the most significant opportunity for improving performance.

With that in mind, I want to discuss how using an image CDN as part of your technology stack can bring improvements both in terms of website performance and the entire development process.

A Quick Introduction to Gatsby

GatsbyJS is so much more than the conventional static site generators of old. Yes, you still have the ability to integrate with a software version control platform, like Git, as well as to build, deploy, and preview Gatsby projects. However, its services consist of a unified cloud platform that includes high-speed, scalable, and secure hosting as well as expert technical support and powerful third-party integrations.

What’s more, all of it comes wrapped in a user-friendly development platform that shares many similarities with the most popular CMSs of the day. For example, you can leverage pre-designed site templates or pre-configured functions (effectively website elements and modules) to speed up the production process.

It also offers many benefits for developers by allowing you to work with leading frameworks and languages, like JavaScript, React, WebPack, and GraphQL as well as baked-in capabilities to deal with performance, development iterations, etc.

For example, Gatsby does a lot to optimize your performance without any intervention. It comes with built-in code-splitting, prefetching resources, and lazy-loading. Static sites are generally known for being inherently performant, but Gatsby kicks it up a notch.

Does Gatsby Provide Built-in Image Optimization?

Gatsby does, in fact, offer built-in image optimization capabilities.

In fact, it recently upgraded in this regard, replacing the now deprecated gatsby-image package with the brand-new Gatsby image plugin. This plugin consists of two components for static and dynamic images, respectively. Typically, you would use the dynamic component if you’re handling images from a CMS, like Contentful.

Installing this plugin allows you to programmatically pass commands to the underlying framework in the form of properties, shown below:

Option Default Description
layout constrained / CONSTRAINED Determines the size of the image and its resizing behavior.
width/height Source image size Change the size of the image.
aspectRatio Source image aspect ratio Force a specific ratio between the image’s width and height.
placeholder "dominantColor" / DOMINANT_COLOR Choose the style of temporary image shown while the full image loads.
formats ["auto", "webp"] / [AUTO,WEBP] File formats of the images generated.
transformOptions [fit: "cover", cropFocus: "attention"] Options to pass to sharp to control cropping and other image manipulations.
sizes Generated automatically The <img> sizes attribute, passed to the img tag. This describes the display size of the image, and does not affect generated images. You are only likely to change this if you are using full width images that do not span the full width of the screen.
quality 50 The default image quality generated. This is override by any format-specific option.
outputPixelDensities For fixed images: [1, 2]

For constrained: [0.25, 0.5, 1, 2]

A list of image pixel densities to generate. It will never generate images larger than the source, and will always include a 1✕ image. The image is multiple by the image width, to give the generated sizes. For example, a 400px wide constrained image would generate 100, 200, 400 and 800px wide images by default. Ignored for full width layout images, which use breakpoints instead.
breakpoints [750, 1000, 1366, 1920] Output widths to generate for full width images. Default is to generate widths for common device resolutions. It will never generate an image larger than the source image. The browser will automatically choose the most appropriate.
blurredOptions None Options for the low-resolution placeholder image. Ignored unless placeholder is blurred.
tracedSVGOptions None Options for traced placeholder SVGs. See potrace options. Ignored unless placeholder is traced SVG.
jpgOptions None Options to pass to sharp when generating JPG images.

As you can see, that’s quite the toolkit to help developers process images in a variety of ways. The various options can be used to transform, style, and optimize images for performance as well as make images behave dynamically in a number of ways.

In terms of performance optimization, there are a few options that are particularly interesting:

  • Lazy-loading: Defers loading of off-screen images until they are scrolled into view.
  • Width/height: Resize image dimensions according to how they will be used.
  • Placeholder: When lazy-loading or while an image is loading in the background, use a placeholder. This can help to avoid performance penalties for core web vitals, like Cumulative Layout Shift (CLS).
  • Format: Different formats have inherently more efficient encoding. GatsbyJS supports WebP and AVIF, two of the most performant next-gen image formats.
  • Quality: Apply a specified level of quality compression to the image between 0 and 100.
  • Pixel density: A lower pixel density will save image data and can be optimized according to the screen size and PPI (pixels per inch).
  • Breakpoints: Breakpoints are important for ensuring that you serve a version of an image that’s sized appropriately for a certain threshold of screen sizes, especially that you serve smaller images for smaller screen sizes, like tablets or mobile phones. This is called responsive syntax.

So, all in all, Gatsby provides developers with a mature and sophisticated framework to process and optimize image content. The only important missing feature that’s missing is some type of built-in support for client hints.

However, there is one big catch: All of this has to be implemented manually. While GatsbyJS does use default settings for some image properties, it doesn’t offer built-in intelligence to automatically and dynamically process and serve optimized images tailored to the accessing device.

If you want to create an ideal image optimization solution, your developers will firstly have to implement device detection capabilities. They will then need to develop the logic to dynamically select optimization operations based on the specific device accessing your web app.

Finally, this code will continually need to be changed and updated. New devices come out all the time with differing properties. What’s more, standards regarding performance as well as image optimization are continually evolving. Even significant changes, additions, or updates to your own image assets may trigger the need to rework your implementation. Not to mention the time it takes to simply stay abreast of the latest information and trends and to make sure development is carried out accordingly.

Another problem is that you will need to continually test and refine your implementation. Without the help of an intelligent optimization engine, you will need to “feel out” how your settings will affect the visual quality of your images and continually fine-tune your approach to get the right results.

This will add a considerable amount of overhead to your development workload in the immediate and long term.

Gatsby also admits that these techniques are quite CPU intensive. In that case, you might want to preoptimize images. However, this also needs to be manually implemented in-code on top of being even less dynamic and flexible.

But, what if there was a better way to optimize your image assets while still enjoying all the benefits of using a platform like Gatsby? The solution I’m about to propose will help solve a number of key issues that arise from using Gatsby (and any development framework, for that matter) for the majority of your image optimization:

  • Reduce the impact optimizing images have on the development and design process in the immediate and long term.
  • Remove an additional burden and responsibility from your developers’ shoulders, freeing up time and resources to work on the primary aspects of your web app.
  • Improve your web app’s ability to dynamically and intelligently optimize image assets for each unique visitor.
  • All of this, while still integrating seamlessly with GatsbyJS as well as your CMS (in most cases).

Introducing a Better Way to Optimize Image Assets: ImageEngine

In short, ImageEngine is an intelligent, device-aware image CDN.

ImageEngine works just like any other CDN (content delivery network), such as Fastly, Akamai, or Cloudflare. However, it specializes in optimizing and serving image content specifically. 

Like its counterparts, you provide ImageEngine with the location where your image files are stored, it pulls them to its own image optimization servers, and then generates and serves optimized variants of images to your site visitors.

In doing this, ImageEngine is designed to decrease image payload, deliver optimized images tailored to each unique device, and serve images from edge nodes across its global CDN

Basically, image CDNs gather information on the accessing device by analyzing the ACCEPT header. A typical ACCEPT header looks like this (for Chrome):


As you can see, this only provides the CDN with the accepted image formats and the recommended quality compression.

More advanced CDNs, ImageEngine, included, can also leverage client hints for more in-depth data points, such as the DPR (device pixel ratio) and Viewport-Width. This allows a larger degree of intelligent decision-making to more effectively optimize image assets while preserving visual quality.

However, ImageEngine takes things another step further by being the only mainstream image CDN that has built-in WURFL device detection. This gives ImageEngine the ability to read more information on the device, such as the operating system, resolution, and PPI (pixels per inch).

Using AI and machine-learning algorithms, this extra data means ImageEngine has virtually unparalleled decision-making power. Without any manual intervention, ImageEngine can perform all of the following image optimization operations automatically:

  • Resize your images according to the device screen size without the need for responsive syntax.
  • Intelligently compress the quality of the image to reduce the payload while preserving visual quality, using metrics like the Structural Similarity Index Method (SSIM).
  • Convert images to the most optimal, next-gen encoding formats. On top of WebP and AVIF, ImagEngine also supports JPEG 2000 (Safari), JPEG XR (Internet Explorer & Edge), and MP4 (for aGIFs).

These settings also play well with GatsbyJS’ built-in capabilities. So, you can natively implement breakpoints, lazy-loading, and image placeholders that don’t require any expertise or intelligent decision-making using Gatsby. Then, you can let ImageEngine handle the more advanced and intelligence-driven operations, like quality compression, image formatting, and resizing.

The best part is that ImageEngine does all of this automatically, making it a completely hands-off image optimization solution. ImageEngine will automatically adjust its approach with time as the digital image landscape and standards change, freeing you from this concern.

In fact, ImageEngine recommends using default settings for getting the best results in most situations.

What’s more, this logic is built into the ImageEngine edge servers. Firstly, with over 20 global PoPs, it means that the images are processed and served as close to the end-user as possible. It also means that the majority of processing happens server-side. With the exception of installing the ImageEngine Gatsby plugin, there is virtually no processing overhead at build or runtime.

This type of dynamic, intelligent decision-making will only become more important in the near and medium-term. Thanks to the number and variety of devices growing by the year, it’s becoming harder and harder to implement image optimization in a way that’s sensitive to every device.

That’s why ImageEngine can give you the edge in a mobile-first future that’s continually evolving. Simply put, ImageEngine will help futureproof your Gatsby web app.

How to Integrate ImageEngine with Gatsby: A Quick Guide

Integrating ImageEngine with GatsbyJS is trivial if you have experience installing any other third-party plugins. However, the steps will differ somewhat based on which backend CMS you use with GatsbyJS and where you store your image assets.

For example, you could use it alongside WordPress, Drupal, Contentful, and a range of other popular CMSs.

Usually, your stack would look something like this:

  • A CMS, like Contentful, to host your “space” where you’ll manage your assets and create structured data. Your images will be uploaded and stored in your space.
  • A versioning platform, like Github, to host your code and manage your versions and branches.
  • GatsbyJS to host your workspace, where you’ll build, deploy, and host the front end of your website.

So, the first thing you need to do is set up a site, or project, using GatsbyJS and link it to your CMS.

Next, you’ll install the ImageEngine plugin for GatsbyJS:

npm install @imageengine/gatsby-plugin-imageengine

You’ll also need to create a delivery address for your images via ImageEngine. You can get one by signing up for the 30-day trial here. The only thing you need to do is supply ImageEngine with the host origin URL. For Contentful, it’s and for, it’s

ImageEngine will then provide you with a delivery address, usually in the format of {random_string}

You’ll use this delivery address to configure the ImageEngine plugin in your gatsby-config.js file. As part of this, you’ll indicate the source (Contentful, e.g.) as well as provide the ImageEngine delivery address. You can find examples of how that’s done in the documentation here.

Note that the ImageEngine plugin features built-in support for Contentful and as asset sources. You can also configure the plugin to pull locally stored images or from another custom source.

Once that’s done, development can begin!

Basically, Gatsby will create Graphql Nodes for the elements created in your CMS (e.g., ContentfulAsset, allSanityImageAsset, etc.). ImageEngine will then create a child node of childImageEngineAsset for each applicable element node.

You’ll then use GraphQL queries in the code for your Gatsby pages to specify the properties of the image variants you want to serve. For example, you can display an image that’s 500 ✕ 300px in the WebP format using the following query:

gatsbyImageData(width: 500, height: 300, format: jpg)

Once again, you should refer to the documentation for a more thorough treatment. You can find guides for integrating ImageEngine with Contentful,, and any other Gatsby project.

For a competent Gatsby user, integrating ImageEngine will only take a few minutes. And, ongoing maintenance will be minimal. If you know how to use GraphQL, then the familiar syntax to send directives and create specific image variants will be nearly effortless and should take about the same time as manually optimizing images using standard Gatsby React.


For most web projects, ImageEngine can reduce image payloads by up to 80%. That number can go up if you have especially high-res images.

However, you can really get the most out of your image optimization by combining the best parts of a static front-end development framework like Gatsby and an image CDN like ImageEngine. Specifically, you can use both to target Google’s core web vitals:

  • ImageEngine’s dynamic, intelligent, run-time optimization will optimize payloads to improve LCP, SI, FCP, and other data size-related metrics.
  • Using Gatsby, you can optimize for CLS and FID using best practices and by natively implementing lazy loading and image placeholders.

ImageEngine provides an Image Speed Test tool where you can quickly evaluate your current performance and see the impact of ImageEngine on key metrics. Even for a simple GatsbyJS project, the results in the Demo tool can be impressive. If you extrapolate those percentages for a larger, image-heavy site, combining Gatsby with ImageEngine could have a dramatic impact on the performance and user experience of your web app. What’s more, your developers will thank you for sparing them from the challenging and time-consuming chore of manual image optimization.

Superior Image Optimization: An Ideal Solution Using Gatsby & ImageEngine originally published on CSS-Tricks. You should get the newsletter.


, , , , , , ,

Explain the First 10 Lines of Twitter’s Source Code to Me

For the past few weeks, I’ve been hiring for a senior full-stack JavaScript engineer at my rental furniture company, Pabio. Since we’re a remote team, we conduct our interviews on Zoom, and I’ve observed that some developers are not great at live-coding or whiteboard interviews, even if they’re good at the job. So, instead, we have an hour-long technical discussion where I ask them questions about web vitals, accessibility, the browser wars, and other similar topics about the web. One of the questions I always like to ask is: “Explain the first ten or so lines of the Twitter source code to me.”

I think it’s a simple test that tells me a lot about the depth of fundamental front-end knowledge they have, and this article lists the best answers.

For context, I share my screen, open and click View source. Then I ask them to go line-by-line to help me understand the HTML, and they can say as much or as little as they like. I also zoom in to make the text more legible, so you don’t see the full line but you get an idea. Here’s what it looks like:

Screenshot of source code from Twitter.

Note that since our technical discussion is a conversation. I don’t expect a perfect answer from anyone. If I hear some right keywords, I know that the candidate knows the concept, and I try to push them in the right direction.

Line 1: <!DOCTYPE html>

The first line of every document’s source code is the perfect for this interview because how much a candidate knows about the DOCTYPE declaration closely resembles how many years of experience they have. I still remember my Dreamweaver days with the long XHTML DOCTYPE line, like Chris listed in his article “The Common DOCTYPES” from 2009.

Perfect answer: This is the document type (doc-type) declaration that we always put as the first line in HTML files. You might think that this information is redundant because the browser already knows that the MIME type of the response is text/html; but in the Netscape/Internet Explorer days, browsers had the difficult task of figuring out which HTML standard to use to render the page from multiple competing versions.

This was especially annoying because each standard generated a different layout so this tag was adopted to make it easy for browsers. Previously, DOCTYPE tags were long and even included the specification link (kinda like SVGs have today), but luckily the simple <!doctype html> was standardized in HTML5 and still lives on.

Also accepted: This is the DOCTYPE tag to let the browser know that this is an HTML5 page and should be rendered as such.

Line 2: <html dir="ltr" lang="en">

This line in the source code tells me if the candidate knows about accessibility and localization. Surprisingly, only a few people knew about the dir attribute in my interviews, but it’s a great segue into a discussion about screen readers. Almost everyone was able to figure out the lang="en" attribute, even if they haven’t used it before.

Perfect answer: This is the root element of an HTML document and all other elements are inside this one. Here, it has two attributes, direction and language. The direction attribute has the value left-to-right to tell user agents which direction the content is in; other values are right-to-left for languages like Arabic, or just auto which leaves it to the browser to figure out.

The language attribute tells us that all content inside this tag is in English; you can set this value to any language tag, even to differentiate en-us and en-gb, for example. This is also useful for screen readers to know which language to announce in.

Line 3: <meta charset="utf-8">

Perfect answer: The meta tag in the source code is for supplying metadata about this document. The character set (char-set) attribute tells the browser which character encoding to use, and Twitter uses the standard UTF-8 encoding. UTF-8 is great because it has many character points so you can use all sorts of symbols and emoji in your source code. It’s important to put this tag near the beginning of your code so the browser hasn’t already started parsing too much text when it comes across this line; I think the rule is to put it in the first kilobyte of the document, but I’d say the best practice is to put it right at the top of <head>.

As a side note, it looks like Twitter omits the <head> tag for performance reasons (less code to load), but I still like to make it explicit as it’s a clear home for all metadata, styles, etc.

Line 4: <meta name="viewport" content="width=device-...

Perfect answer: This meta tag in the source code is for properly sizing the webpage on small screens, like smartphones. If you remember the original iPhone keynote, Steve Jobs showed the entire New York Times website on that tiny 4.5-inch screen; back then it was an amazing feature that you had to pinch to zoom to actually be able to read.

Now that websites are responsive by design, width=device-width tells the browser to use 100% of the device’s width as the viewport so there’s no horizontal scrolling, but you can even specify specific pixel values for width. The standard best practice is to set the initial scale to 1 and the width to device-width so people can still zoom around if they wish.

The screenshot of the source code doesn’t show these values but it’s good to know: Twitter also applies user-scalable=0 which, as the name suggests, disables the ability to zoom. This is not good for accessibility but makes the webpage feel more like a native app. It also sets maximum-scale=1 for the same reason (you can use minimum and maximum scale to clamp the zoom-ablity between these values). In general, setting the full width and initial scale is enough.

Line 5: <meta property="og:site_name" content="Twitt...

About 50% of all candidates knew about Open Graph tags, and a good answer to this question shows that they know about SEO.

Perfect answer: This tag is an Open Graph (OG) meta tag for the site name, Twitter. The Open Graph protocol was made by Facebook to make it easier to unfurl links and show their previews in a nice card layout; developers can add all sorts of authorship details and cover images for fancy sharing. In fact, these days it’s even common to auto-generate the open graph image using something like Puppeteer. (CSS-Tricks uses a WordPress plugin that does it.)

Another interesting side note is that meta tags usually have the name attribute, but OG uses the non-standard property attribute. I guess that’s just Facebook being Facebook? The title, URL, and description Open Graph tags are kinda redundant because we already have regular meta tags for these, but people add them just to be safe. Most sites these days use a combination of Open Graph and other metatags and the content on a page to generate rich previews.

Line 6: <meta name="apple-mobile-web-app-title" cont...

Most candidates didn’t know about this one, but experienced developers can talk about how to optimize a website for Apple devices, like apple-touch-icons and Safari pinned tab SVGs.

Perfect answer: You can pin a website on an iPhone’s homescreen to make it feel like a native app. Safari doesn’t support progressive web apps and you can’t really use other browser engines on iOS, so you don’t really have other options if you want that native-like experience, which Twitter, of course, likes. So they add this to tell Safari that the title of this app is Twitter. The next line is similar and controls how the status bar should look like when the app has launched.

Line 8: <meta name="theme-color" content="#ffffff"...

Perfect answer: This is the proper web standards-esque equivalent of the Apple status bar color meta tag. It tells the browser to theme the surrounding UI. Chrome on Android and Brave on desktop both do a pretty good job with that. You can put any CSS color in the content, and can even use the media attribute to only show this color for a specific media query like, for example, to support a dark theme. You can also define this and additional properties in the web app manifest.

Line 9: <meta http-equiv="origin-trial" content="...

Nobody I interviewed knew about this one. I would assume that you’d know this only if you have in-depth knowledge about all the new things that are happening on the standards track.

Perfect answer: Origin trials let us use new and experimental features on our site and the feedback is tracked by the user agent and reported to the web standards community without users having to opt-in to a feature flag. For example, Edge has an origin trial for dual-screen and foldable device primitives, which is pretty cool as you can make interesting layouts based on whether a foldable phone is opened or closed.

Also accepted: I don’t know about this one.

Line 10: html{-ms-text-size-adjust:100%;-webkit-text...

Almost nobody knew about this one too; only if you know about CSS edge cases and optimizations, you’d be able to figure this line out.

Perfect answer: Imagine that you don’t have a mobile responsive site and you open it on a small screen, so the browser might resize the text to make it bigger so it’s easier to read. The CSS text-size-adjust property can either disable this feature with the value none or specify a percentage up to which the browser is allowed to make the text bigger.

In this case, Twitter says the maximum is 100%, so the text should be no bigger than the actual size; they just do that because their site is already responsive and they don’t want to risk a browser breaking the layout with a larger font size. This is applied to the root HTML tag so it applies to everything inside it. Since this is an experimental CSS property, vendor prefixes are required. Also, there’s a missing <style> before this CSS, but I’m guessing that’s minified in the previous line and we don’t see it.

Also accepted: I don’t know about this property in specific but the -ms and -webkit- are vendor prefixes needed by Internet Explorer and WebKit-based browsers, respectively, for non-standard properties. We used to require these prefixes when CSS3 came out, but as properties go from experimental to stable or are adopted to a standards track, these prefixes go away in favor of a standardized property.

Bonus — Line 11: body{margin:0;}

This line from Twitter’s source code is particularly fun because you can follow-up with a question about the difference between resetting and normalizing a webpage. Almost everyone knew a version of the right answer.

Perfect answer: Because different browsers have different default styles (user agent stylesheet), you want to overwrite them by resetting properties so your site looks the same across devices. In this case, Twitter is telling the browser to remove the body tag’s default margin. This is just to reduce browser inconsistencies, but I prefer normalizing the styles instead of resetting them, i.e., applying the same defaults across browsers rather than removing them altogether. People even used to use * { margin: 0 } which is totally overkill and not great for performance, but now it’s common to import something like normalize.css or reset.css (or even something newer) and start from there.

More lines!

I always enjoy playing with the browser Inspector tool to see how sites are made, which is how I came up with this idea. Even though I consider myself sort of an expert on semantic HTML, I learn something new every time I do this exercise.

Since Twitter is mostly a client-side React app, there’s only a few dozen lines in the source code. Even with that, there’s so much to learn! There are a few more interesting lines in the Twitter source code that I leave as an exercise for you, the reader. How many of them could you explain in an interview?

<link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="Twitter">

…tells browsers that users can add Twitter as a search engine.

<link rel="preload" as="script" crossorigin="anonymous" href="" nonce="MGUyZTIyN2ItMDM1ZC00MzE5LWE2YmMtYTU5NTg2MDU0OTM1" />

…has many interesting attributes that can be discussed, especially nonce.

<link rel="alternate" hreflang="x-default" href="" />

…for international landing pages.

:focus:not([data-focusvisible-polyfill]){outline: none;}

…for removing the focus outline when not using keyboard navigation (the CSS :focus-visible selector is polyfilled here).

Explain the First 10 Lines of Twitter’s Source Code to Me originally published on CSS-Tricks. You should get the newsletter.


, , , , ,

CSS Database Queries? Sure We Can!

Kinda silly sounding, isn’t it? CSS database queries. But, hey, CSS is capable of talking to other languages in the sense that it can set the values of things that they can read. Plus, CSS can request other files, and I suppose a server could respond to that request with something it requested from a database.

But I’m getting ahead of myself. The idea of CSS database queries was a joke tweet going around the other day about recruiters looking for a developer who can connect to a database with CSS. Lee Mei Chin (total guess on the name there based on the domain name) wrote “Yes, I can connect to a DB in CSS” as an equally funny retort.

Screenshot of a tweet sarcastically looking for someone who can do CSS database queries.

What’s the trick behind CSS database queries?

It’s nicely elaborate:

  1. Use a hand-modified-to-ESM version of SQL.js, which is SQLite in JavaScript.
  2. Get a database ready that SQL.js can query.
  3. Build a Houdini PaintWorklet that executes queries in JavaScript and paints the results back to the screen in that <canvas>-y way that PaintWorklets do.
  4. Pass the query you want to run into the worklet by way of a CSS custom property.

So, the usage is like this in the end:

<script>   CSS.paintWorklet.addModule('./cssdb.js') </script> <style>   main {     --sql-query: SELECT name FROM test;     background: paint(sql-db);   } </style>

Which, you gotta admit, is connecting and querying a database in CSS.

This reminds me that Simon Willison did this last year with a totally different approach. His concept was that you have RESTful endpoints, like /api/roadside_attractions, that return JSON data. But then as an alternative endpoint, you could make that /api/roadside_attractions.css which would return a valid CSS file with all the data as CSS custom properties.

So, instead it looks like this:

<link rel="stylesheet" href="/api/roadside_attractions.css">  <style>   .attraction-name:after { content: var(--name); }   .attraction-address:after { content: var(--address); } </style>  <p class="attraction-name">Attraction name: </p> <p class="attraction-address">Address: </p>

Which, again, is essentially connecting to a database in CSS (with HTML required, though). You can literally see it work.

CSS Database Queries? Sure We Can! originally published on CSS-Tricks. You should get the newsletter.


, ,

5 Accessibility Quick Wins You Can Implement Today

Let’s face it: building an AA or AAA-accessible product can be quite daunting. Luckily, having an accessible product isn’t all-or-nothing. Even seemingly small improvements can have nice quality of life benefits for many people.

In that spirit, here are five accessibility quick wins you can implement today.

Quick Win 1: Indicate the Current Page

It’s probably safe to assume that a different style is the most common way to communicate the current page of a site or app. However, even if those styles are clear and with great contrast ratios, they’re still only a visual cue.

So what happens if a person with limited vision cannot see that separation? How will they know what page they’re on?

Creating an accessible product is to ensure its markup communicates as clearly as its design.

Adding aria-current="page" to the active navigation element is one way to ensure markup and design communicate the same information with or without assistive technologies.

<a aria-current="page" href="/">Home</a>

🎉 Bonus

Use CSS attribute selectors to style the aria-current="page" element to keep the visual and markup cues linked.

[aria-current="page"] {    /* Active element styles */ }

Quick Win 2: Document Language

While some people can visit a website and determine the language or locale of its content, not all people have that luxury. Again, markup must communicate the same information as the visual design — even if that information may seem implied.

Add the lang attribute to the <html> tag to communicate not only the document’s language, but its locale. This will help assistive technologies like screen readers understand and communicate the content. Even if the app only supports one language, this can be a nice quality of life improvement for many people.

<html lang="en-US">

For apps which support multiple languages, the <html> element is likely not the only one to need its lang value defined. Use the lang attribute on specific elements whose language differs from the rest of the document, like links within a language toggle menu. In this case, pair the use of lang with the hreflang attribute to not only communicate the language of the link itself, but also of its destination.

<a lang="fi" hreflang="fi" href="/" title="Suomeksi">   <bdi>Suomeksi</bdi> </a>

Quick Win 3: Use prefers-reduced-motion

Whether drawing attention to actions or updates, or creating a sense of life and charm, adding motion to an app can really elevate its experience. However, some people may find that experience disorienting.

Windows and MacOS both offer a setting at the OS level for people to greatly reduce the amount of motion when using their systems. The prefers-reduced-motion setting can greatly improve the experience on a computer, but it does not extends beyond the UI of the operating system. So wouldn’t it be nice if our apps could respect that same system setting and provide a more static experience for those who prefer it?

Well, with CSS media queries, they can.

The prefers-reduced-motion media query can be used to greatly reduce or remove all motion from an app whenever the system setting is enabled.

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

The blanket approach shown here prevents all motion, but it can leave little room for nuance. It’d be best to review the needs of those using the product, but consider these other options as well.

One approach could be to only animate one property at a time in prefers-reduced-motion settings. So consider a <Modal /> that fades and scales into view with opacity and transform. In reduced motion environments, only the opacity would transition. The scaling effect would be removed as they are more commonly problematic than fading.

Another option could be to look at the prefers-reduced-motion environment a bit more literally and remove all motion. This would do away with our scaling modals, sliding drawers, and bouncing notifications, but would still leave room for color transitions on links and buttons.

Quick Win 4: Indicate Data Sorting State

A common theme across all of these tips is to ensure that an app’s visual design and markup communicate the same things. So, when the design uses an arrow to indicate the sort direction of a table column, how can that also be communicated in the markup?

Setting the aria-sort attribute to ascending /descending on the header of the actively-sorted column allows the markup to communicate the same content structure as a visual indicator in the UI.

This will help ensure that people using assistive technologies and those who aren’t can understand the content in the same way.

<thead>   <tr>     <th>First Name</th>     <th aria-sort="ascending">Last Name</th>   </tr> </thead>

Quick Win 5: Lazy Loading Lists

Whether scrolling through an endless stream of tweets or through an impossible-to-decide list of products, the web has fully embraced lazy loading long lists of data (and alliteration, apparently).

This is when the aria-setsize and aria-posinset attributes become very valuable. While a person’s progression through the list can be communicated visually in many different ways, these attributes are used to communicate that same progression to many assistive technologies.

As developers, we likely have access to the length of an entire list as well as the index of the current items being displayed. With that, the aria-setsize attribute would define the total length of the list, while the aria-posinset attribute would define an item’s specific position (or index) within that list.

If the total length of the list is not known, then aria-setsize should be set to -1.

With these attributes, assistive technologies can better interpret a list and a person can better understand their position within it.

<h2 id="top-artists-title">Top Artists of 2021</h2> <ul role="listbox" aria-labelledby="top-artists-title">   <li role="option" aria-setsize="20" aria-posinset="5">Bloodbound</li>   <li role="option" aria-setsize="20" aria-posinset="6">Manimal</li>   <li role="option" aria-setsize="20" aria-posinset="7">Powerwolf</li> </ul>

Take a listen to how these attributes are announced using MacOS VoiceOver.

🎉 Bonus Win: Axe-DevTools Extension

Implementing those five accessibility quick wins is a great start, but that’s exactly what it is —a start. There’s a sprawling landscape of assistive technologies and sets of abilities a person can posses, and navigating it all alone can feel overwhelming.

Fortunately, there are plenty of tools to help with auditing a product’s accessibility that make the journey much more manageable. My personal favorite — my trusty accessibility compass — is the Axe-DevTools browser extension.

Running the Axe-DevTools accessibility scanner can return tons of valuable information. Not only will it display all issues and warnings found on the page, but it groups them by approximate severity. It can also highlight the element on the page or in the Elements tab and provide links to learn more about the specific issue.

However, most importantly, it will offer clear and concise approaches to fix the specific issue.

A screenshot of an Axe-DevTools accessibility report. Using the extension can lead to accessibility quick wins.

Wrapping Up

A product isn’t made accessible overnight; nor is a product’s accessibility work ever really complete. Like anything else on the web, accessibility evolves and requires maintenance. However, even seemingly small additions can have an impact on a product’s accessibility and a person’s overall experience.

After stepping into a new codebase, these are often some of the first few things I look into — some “low-hanging fruit” of accessibility, if you will.

Reaching AAA or even AA conformance can feel like scaling an 8,000 meter peak. These steps won’t carry you to the summit, but an expedition is never completed in a single stride.


5 Accessibility Quick Wins You Can Implement Today originally published on CSS-Tricks. You should get the newsletter.


, , , ,

Before I go: When it comes to complaining about web browsers

That’s a damn one-two punch from Dave. He goes for the ultimate clickbait title¹, then follows up with a pile of epic advice for us all. If you want web browsers to get better, listen up:

Complaining on Twitter sure does feel good but it doesn’t do much other than burning bridges and burning through people’s patience. I guess you may also get hit with the mute button which is probably the opposite effect you were hoping for. Despite how good or valid your complaint is, combativeness results in immediate dismissal by your target audience… not once, but for years. People hold grudges for a shockingly long time.

And so:

The best thing I’ve ever done in my career is blog about my specific problems with browsers (or any software you’re passionate about). This goes for software beyond browsers too. I’ve done this for IE, Safari, Edge, Firefox, Chrome, Windows 10, WSL and I’ve seen first hand how a “friction log” can become a powerful tool in an organization.

Behind the scenes, your posts will get picked up by external-facing developer advocates and shared internally. A single blog post is worth 10,000 tweets.

  1. Stay tuned for my upcoming blog post “I fell into a cement mixer and here’s everything I know about Cascade Layers.”

To Shared LinkPermalink on CSS-Tricks

Before I go: When it comes to complaining about web browsers originally published on CSS-Tricks. You should get the newsletter.


, , , ,