I teach a class over at the local college here in Long Beach and a majority of the content is hosted on the Canvas LMS so students can access it online. And, naturally, I want the content to be as accessible as possible, so thank goodness Canvas has a11y tooling built right into it.
But it ain’t all that rosy. It makes assumptions like all other a11y tooling and adheres to guidelines that were programmed into it. It’s not like the WCAG is baked right in and updated when it updates.
The reason this is even on my mind is that Jeremy yesterday described his love for writing image descriptions:
I enjoy writing alt text. I recently described how I updated my posting interface here on my own site to put a textarea for alt text front and centre for my notes with photos. Since then I’ve been enjoying the creative challenge of writing useful—but also evocative—alt text.
I buy into that! Writing alt text is a challenge that requires a delicate dance between the technical and the creative. It’s both an opportunity to make content more accessible and enhance the user experience.
One of those programmed guidelines in the Canvas tool is a cap of 120 characters on alt text. Why 120? I dunno, I couldn’t find any supporting guideline or rule for that exact number. One answer is that screen readers stop announcing text after 125 characters, but that’s apparently untrue, at least today. The general advice for how long alt text should be comes in varying degrees:
Jake Archibald talks of length in terms of emotion. Detail is great, but too much detail might distort the focal point, which makes total sense.
Carrie Fisher suggests a 150-character limit not because screen readers will truncate them but more as a mental note that maybe things are getting too descriptive.
Daniel Göransson says in this 2017 guide that it comes down to context and knowing when certain details of an image are worth additional explanation. But he generally errs on the side of conciseness.
So, how long should alt text be? The general consensus here is that there is no hard limit, but more of a contextual awareness of what purpose the image serves and adapting to it accordingly.
Which gets me back to Jeremy’s article. He was writing alt text for a group of speaker headshots and realized the text was all starting to sound the same. He paused, thought about the experience, compared it to the experience of a sighted user, and created parity between them:
The more speakers were added to the line-up, the more I felt like I was repeating myself with the alt text. […] The experience of a sighted person looking at a page full of speakers is that after a while the images kind of blend together. So if the alt text also starts to sound a bit repetitive after a while, maybe that’s not such a bad thing. A screen reader user would be getting an equivalent experience.
I dig that. So if you’re looking for a hard and fast rule on character counts, sorry to disappoint. Like so many other things, context is king and that’s the sort of thing that can’t be codified, or even automated for that matter.
And while we’re on the topic, just noticed that Twitter has UI to display alt text:
It wasn’t long ago that Nick Sypteras showed us how to make custom badges for a GitHub repo. Well, Reza Shakeri put Beautify GitHub Profile together and it’s a huuuuuuge repo of different badges that pulls lots of examples together with direct links to the repos you can use to create them.
And it doesn’t stop there! If you’re looking for some sort of embeddable widget, there’s everything from GitHub repo stats and contribution visualizations, all the way to embedded PageSpeed Insights and Spotify playlists. Basically, a big ol’ spot to get some inspiration.
Some things are simply wild!
Just scrolling through the repo gives me flashes of the GeoCities days, though. All it needs is a sparkly unicorn and a tiled background image to complete the outfit. 👔
In this article, we will build off those two articles to create even more complex CSS hover animations. We’re talking about background clipping, CSS masks, and even getting our feet wet with 3D perspectives. In other words, we are going to explore advanced techniques this time around and push the limits of what CSS can do with hover effects!
Cool Hover Effects That Use Background Clipping, Masks, and 3D (you are here!)
Here’s just a taste of what we’re making:
Hover effects using background-clip
Let’s talk about background-clip. This CSS property accepts a text keyword value that allows us to apply gradients to the text of an element instead of the actual background.
So, for example, we can change the color of the text on hover as we would using the color property, but this way we animate the color change:
All I did was add background-clip: text to the element and transition the background-position. Doesn’t have to be more complicated than that!
But we can do better if we combine multiple gradients with different background clipping values.
In that example, I use two different gradients and two values with background-clip. The first background gradient is clipped to the text (thanks to the text value) to set the color on hover, while the second background gradient creates the bottom underline (thanks to the padding-box value). Everything else is straight up copied from the work we did in the first article of this series.
How about a hover effect where the bar slides from top to bottom in a way that looks like the text is scanned, then colored in:
This time I changed the size of the first gradient to create the line. Then I slide it with the other gradient that update the text color to create the illusion! You can visualize what’s happening in this pen:
We’ve only scratched the surface of what we can do with our background-clipping powers! However, this technique is likely something you’d want to avoid using in production, as Firefox is known to have a lot of reported bugs related to background-clip. Safari has support issues as well. That leaves only Chrome with solid support for this stuff, so maybe have it open as we continue.
Let’s move on to another hover effect using background-clip:
You’re probably thinking this one looks super easy compared to what we’ve just covered — and you are right, there’s nothing fancy here. All I am doing is sliding one gradient while increasing the size of another one.
But we’re here to look at advanced hover effects, right? Let’s change it up a bit so the animation is different when the mouse cursor leaves the element. Same hover effect, but a different ending to the animation:
We have three background layers — two gradients and the background-color defined using --_c variable which is initially set to transparent (#0000). On hover, we change the color to white and the --_c variable to the main color (--c).
Here’s what is happening on that transition: First, we apply a transition to everything but we delay the color and background-color by 0.5s to create the sliding effect. Right after that, we change the color and the background-color. You might notice no visual changes because the text is already white (thanks to the first gradient) and the background is already set to the main color (thanks to the second gradient).
Then, on mouse out, we apply an instant change to everything (notice the 0s delay), except for the color and background-color that have a transition. This means that we put all the gradients back to their initial states. Again, you will probably see no visual changes because the text color and background-color already changed on hover.
Lastly, we apply the fading to color and a background-color to create the mouse-out part of the animation. I know, it may be tricky to grasp but you can better visualize the trick by using different colors:
Hover the above a lot of times and you will see the properties that are animating on hover and the ones animating on mouse out. You can then understand how we reached two different animations for the same hover effect.
Let’s not forget the DRY switching technique we used in the previous articles of this series to help reduce the amount of code by using only one variable for the switch:
If you’re wondering why I reached for the RGB syntax for the main color, it’s because I needed to play with the alpha transparency. I am also using the variable --_t to reduce a redundant calculation used in the transition property.
Before we move to the next part here are more examples of hover effects I did a while ago that rely on background-clip. It would be too long to detail each one but with what we have learned so far you can easily understand the code. It can be a good inspiration to try some of them alone without looking at the code.
I know, I know. These are crazy and uncommon hover effects and I realize they are too much in most situations. But this is how to practice and learn CSS. Remember, we pushing the limits of CSS hover effects. The hover effect may be a novelty, but we’re learning new techniques along the way that can most certainly be used for other things.
Hover effects using CSS mask
Guess what? The CSS mask property uses gradients the same way the background property does, so you will see that what we’re making next is pretty straightforward.
Let’s start by building a fancy underline.
I’m using background to create a zig-zag bottom border in that demo. If I wanted to apply an animation to that underline, it would be tedious to do it using background properties alone.
Enter CSS mask.
The code may look strange but the logic is still the same as we did with all the previous background animations. The mask is composed of two gradients. The first gradient is defined with an opaque color that covers the content area (thanks to the content-box value). That first gradient makes the text visible and hides the bottom zig-zag border. content-box is the mask-clip value which behaves the same as background-clip
linear-gradient(#000 0 0) content-box
The second gradient will cover the whole area (thanks to padding-box). This one has a width that’s defined using the --_p variable, and it will be placed on the left side of the element.
Now, all we have to do is to change the value of --_p on hover to create a sliding effect for the second gradient and reveal the underline.
.hover:hover {
--_p: 100%;
color: var(--c);
}
The following demo uses with the mask layers as backgrounds to better see the trick taking place. Imagine that the green and red parts are the visible parts of the element while everything else is transparent. That’s what the mask will do if we use the same gradients with it.
With such a trick, we can easily create a lot of variation by simply using a different gradient configuration with the mask property:
Each example in that demo uses a slightly different gradient configuration for the mask. Notice, too, the separation in the code between the background configuration and the mask configuration. They can be managed and maintained independently.
Let’s change the background configuration by replacing the zig-zag underline with a wavy underline instead:
Another collection of hover effects! I kept all the mask configurations and changed the background to create a different shape. Now, you can understand how I was able to reach 400 hover effects without pseudo-elements — and we can still have more!
Like, why not something like this:
Here’s a challenge for you: The border in that last demo is a gradient using the mask property to reveal it. Can you figure out the logic behind the animation? It may look complex at first glance, but it’s super similar to the logic we’ve looked at for most of the other hover effects that rely on gradients. Post your explanation in the comments!
Hover effects in 3D
You may think it’s impossible to create a 3D effect with a single element (and without resorting to pseudo-elements!) but CSS has a way to make it happen.
What you’re seeing there isn’t a real 3D effect, but rather a perfect illusion of 3D in the 2D space that combines the CSS background, clip-path, and transform properties.
The top and right sides of the element both need to equal the --b value while the bottom and left sides need to equal to the sum of --b and --d (which is the --_s variable).
For the second part of the trick, we need to define one gradient that covers all the border areas we previously defined. A conic-gradient will work for that:
We add another gradient for the third part of the trick. This one will use two semi-transparent white color values that overlap the first previous gradient to create different shades of the main color, giving us the illusion of shading and depth.
That’s all! We just made a 3D rectangle with nothing but two gradients and a clip-path that we can easily adjust using CSS variables. Now, all we have to do is to animate it!
Notice the coordinates from the previous figure (indicated in red). Let’s update those to create the animation:
The trick is to hide the bottom and left parts of the element so all that’s left is a rectangular element with no depth whatsoever.
This pen isolates the clip-path portion of the animation to see what it’s doing:
The final touch is to move the element in the opposite direction using translate — and the illusion is perfect! Here’s the effect using different custom property values for varying depths:
The second hover effect follows the same structure. All I did is to update a few values to create a top left movement instead of a top right one.
The actual code might be confusing at first, but go ahead and dissect it a little further — you’ll notice that it’s merely a combination of those three different effects, pretty much smushed together.
Let me finish this article with a last hover effect where I am combining background, clip-path, and a dash of perspective to simulate another 3D effect:
I applied the same effect to images and the result was quite good for simulating 3D with a single element:
Oof, we are done! I know, it’s a lot of tricky CSS but (1) we’re on the right website for that kind of thing, and (2) the goal is to push our understanding of different CSS properties to new levels by allowing them to interact with one another.
You may be asking what the next step is from here now that we’re closing out this little series of advanced CSS hover effects. I’d say the next step is to take all that we learned and apply them to other elements, like buttons, menu items, links, etc. We kept things rather simple as far as limiting our tricks to a heading element for that exact reason; the actual element doesn’t matter. Take the concepts and run with them to create, experiment with, and learn new things!
Case in point: there’s a little progressive enhancement in there using @supports for older browsers lacking support the font-palette property. Then the post gets into a strategy that shows the property’s light and dark values at play to make the font more legible in certain contexts. There’s also a clever idea about how creating multiple @font-palette-values blocks with the same name can be used for fallbacks.
Earlier this year, I self-published an ebook called Understanding JavaScript Promises (free for download). Even though I didn’t have any intention of turning it into a print book, enough people reached out inquiring about a print version that I decided to self-publish that as well .I thought it would be an easy exercise using HTML and CSS to generate a PDF and then send it off to the printer. What I didn’t realize was that I didn’t have an answer to an important part of a print book: the table of contents.
The makeup of a table of contents
At its core, a table of contents is fairly simple. Each line represents a part of a book or webpage and indicates where you can find that content. Typically, the lines contain three parts:
The title of the chapter or section
Leaders (i.e. those dots, dashes, or lines) that visually connect the title to the page number
The page number
A table of contents is easy to generate inside of word processing tools like Microsoft Word or Google Docs, but because my content was in Markdown and then transformed into HTML, that wasn’t a good option for me. I wanted something automated that would work with HTML to generate the table of contents in a format that was suitable for print. I also wanted each line to be a link so it could be used in webpages and PDFs to navigate around the document. I also wanted dot leaders between the title and page number.
And so I began researching.
I came across two excellent blog posts on creating a table of contents with HTML and CSS. The first was “Build a Table of Contents from your HTML” by Julie Blanc. Julie worked on PagedJS, a polyfill for missing paged media features in web browsers that properly formats documents for print. I started with Julie’s example, but found that it didn’t quite work for me. Next, I found Christoph Grabo’s “Responsive TOC leader lines with CSS” post, which introduced the concept of using CSS Grid (as opposed to Julie’s float-based approach) to make alignment easier. Once again, though, his approach wasn’t quite right for my purposes.
After reading these two posts, though, I felt I had a good enough understanding of the layout issues to embark on my own. I used pieces from both blog posts as well as adding some new HTML and CSS concepts into the approach to come up with a result I’m happy with.
Choosing the correct markup
When deciding on the correct markup for a table of contents, I thought primarily about the correct semantics. Fundamentally, a table of contents is about a title (chapter or subsection) being tied to a page number, almost like a key-value pair. That led me to two options:
One option is to use a table (<table>) with one column for the title and one column for the page.
Then there’s the often unused and forgotten definition list (<dl>) element. It also acts as a key-value map. So, once again, the relationship between the title and the page number would be obvious.
Either of these seemed like good options until I realized that they really only work for single-level tables of contents, namely, only if I wanted to have a table of contents with just chapter names. If I wanted to show subsections in the table of contents, though, I didn’t have any good options. Table elements aren’t great for hierarchical data, and while definition lists can technically be nested, the semantics didn’t seem correct. So, I went back to the drawing board.
I decided to build off of Julie’s approach and use a list; however, I opted for an ordered list (<ol>) instead of an unordered list (<ul>). I think an ordered list is more appropriate in this case. A table of contents represents a list of chapters and subheadings in the order in which they appear in the content. The order matters and shouldn’t get lost in the markup.
Unfortunately, using an ordered list means losing the semantic relationship between the title and the page number, so my next step was to re-establish that relationship within each list item. The easiest way to solve this is to simply insert the word “page” before the page number. That way, the relationship of the number relative to the text is clear, even without any other visual distinction.
Here’s a simple HTML skeleton that formed the basis of my markup:
Once I had established the markup I planned to use, the next step was to apply some styles.
First, I removed the autogenerated numbers. You can choose to keep the autogenerated numbers in your own project if you’d like, but it’s common for books to have unnumbered forewords and afterwords included in the list of chapters, which makes the autogenerated numbers incorrect.
For my purpose, I would fill in the chapter numbers manually then adjust the layout so the top-level list doesn’t have any padding (thus aligning it with paragraphs) and each embedded list is indented by two spaces. I chose to use a 2ch padding value because I still wasn’t quite sure which font I would use. The ch length unit allows the padding to be relative to the width of a character — no matter what font is used — rather than an absolute pixel size that could wind up looking inconsistent.
Here’s the CSS I ended up with:
.toc-list, .toc-list ol {
list-style-type: none;
}
.toc-list {
padding: 0;
}
.toc-list ol {
padding-inline-start: 2ch;
}
Sara Soueidan pointed out to me that WebKit browsers remove list semantics when list-style-type is none, so I needed to add role="list" into the HTML to preserve it:
With the list styled to my liking, it was time to move on to styling an individual list item. For each item in the table of contents, the title and page number must be on the same line, with the title to the left and the page number aligned to the right.
You might be thinking, “No problem, that’s what flexbox is for!” You aren’t wrong! Flexbox can indeed achieve the correct title-page alignment. But there are some tricky alignment issues when the leaders are added, so I instead opted to go with Christoph’s approach using a grid, which as a bonus as it also helps with multiline titles. Here is the CSS for an individual item:
.toc-list li > a {
text-decoration: none;
display: grid;
grid-template-columns: auto max-content;
align-items: end;
}
.toc-list li > a > .page {
text-align: right;
}
The grid has two columns, the first of which is auto-sized to fill up the entire width of the container, minus the second column, which is sized to max-content. The page number is aligned to the right, as is traditional in a table of contents.
The only other change I made at this point was to hide the “Page” text. This is helpful for screen readers but unnecessary visually, so I used a traditional visually-hidden class to hide it from view:
With this foundation in place, I moved on to address the leaders between the title and the page.
Creating dot leaders
Leaders are so common in print media that you might be wondering, why doesn’t CSS already support that? The answer is: it does. Well, kind of.
There is actually a leader() function defined in the CSS Generated Content for Paged Media specification. However, as with much of the paged media specifications, this function isn’t implemented in any browsers, therefore excluding it as an option (at least at the time I’m writing this). It’s not even listed on caniuse.com, presumably because no one has implemented it and there are no plans or signals that they will.
Fortunately, both Julie and Christoph already addressed this problem in their respective posts. To insert the dot leaders, they both used a ::after pseudo-element with its content property set to a very long string of dots, like this:
The ::after pseudo-element is set to an absolute position to take it out of the flow of the page and avoid wrapping to other lines. The text is aligned to the right because we want the last dots of each line flush to the number at the end of the line. (More on the complexities of this later.) The .title element is set to have a relative position so the ::after pseudo-element doesn’t break out of its box. Meanwhile, the overflow is hidden so all those extra dots invisible. The result is a pretty table of contents with dot leaders.
However, there’s something else that needs consideration.
Sara also pointed out to me that all of those dots count as text to screen readers. So what do you hear? “Introduction dot dot dot dot…” until all of the dots are announced. That’s an awful experience for screen reader users.
The solution is to insert an additional element with aria-hidden set to true and then use that element to insert the dots. So the HTML becomes:
Now screen readers will ignore the dots and spare users the frustration of listening to multiple dots being announced.
Finishing touches
At this point, the table of contents component looks pretty good, but it could use some minor detail work. To start, most books visually offset chapter titles from subsection titles, so I made the top-level items bold and introduced a margin to separate subsections from the chapters that followed:
.toc-list > li > a {
font-weight: bold;
margin-block-start: 1em;
}
Next, I wanted to clean up the alignment of the page numbers. Everything looked okay when I was using a fixed-width font, but for variable-width fonts, the leader dots could end up forming a zigzag pattern as they adjust to the width of a page number. For instance, any page number with a 1 would be narrower than others, resulting in leader dots that are misaligned with the dots on previous or following lines.
To fix this problem, I set font-variant-numeric to tabular-nums so all numbers are treated with the same width. By also setting the minimum width to 2ch, I ensured that all numbers with one or two digits are perfectly aligned. (You may want to set this to 3ch if your project has more than 100 pages.) Here is the final CSS for the page number:
.toc-list li > a > .page {
min-width: 2ch;
font-variant-numeric: tabular-nums;
text-align: right;
}
And with that, the table of contents is complete!
Conclusion
Creating a table of contents with nothing but HTML and CSS was more of a challenge than I expected, but I’m very happy with the result. Not only is this approach flexible enough to accommodate chapters and subsections, but it handles sub-subsections nicely without updating the CSS. The overall approach works on web pages where you want to link to the various locations of content, as well as PDFs where you want the table of contents to link to different pages. And of course, it also looks great in print if you’re ever inclined to use it in a brochure or book.
I’d like to thank Julie Blanc and Christoph Grabo for their excellent blog posts on creating a table of contents, as both of those were invaluable when I was getting started. I’d also like to thank Sara Soueidan for her accessibility feedback as I worked on this project.
So, SVG has this stroke-miterlimit presentation attribute. You’ve probably seen it when exporting an SVG from a graphic editor program, or perhaps you find out you could remove it without noticing any change to the visual appearance.
After a good amount of research, one of the first things I discovered is that the attribute works alongside stroke-linejoin, and I’ll show you how as well as a bunch of other things I learned about this interesting (and possibly overlooked) SVG attribute.
TLDR;
stroke-miterlimit depends on stroke-linejoin: if we use round or bevel for joins, then there’s no need to declare stroke-miterlimit. But if we use miter instead, we can still delete it and maybe the default value will be enough. Beware that many graphic software editors will add this attribute even when is not necessary.
What is stroke-linejoin?
I know, we’re actually here to talk about stroke-miterlimit, but I want to start with stroke-linejoin because of how tightly they work together. This is the definition for stroke-linejoin pulled straight from the SVG Working Group (SVGWG):
stroke-linejoin specifies the shape to be used at the corners of paths or basic shapes when they are stroked.
This means we can define how the corner looks when two lines meet at a point. And this attribute accepts five possible values, though two of them have no browser implementation and are identified by the spec as at risk of being dropped. So, I’ll briefly present the three supported values the attribute accepts.
miter is the default value and it just so happens to be the most important one of the three we’re looking at. If we don’t explicitly declare stroke-linejoin in the SVG code, then miter is used to shape the corner of a path. We know a join is set to miter when both edges meet at a sharp angle.
But we can also choose round which softens the edges with — you guessed it — rounded corners.
The bevel value, meanwhile, produces a flat edge that sort of looks like a cropped corner.
What is stroke-miterlimit?
OK, now that we know what stroke-linejoin is, let’s get back to the topic at hand and pick apart the definition of stroke-miterlimit from the book Using SVG with CSS3 and HTML5:
[…] on really tight corners, you have to extend the stroke for quite a distance, before the two edges meet. For that reason, there is a secondary property: stroke-miterlimit. It defines how far you can extend the point when creating a miter corner.
In other words, stroke-miterlimit sets how far the stroke of the edges goes before they can meet at a point. And only when the stroke-linejoin is miter.
So, the stroke-miterlimit value can be any positive integer, where 4 is the default value. The higher the value, the further the corner shape is allowed to go.
How they work together
You probably have a good conceptual understanding now of how stroke-linejoin and stroke-miterlimit work together. But depending on the stroke-miterlimit value, you might get some seemingly quirky results.
Case in point: if stroke-linejoin is set to miter, it can actually wind up looking like the bevel value instead when the miter limit is too low. Here’s the spec again to help us understand why:
If the miter length divided by the stroke width exceeds the stroke-miterlimit then [the miter value] is converted to a bevel.
That makes sense, right? If the miter is unable to exceed the width of the stroke, then it ought to be a flat edge. Otherwise, the miter can grow and form a point.
Sometimes seeing is believing, so here’s Ana Tudor with a wonderful demo showing how the stroke-miterlimit value affects an SVG’s stroke-linejoin:
Setting miter limits in design apps
Did you know that miter joins and limits are available in many of the design apps we use in our everyday work? Here’s where to find them in Illustrator, Figma, and Inkscape.
Setting miter limits in Adobe Illustrator
Illustrator has a way to modify the miter value when configuring a path’s stroke. You can find it in the “Stroke” settings on a path. Notice how — true to the spec — we are only able to set a value for the “Limit” when the path’s “Corner” is set to “Miter Join”.
One nuance is that Illustrator has a default miter limit of 10 rather than the default 4. I’ve noticed this every time I export the SVG file or copy and paste the resulting SVG code. That could be confusing when you open up the code because even if you do not change the miter limit value, Illustrator adds stroke-miterlimit="10" where you might expect 4 or perhaps no stroke-miterlimit at all.
And that’s true even if we choose a different stroke-linejoin value other than “Miter Join”. Here is the code I got when exporting an SVG with stroke-linejoin="round".
Miter joins and limits are slightly different in Figma. When we click the node of an angle on a shape, under the three dots of the Stroke section, we can find a place to set the join of a corner. The option “Miter angle” appears by default, but only when the join is set to miter:
This part works is similar to Illustrator except for how Figma allows us to set the miter angle in degree units instead of decimal values. There are some other specific nuances to point out:
The angle is 7.17° by default and there is no way to set a lower value. When exporting the SVG, that value is becomes stroke-miterlimit='16‘ in the markup, which is different from both the SVG spec and the Illustrator default.
The max value is 180°, and when drawing with this option, the join is automatically switched to bevel.
When exporting with bevel join, the stroke-miterlimit is there in the code, but it keeps the value that was set when the miter angle was last active (Illustrator does the same thing).
When exporting the SVG with a round join, the path is expanded and we no longer have a stroke, but a path with a fill color.
I was unable to find a way to avoid the extra code that ends up in the exported SVG when stroke-miterlimit is unneeded.
Setting miter limits in Inkscape
Inkscape works exactly the way I’d expect a design app to manage miter joins and limits. When selecting a a miter join, the default value is 4, exactly what it is in the spec. Better yet, stroke-miterlimit is excluded from the exported SVG code when it is the default value!
Still, if we export any path with bevel or round after the limit was modified, the stroke-miterlimit will be back in the code, unless we keep the 4 units of the default in the Limit box. Same trick as Illustrator.
These examples will work nicely if we choose the Save As → Optimized SVG option. Inkscape is free and open source and, at the end of the day, has the neatest code as far as stroke-miterlimit goes and the many options to optimize the code for exporting.
But if you are more familiar with Illustrator (like I am), there is a workaround to keep in mind. Figma, because of the degree units and the expansion of the strokes, feels like the more distant from the specs and expected behavior.
Wrapping up
And that’s what I learned about SVG’s stroke-miterlimit attribute. It’s another one of those easy-to-overlook things we might find ourselves blindly cutting out, particularly when optimizing an SVG file. So, now when you find yourself setting stroke-miterlimit you’ll know what it does, how it works alongside stroke-linejoin, and why the heck you might get a beveled join when setting a miter limit value.
The use case? Well, Ahmad wastes no time showing how to use the property to accomplish what used to require either (1) a wrapping element with hidden overflow around an image that’s sized and positioned inside that element or (2) the background-image route.
But with object-view-box we can essentially draw the image boundaries as we can with an SVG’s viewbox. So, take a plain ol’ <img> and call on object-view-box to trim the edges using an inset function. I’ll simply drop Ahmad’s pen in here:
Only supported in Chrome Canary for now, I’m afraid. But it’s (currently) planned to release in Chrome 104. Elsewhere:
As the author of a library called AgnosticUI, I’m always on the lookout for new components. And recently, I decided to dig in and start work on a new dialog (aka modal) component. That’s something many devs like to have in their toolset and my goal was to make the best one possible, with an extra special focus on making it inclusive and accessible.
My first thought was that I would avoid any dependencies and bite the bullet to build my own dialog component. As you may know, there’s a new <dialog> element making the rounds and I figured using it as a starting point would be the right thing, especially in the inclusiveness and accessibilities departments.
But, after doing some research, I instead elected to leverage a11y-dialog by Kitty Giraudel. I even wrote adapters so it integrates smoothly with Vue 3, Svelte, and Angular. Kitty has long offered a React adapter as well.
Why did I go that route? Let me take you through my thought process.
First question: Should I even use the native <dialog> element?
The native <dialog> element is being actively improved and will likely be the way forward. But, it still has some issues at the moment that Kitty pointed out quite well:
Clicking the backdrop overlay does not close the dialog by default
The alertdialog ARIA role used for alerts simply does not work with the native <dialog> element. We’re supposed to use that role when a dialog requires a user’s response and shouldn’t be closed by clicking the backdrop, or by pressing ESC.
And as Kitty also points out, there are general issues with the element’s default styles, like the fact they are left to the browser and will require JavaScript. So, it’s sort of not 100% HTML anyway.
Here’s a pen demonstrating these points:
Now, some of these issues may not affect you or whatever project you’re working on specifically, and you may even be able to work around things. If you still would like to utilize the native dialog you should see Adam Argyle’s wonderful post on building a dialog component with native dialog.
OK, let’s discuss what actually are the requirements for an accessible dialog component…
What I’m looking for
I know there are lots of ideas about what a dialog component should or should not do. But as far as what I was personally going after for AgnosticUI hinged on what I believe make for an accessible dialog experience:
The dialog should close when clicking outside the dialog (on the backdrop) or when pressing the ESC key.
It should trap focus to prevent tabbing out of the component with a keyboard.
It should allow forwarding tabbing with TAB and backward tabbing with SHIFT+TAB.
It should return focus back to the previously focused element when closed.
It should correctly apply aria-* attributes and toggles.
It should provide Portals (only if we’re using it within a JavaScript framework).
It should support the alertdialog ARIA role for alert situations.
It would be great if our implementation could avoid the common pitfalls that come with the native <dialog> element.
It would ideally provide a way to apply custom styling while also taking the prefers-reduced-motion user preference query as a further accessibility measure.
It should be clear right about now why I nixed the native <dialog> element from my component library. I believe in the work going into it, of course, but my current needs simply outweigh the costs of it. That’s why I went with Kitty’s a11y-dialog as my starting point.
Auditing <dialog> accessibility
Before trusting any particular dialog implementation, it’s worth making sure it fits the bill as far as your requirements go. With my requirements so heavily leaning on accessibility, that meant auditing a11y-dialog.
Accessibility audits are a profession of their own. And even if it’s not my everyday primary focus, I know there are some things that are worth doing, like:
manually verifying the functionality listed above, of course in different browsers,
This is quite a lot of work, as you might imagine (or know from experience). It’s tempting to take a path of less resistance and try automating things but, in a study conducted by Deque Systems, automated tooling can only catch about 57% of accessibility issues. There’s no substitute for good ol’ fashioned hard work.
The auditing environment
The dialog component can be tested in lots of places, including Storybook, CodePen, CodeSandbox, or whatever. For this particular test, though, I prefer instead to make a skeleton page and test locally. This way I’m preventing myself from having to validate the validators, so to speak. Having to use, say, a Storybook-specific add-on for a11y verification is fine if you’re already using Storybook on your own components, but it adds another layer of complexity when testing the accessibility of an external component.
A skeleton page can verify the dialog with manual checks, existing a11y tooling, and screen readers. If you’re following along, you’ll want to run this page via a local server. There are many ways to do that; one is to use a tool called serve, and npm even provides a nice one-liner npx serve <DIRECTORY> command to fire things up.
Let’s do an example audit together!
I’m obviously bullish on a11y-dialog here, so let’s put it to the test and verify it using some of the the recommended approaches we’ve covered.
Again, all I’m doing here is starting with an HTML. You can use the same one I am (complete with styles and scripts baked right in).
I know, we’re ignoring a bunch of best practices (what, styles in the <head>?!) and combined all of the HTML, CSS, and JavaScript in one file. I won’t go into the details of the code as the focus here is testing for accessibility, but know that this test requires an internet connection as we are importing a11y-dialog from a CDN.
First, the manual checks
I served this one-pager locally and here are my manual check results:
Feature
Result
It should close when clicking outside the dialog (on the backdrop) or when pressing the ESC key.
✅
It ought to trap focus to prevent tabbing out of the component with a keyboard.
✅
It should allow forwarding tabbing with TAB and backward tabbing with SHIFT+TAB.
✅
It should return focus back to the previously focused element when closed.
✅
It should correctly apply aria-* attributes and toggles.
✅
I verified this one “by eye” after inspecting the elements in the DevTools Elements panel.
It should provide Portals.
Not applicable.
This is only useful when implementing the element with React, Svelte, Vue, etc. We’ve statically placed it on the page with aria-hidden for this test.
It should support for the alertdialog ARIA role for alert situations.
✅
You’ll need to do two things:
First, remove data-a11y-dialog-hide from the overlay in the HTML so that it is <div class="dialog-overlay"></div>. Replace the dialog role with alertdialog so that it becomes:
Now, clicking on the overlay outside of the dialog box does not close the dialog, as expected.
It should prevent the underlying bodyfrom scrolling, if needed.
✅
I didn’t manually test but this, but it is clearly available per the documentation.
It should avoid the common pitfalls that come with the native <dialog> element.
✅
This component does not rely on the native <dialog> which means we’re good here.
Next, let’s use some a11y tooling
I used Lighthouse to test the component both on a desktop computer and a mobile device, in two different scenarios where the dialog is open by default, and closed by default.
I’ve found that sometimes the tooling doesn’t account for DOM elements that are dynamically shown or hidden DOM elements, so this test ensures I’m getting full coverage of both scenarios.
I also tested with IBM Equal Access Accessibility Checker. Generally, this tool will give you a red violation error if there’s anything egregious wrong. It will also ask you to manually review certain items. As seen here, there a couple of items for manual review, but no red violations.
Moving on to screen readers
Between my manual and tooling checks, I’m already feeling relatively confident that a11y-dialog is an accessible option for my dialog of choice. However, we ought to do our due diligence and consult a screen reader.
VoiceOver is the most convenient screen reader for me since I work on a Mac at the moment, but JAWS and NVDA are big names to look at as well. Like checking for UI consistency across browsers, it’s probably a good idea to test on more than one screen reader if you can.
Here’s how I conducted the screen reader part of the audit with VoiceOver. Basically, I mapped out what actions needed testing and confirmed each one, like a script:
Step
Result
The dialog component’s trigger button is announced.
“Entering A11y Dialog Test, web content.”
The dialog should open when pressing CTRL+ALT +Space should show the dialog.
“Dialog. Some description of what’s inside this dialog. You are currently on a dialog, inside of web content.”
The dialog should TAB to and put focus on the component’s Close button.
“Close this dialog button. You are currently on a button, inside of web content.”
Tab to the link element and confirm it is announced.
“Link, Rando Yahoo Link”
Pressing the SPACE key while focused on the Close button should close the dialog component and return to the last item in focus.
✅
Testing with people
If you’re thinking we’re about to move on to testing with real people, I was unfortunately unable to find someone. If I had done this, though, I would have used a similar set of steps for them to run through while I observe, take notes, and ask a few questions about the general experience.
As you can see, a satisfactory audit involves a good deal of time and thought.
Fine, but I want to use a framework’s dialog component
That’s cool! Many frameworks have their own dialog component solution, so there’s lots to choose from. I don’t have some amazing spreadsheet audit of all the frameworks and libraries in the wild, and will spare you the work of evaluating them all.
Instead, here are some resources that might be good starting points and considerations for using a dialog component in some of the most widely used frameworks.
Disclaimer: I have not tested these personally. This is all stuff I found while researching.
You might want to look at svelte-headlessui. Material has a port in svelterial that is also worth a look. It seems that many current SvelteKit users prefer to build their own component sets as SvelteKit’s packaging idiom makes it super simple to do. If this is you, I would definitely recommend considering svelte-a11y-dialog as a convenient means to build custom dialogs, drawers, bottom sheets, etc.
I’ll also point out that my AgnosticUI library wraps the React, Vue, Svelte and Angular a11y-dialog adapter implementations we’ve been talking about earlier.
If you have other inclusive and accessible library-based dialog components that merit consideration, I’d love to know about them in the comments!
But I’m creating a custom design system
If you’re creating a design system or considering some other roll-your-own dialog approach, you can see just how many things need to be tested and taken into consideration… all for one component! It’s certainly doable to roll your own, of course, but I’d say it’s also extremely prone to error. You might ask yourself whether the effort is worthwhile when there are already battle-tested options to choose from.
You could put in the effort to add in those extensions, or you could use a robust plugin like a11y-dialog and ensure that your dialogs will have a pretty consistent experience across all browsers.
Back to my objective…
I need that dialog to support React, Vue, Svelte, and Angular implementations.
I mentioned earlier that a11y-dialog already has ports for Vue and React. But the Vue port hasn’t yet been updated for Vue 3. Well, I was quite happy to spend the time I would have spent creating what likely would have been a buggy hand-rolled dialog component toward helping update the Vue port. I also added a Svelte port and one for Angular too. These are both very new and I would consider them experimental beta software at time of writing. Feedback welcome, of course!
It can support other components, too!
I think it’s worth pointing out that a dialog uses the same underlying concept for hiding and showing that can be used for a drawer (aka off-canvas) component. For example, if we borrow the CSS we used in our dialog accessibility audit and add a few additional classes, then a11y-dialog can be transformed into a working and effective drawer component:
These classes are used in an additive manner, essentially extending the base dialog component. This is exactly what I have started to do as I add my own drawer component to AgnosticUI. Saving time and reusing code FTW!
Wrapping up
Hopefully I’ve given you a good idea of the thinking process that goes into the making and maintenance of a component library. Could I have hand-rolled my own dialog component for the library? Absolutely! But I doubt it would have yielded better results than what a resource like Kitty’s a11y-dialog does, and the effort is daunting. There’s something cool about coming up with your own solution — and there may be good situations where you want to do that — but probably not at the cost of sacrificing something like accessibility.
Anyway, that’s how I arrived at my decision. I learned a lot about the native HTML <dialog> and its accessibility along the way, and I hope my journey gave you some of those nuggets too.