Home
sarasoueidan.com.web.brid.gy
Home
@sarasoueidan.com.web.brid.gy
– The personal website of Sara Soueidan, inclusive design engineer

[bridged from https://sarasoueidan.com/ on the web: https://fed.brid.gy/web/sarasoueidan.com ]
Accessible notifications with ARIA Live Regions (Part 1)
In this chapter, we’re going to learn about ARIA live regions — the accessible notifications system that enables us to make **dynamic** web content more accessible to screen reader users. Without live regions, some rich web applications would be more challenging to use for screen reader users. So if you’re building web applications such as Single Page Applications (SPAs), you need to understand live regions so that you can utilize them **where appropriate**. This chapter is split into two parts. In **this first part** , we’re going to learn about why ARIA live regions are important, and the different ARIA attributes and roles that you can use to create them. We’re going to get an overview of these attributes, as well as learn about their current support landscape and limitations. In the second part, we’re going to get more practical and discuss why you should _not_ use live regions as much as you might think that you do, and we’ll talk about alternative approaches you should use instead when you create some common UI patterns. And then we’ll discuss best practices for implementing more robust live regions when you need them today. But first, in order to understand _why_ live regions are important, **we must first understand how a screen reader parses web content and presents it to the user.** We won’t get into much detail (not at all, really!), just enough to get a good understanding of what problem live regions solve. ## How screen readers parse web content The way screen readers parse and present web content to their users is very different to how sighted users see that content. Screen readers work by **linearizing web content.** Linearizing a page’s content means converting the page’s two-dimensional content into **a one-dimensional string** (that is then either spoken to the user using text-to-speech, or delivered to them via a refreshable braille display). When content is linearized, it is presented to the user **one item at a time.** "You can think of it like listening to a cassette tape", says Web accessibility specialist Ugi Kutluoglu, "which you can rewind, fast forward, pause and play." This means that a screen reader user can skip to items or sections they want, and they can tab through interactive elements and Shift-tab their way back. But at the end of the day, they can only move forwards or backwards, **one item at a time** , because what they are presented with is a one-dimensional version of the page. ## Why we need an accessible notification system for screen reader users Reading content linearly works well for static webpages, but it doesn’t work so well for pages where content is altered and updated dynamically or asynchronously using JavaScript. If the user can only move in one dimension, and focus on one item at a time, how would they know when content is added, removed, or modified **somewhere else** on the page? For example, when a user sends an email in most email web apps, a status message is shown at the top of the screen, or a “toast” message pops up (typically at the bottom of the screen) to notify them of the status of their interaction — for example that the email is sending, has been sent, or maybe that the email could _not_ be sent. Some of these messages are urgent (like an error message), and some of them are not (like a success message, or a Draft Saved notification). When these status messages appear, they are intended to be communicated to all users. But while these messages may be perceivable by a sighted user, they’re not preceivable by a blind screen reader user. When the status message is shown, it is not communicated to screen reader users by default because the screen reader focus is on another element at that moment (on the ‘Send Email’ button in this case). Here is what happens when I use NVDA and activate the Send Email button and show a status message in a dummy email app demo I created. NVDA does not announce the status message when it is shown. Sorry, your browser doesn't support embedded videos. Here is a dummy email app demo you can try for yourself. See the Pen Live region status message in email web app by Sara Soueidan (@SaraSoueidan) on CodePen. If you activate the Send Email button in the debug version of the dummy email app, the screen reader will _not_ announce the status message that is shown. **A screen reader can only focus on one element or part of the page at a time.** This means that if the user presses a button and that button triggers an update somewhere else on the page, **there’s a good chance they will be oblivious to it**. So they need a way to be notified of these updates when they happen. There are two primary ways you make a screen reader announce an update when it happens: 1. By **_moving_ focus** to where the update has happened, (like we did with the summary of error messages in the accessible form validation chapter); 2. By **notifying** the screen reader of these updates when they happen. When you move the user’s focus to an element, screen readers typically announce the element to the user. But when an update happens and you don’t move the user’s focus to it, you must notify screen readers in some other way. ## Status messages in WCAG WCAG Success Criterion **4.1.3 Status Messages (Level AA)** states that: > In content implemented using markup languages, status messages can be programmatically determined through role or properties such that they can be presented to the user by assistive technologies without receiving focus. A status message is defined in the specification as "change in content that is not a change of context, and that provides information to the user on the success or results of an action, on the waiting state of an application, on the progress of a process, or on the existence of errors." Examples of a **change of context** are opening a new window, **moving focus to a different component** , going to a new page (including anything that would look to a user as if they had moved to a new page) or significantly re-arranging the content of a page. From the Understanding Status Messages page: > This Success Criterion specifically addresses scenarios where new content is added to the page without changing the user’s context. Changes of context, by their nature, interrupt the user by taking focus. They are already surfaced by assistive technologies, and so have already met the goal to alert the user to new content. As such, messages that involve changes of context do not need to be considered and are not within the scope of this Success Criterion. In other words, this success criterion aims to ensure that, unless you move the user’s focus or cause another change of context like a page refresh, you must ensure that status messages are communicated to screen reader users using the appropriate roles and properties. To do that, you currently need ARIA live regions. ## What are ARIA live regions? ARIA live regions are **a specific type of notification system primarily surfaced for screen reader users.** Using live regions, you can communicate content updates down to the accessibility layer so that screen readers are made aware of these updates when they happen. On an implementation level, a **live region** (**not to be confused with the`region` landmark**) is an element on the page that has been designated as being “live”. When an element is designated as a live region, **a screen reader is notified when any updates take place within the element (and its children), wherever its focus is at the time.** "Think of live regions as something like a livestream" says Web accessibility specialist Ugi Kutluoglu, "everything happening inside will be announced live like a news channel you’re listening to in the background." Using live regions, you can mark up status messages and other similar updates so that they are communicated to screen reader users. Here is our email notification example again with ARIA live regions working. Notice how when the ‘Send Email’ button is activated, NVDA announces the contents of the status message that is shown: Sorry, your browser doesn't support embedded videos. The screen reader announces the contents of the message because I’ve designated the message container as a live region (we’re going to learn how to do that shortly). So now the element is monitored for updates and the screen reader is notified of these updates when they happen. Then, when the button is activated, I inserted the contents of the message into the message container. And when I did, the screen reader was notified and it announced the update to the user. When an update happens in a live region, the screen reader announces that update, and it only announces it once. Toast messages and other similar status messages are the closest **visual equivalent** of a live region announcement. (Though not all toast messages are a good candidate for live regions. We’ll talk more about this later.) A toast message is used to present **timely information** — including confirmation of actions, statuses, and alerts. By nature, **toast messages are auto-expiring** — they disappear on their own after a few seconds. And once they disappear, they’re gone. The user cannot review the message again. Like toasts, **live region notifications are transient.** **Once an announcement is made, it disappears forever.** They cannot be reviewed, replayed, or revealed later. If the user misses an announcement, they miss it. It’s gone. That is, unless you provide them with a way to review it (like collecting all notifications in a notifications center, for example). Because of their transient nature, live regions have specific and limited use cases and should not be used as an alternative to other more persistent approaches. In fact, if you _can_ use another more persistent approach, you almost definitely should. We’ll talk more about how to use live regions and when _not_ to use live regions later in the chapter. ## Creating a live region Using ARIA, **almost any element can be designated as a live region**. It doesn’t need to be a structural element; and it doesn’t need to have any implicit semantics by default. You can designate an element as a live region using: 1. The `aria-live` attribute. 2. ARIA live region roles. HTML also provides one native element that has implicit live region semantics: the `<output>` element. We’re going to talk more about it in another section. ### 1. Using the `aria-live` attribute `aria-live` is the primary attribute used **to designate an element as a live region.** When used on an element, it indicates that this element may be updated, and those updates should be communicated to screen readers. The value of `aria-live` **describes the types of updates** that can be expected from the region. It accepts three values: `assertive`, `polite`, and `off` (which is equivalent to removing the property altogether). <!-- this div is now a live region! It's as simple as that. --> <div aria-live="[ polite | assertive ]"> ... </div> The value of `aria-live` you choose will depend on **the type, urgency and priority of the update.** * If the update is important enough that it requires the user’s **immediate attention** , `assertive` will tell the screen reader to _immediately_ notify the user, **interrupting whatever the user’s currently doing.** Assertive notifications are good for when users need to immediately know something and act on it, like when there’s **an error** in submitting information in a form, or something more serious like a **session timeout** or a **security alert**. Assertive notifications are very disruptive and should be limited to a few use cases where the messages are critical to the user and require their immediate attention. Otherwise, they may disorient users or cause them not to complete their current task. * `polite` on the other hand, is more… polite. It indicates that the screen reader **should wait until the user is idle** (such as when the screen reader has finished reading the current sentence, or when the user pauses typing) before presenting updates to them. Polite regions do not interrupt the user’s current task. They are more suitable for things like success messages, feeds, chat logs, and loading indicators, for example. `aria-live="off"` is the assumed default value for all elements. It indicates that updates to the element should not be presented to the user **unless the user is currently focused on that region**. So creating a live region is literally as simple as declaring the `aria-live` on an element. Here is an example where I have a `<div>` with no `aria-live` set on it. When you activate the button, the `<div>` will get populated with a message via JavaScript; but the screen reader will not announce the update. So the user will not be aware that any content has been added to the `<div>` at this point. Try adding `aria-live="polite"` or `aria-live="assertive"` to it and then activate the button again. The screen reader will announce the contents of the message even though focus is not moved to the message: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. This is pretty powerful! The live region works even if it is visually-hidden, as long as it is not hidden in a way that removes it from the accessibility tree. (We’ve learned all about choosing an appropriate hiding technique for your content in the hiding techniques chapter.) There are some valid use cases for visually-hidden live regions, but the general rule of thumb is that **if the update or message is visible to all users and the conveyed text is equivalent to the visible text (as is the case for most status messages), then you might as well use the same element for screen reader users that you are using for everyone else** , and designate it as a live region so that all users get the same update. For example, consider the dummy email example from the previous section again. To convey the same status message to screen reader users, all you would need to do is designate the message container as a live region using the `aria-live` property. Error notifications are urgent and require the user’s immediate attention, and you want the user to know that an error has occured as soon as possible. As such, the value of `aria-live` should be `assertive`. <div aria-live="assertive"></div> At this point it is important to note that you should place the live region container in the DOM as early as possible and _then_ populate it with the contents of the message using JavaScript when the notification needs to be announced. **This ensures that the live region is monitored for updates before they happen.** Otherwise, the update may not be communicated to screen readers. We will learn more about best practices for implementing live regions later in the chapter. By default, any padding, margin, and border on an element will take up space in the page’s layout even if the element is empty. Since the message container is placed in the DOM before the notification is shown, you will probably want to prevent it from taking up any visual space on the page when it is empty. To do that, you can use the `:not(:empty)` CSS selector to only apply the visual styles to it when it is _not_ empty (i.e. when the notification is shown). [aria-live="assertive"]:not(:empty) { padding: .25em 1em; background: maroon; ... } Because it is a best practice to include live region containers in the DOM as early as possible, I always use this CSS selector to visually “hide” my live regions when they are empty. And yes, yes I know that you can just apply these styles to the notification using a class name that you could add to the container via JavaScript, but why require JS for something so simple that can so easily be accomplished using CSS? Here is a live demonstration of this implementation: See the Pen Live region status message in email web app by Sara Soueidan (@SaraSoueidan) on CodePen. Fire up a screen reader on the debug version of this demo and then activate the ‘Send’ button. Try removing the `:not(:empty)` selector from the live region’s ruleset in the to see how it affects the visibility of notification container when it is empty. A live region does not need to be initially empty. Here’s another example where I have a list and I’m adding items to the list. I’ve designated the list as a assertive live region using `aria-live`. So now every time an item is added, the screen reader makes an announcement. See the Pen #PracticalA11y: Basic live region by Sara Soueidan (@SaraSoueidan) on CodePen. This means that you can use live regions to communicate different types of updates to an element, such as when content is added to the element or existing content is modified. ### Live region configuration ARIA provides three attributes that enable you to ‘configure’ when the screen reader should make an announcement, and what that announcement should contain: * `aria-relevant`, * `aria-atomic`, and * `aria-busy`. These attributes are _very_ useful and would enable you to use live regions to communicate different kinds of content updates when they are needed, but unfortunately current browser and screen reader support is inconsistent, so you can’t rely on them in your projects just yet. But we’re still going to get a quick overview of what they do because it will help you better understand the current limitations with ARIA live regions. #### aria-relevant: when should an announcement be made? The `aria-relevant` attribute is used to specify **what type of changes in the live region should trigger an announcement.** For example, should the screen reader make an announcement when a node is _added_ to the region? or when a node is _removed_? or when the text within an element changes? or maybe when any of these updates happen? `aria-relevant` accepts a space-separated list of the following values: `additions`, `removals`, `text`, and a single catch-all value: `all`. * `additions` will trigger a notification **when a DOM node is added to the region.** * `removals` will trigger a notification **when a DOM node is removed from the region.** * `text` will trigger when **text changes happen inside the region** , such as changing a text node inside the region or changing a text alternative for an image inside the region. * `all` is a shorthand for all three options. The default value is `additions text`, which means that a live region will trigger an announcement when content is added or text is changed within the region. The `removals` and `all` values should be used sparingly. Screen reader users only need to be informed of content removal when its removal represents an important change, such as when a user is removed from the list of active users in a chat room, for example. #### aria-atomic: what is contained in an announcement? The `aria-atomic` attribute determines what is contained in the announcement. It indicates whether the screen reader should present all or only parts of the changed element based on the change notifications defined by the `aria-relevant` attribute. For example, if a piece of text changes inside an element, should the screen reader announce only the changed text? or the entire contents of the live region? If text is _added_ to a live region, should only the newly added text be announced? or should the entire region’s content be announced every time? `aria-atomic` accepts two values: `true`, and `false`. * When `aria-atomic` is `false`, a screen reader should only announce the parts of the element that have changed. **This is the default value.** * When `aria-atomic` is `true`, the screen reader should announce the entire contents of the live region when a change happens inside of it. It doesn’t matter what has changed. It’s going to read everything — the entire content of the live region, plus the region’s accessible name, if it has one. `aria-atomic="true"` is useful for when a part of the region changes but you want the entire content to be read because otherwise the updated content may not make much sense on its own. A practical example is a “Now Playing” indicator. <p> <span>Now Playing:</span> <span>[ movie/soundtrack title ]</span> </p> If a playlist of movies or soundtracks is playing while the user performs other tasks on the page, and the name of the soundtrack that is currently playing changes, you can utilize live regions to announce that a new soundtrack is playing. When the soundtrack changes, the only part of the indicator that gets updated is the soundtrack’s name. But you want the entire sentence to be announced so that the user gets the context they need. You can do that by designating the indicator as an atomic live region (using `aria-atomic="true"`): <p aria-live="polite" aria-atomic="true"><span>Now Playing:</span><span>[ movie/soundtrack title ]</span></p> Now every time the title of the soundtrack or movie changes, the screen reader should announce “Now playing” followed by the name of the soundtrack. Here is a live demo: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. Start a screen reader and try out the debug version of the playing indicator #### aria-busy: please wait until the changes are complete The `aria-busy` attribute is used to indicate that an element (typically an entire section on the page) is undergoing changes (such as a section loading new content), and that screen readers should therefore **wait until the changes are complete before exposing the content to the user**. By default, all elements have an `aria-busy` value of `false`. Meaning that they are _not_ undergoing changes and screen readers can, therefore, expose their content when the user navigates to them. To use `aria-busy`, you would set it to `true` on an element while the element is undergoing changes, and then flip its value to `false` when the changes are complete and ready to be exposed or announced to the user. `aria-busy` can be used on any element that is undergoing changes, even if that element is not a live region. If you use `aria-busy` on a live region, the contents of the live region will be announced after `aria-busy` is set to `false`. If multiple changes have been made to the element while it was busy, they are announced as a single unit of speech when `aria-busy` is turned off. ‘Skeleton screens’ in Single Page Applications (SPA) are a practical use case for the `aria-busy` attribute. A skeleton screen is a specific type of loading indicator that is shown in lieu of the content of a section being loaded until the content of that section loads. They often provide a wireframe-like visual that mimics the layout of the page and helps users build a mental model of what will be on the page when the content loads. `aria-busy` can be used to tell screen readers to ignore the section that is currently loading content until the content finishes loading. In that sense, it has a similar effect to `aria-hidden` — it ‘hides’ the contents of a busy region while it is undergoing changes. <!-- This section is updating... --> <section aria-busy="true"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> Since the busy section is effectively hidden from screen reader users, you will want to communicate the state of the loading content to screen reader users. You can do that by using a separate, visually-hidden live region. Using this region, you can communicate to the user that a screen has started loading, and then let them know when the loading is complete. So, when the content is loading, your markup would at a certain moment look like this: <div aria-live="polite" class="visually-hidden">Loading content...</div> <section aria-busy="true"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> and then when the content is loaded, flip the value of `aria-busy` to `false`, and update the content of the live region: <div aria-live="polite" class="visually-hidden">Content loaded.</div> <section aria-busy="false"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> This is an example of when a live region can be used exclusively for notifying screen reader users, but doesn’t need to be rendered visually because there is an alternative visual indicator for sighted users (the skeleton screen, in our case). So you can think of the live region like a text alternative for the skeleton screen in this case. Unfortunately, because `aria-busy` is currently not well-supported across screen reader and browser pairings, most screen readers (except JAWS) will read the contents of the busy region even before it’s done loading, which would result in a sub-optimal experience. You currently need to work your way around that by hiding the busy region using `aria-hidden`, and un-hiding it when its contents are done loading. For a detailed writeup about implementing accessible skeleton screens, check out Adrian Roselli’s article “More Accessible Skeletons”. Adrian provides a solution that doesn’t even require you to use live regions at all, and his article includes a live demo that you can tinker with and try for yourself. #### Summary and support landscape Properties for configuring live region announcements Value | Description ---|--- `aria-atomic` | _What is announced? When you update a live region, should it read all the content again or just the added content?_ If `true`: Announce the entire content of the live region, including its label, if present. If `false`: announce only the changed content. `aria-relevant` | _When is an announcement made? What types of changes to a live region should trigger the announcement? additions? removals? or all?_ If `additions`: Trigger an announcement when new elements are added to the accessibility tree of the live region. If `text`: Trigger an announcement when text content or a text alternative is added to any descendant in the accessibility tree of the live region. If `removals`: Trigger an announcement when an element, text, or text alternative is removed from the accessibility tree of the live region. If `additions text (default)`: Equivalent to the combination of `additions` and `text`. If `all`: Equivalent to the combination of `additions removals text`. `aria-busy` | Indicates that an entire section on the page is undergoing changes (such as a section loading new content), and you're telling screen readers to **wait until the changes are complete before notifying the user** If `true`: The element is being updated. If `false`: There are no expected updates for the element. As we mentioned earlier, **support for the`aria-relevant`, `aria-atomic`, and `aria-busy` attributes is currently inconsistent across browsers and screen reader pairings.** Paul J. Adam has created a test page that includes test cases for `aria-atomic` and `aria-relevant` when used on live regions, and has documented support gaps across platforms and screen readers. So, unfortunately, you can’t rely on these properties in your projects just yet. If you do, many of your content updates may be announced in ways that you did not intend them to be announced, which could result in a sub-optimal user experience. ### 2. Using live region roles When you use `aria-live` to create a live region, the element’s implicit semantics (if it has any) are retained. This means that you can use the appropriate element to represent the component you’re creating, and if the component is getting updated you can then designate it as a live region with the `aria-live` attribute. <!-- this list will be treated like any list on the page would be; since it is also designated as being live, any changes that happen to it should be communicated to screen readers and announced to the user --> <ul aria-live="polite"> <li>My list semantics are important.</li> <li>But I want you to know when new list items are added.</li> </ul> But what if you’re creating a notification or status message that has no semantic HTML element to represent it? For example, there are no semantic elements to represent (and distinguish between) different types of notifications (such as an alert notification or a status message, for example). While it is fine to use a `<div aria-live="">` for these notifications, it would be ideal if we exposed the nature or type of a notification to the user using appropriate semantics. ARIA provides five live regions roles that semantically represent five different types of updates: * **The`alert` role:** represents a live region with important, and usually time-sensitive information, such as error notifications. * **The`status` role:** represents a live region whose content is advisory information for the user but is not important enough to justify an alert, often but not necessarily presented as a status bar (such as a status or success message). * **The`log` role:** represents a live region where new information is added **in meaningful order** , and old information may disappear. Examples of logs are chat logs, messaging history, a game log, or an error log. In contrast to other live regions, **in this role there is a relationship between the arrival of new items in the log and the reading order.** The log contains a meaningful sequence and new information is added only to the end of the log, not at arbitrary points. * **The`marquee` role:** represents a live region where non-essential information changes frequently, such as stock tickers. The primary difference between a marquee and a log is that logs usually have a meaningful order or sequence of important content changes. * **The`timer` role:** represents a live region containing a numerical counter which indicates an amount of elapsed time from a start point, or the time remaining until an end point. Live region roles are **pre-configured**. They come with _implicit_ `aria-live` and `aria-atomic` values. ARIA live region roles and their implicit `aria-live` and `aria-atomic` mappings Role | `aria-live` value | `aria-atomic` value ---|---|--- `alert` | `assertive` | `true` `status` | `polite` | `true` `log` | `polite` `marquee` | `off` `timer` | `off` `alert` and `status` are the most commonly used live regions roles and have generally good support. The others have specialized uses and have **poor or no support** , and `marquee` and `timer` are even in danger of being deprecated and removed from the ARIA specification. #### Difference between using `aria-live` and live region roles The primary difference between using live region roles and using `aria-live` on its own is that **live region roles have semantic meaning.** They add explicit _semantics_ to an element ("This is an alert", "This is a status message", etc.), so some screen readers may announce “alert” before announcing the content of the message. For example, here is the dummy email app example again. Instead of using `aria-live="assertive"` on the notification container, I’m using `role="alert`. Here’s a video comparing how NVDA announces the notification, first when it is designated as a live region using `aria-live="assertive"`, and the when it is designated as a live region using `role="alert"`. Sorry, your browser doesn't support embedded videos. NVDA announces the word “Alert” before announcing the content of the notification when `role="alert"` is used. You can try it for yourself in the debug version of the demo using the role attribute. Here is another example that implements a form success message using `role="status"`: See the Pen #PracticalA11y: role status success message by Sara Soueidan (@SaraSoueidan) on CodePen. Another advantage to using a live region role over `aria-live` is that **live region roles accept an accessible name.** If you use `aria-live` to create a live region, the implicit semantics of the element you’re using it on will determine whether or not it can have an accessible name. As we learned in the accessible names and descriptions chapter, some elements are name-prohibited. For instance, a `<div>` will not consistently expose an accessible name unless you give it a meaningful role. ARIA live region roles provide meaningful roles to the elements they are used on and can therefore accept an accessible name. When a live region has an accessible name, screen readers include the name of the region in the announcement. See the Pen #PracticalA11y: shopping cart by Sara Soueidan (@SaraSoueidan) on CodePen. In this example I have a polite live region that contains the number of items in the user’s shopping cart. When the ‘Add to cart’ button is activated, the number of items is updated and the screen reader announces that number. But anouncing the number of items alone doesn’t provide the user with the same context that sighted users get when the shopping cart is visually updated. Ideally, you’d want the screen reader to announce “Shopping cart, 5 items”. Using `aria-labelledby`, you can provide an accessible name to the live region (namely: “Shopping Cart”). So now when the number of items is announced, the screen reader announces ‘Shopping cart’ before announcing the number of items it contains. You can try the live example out for yourself in the debug version of the shopping cart example. Providing an accessible name to a live region is useful for when you may have multiple updating regions on the page and you want to communicate which region the updates are coming from. A region’s name thus provides the necessary context for each announcement. ### 3. Using the HTML `output` element HTML provides one native live region element: `<output>`. By definition, `<output>` represents an element into which you can inject the results of a calculation **or the outcome of a user action.** The second part of the definition can be interpreted to mean that the `<output>` element can be used to display a feedback message as a result of user interaction (like a toast or status message!). The `<output>` element has implicit live region semantics. It maps to the ARIA `status` role, which means that it represents a polite live region. `<output>` is also a labelable element, which means that you can give it an accessible name using the `<label>` element. <label for="[ outputID ]">..</label> <output id="[ outputID ]"> .. </output> <!-- or --> <label for="[ outputID ]"> .. <output id="[ outputID ]"> </output> </label> A practical use case for the `<output>` element is using it to represent the total price of products in a cart on an e-commerce website. <label for="result">Your total is:</label> <output id="result"> </output> <!-- or --> <label for="result"> Your total is: <output id="result"> </output> </label> When the user updates the number of items in their cart, the total price is updated to reflect the new total price. Wrapping the price in an `<output>` element allows it to be announced by the screen reader when it is updated. Here is a dummy example where the total price is updated based on how many items are chosen in the select dropdown: See the Pen #PracticalA11y: The <output> live region element by Sara Soueidan (@SaraSoueidan) on CodePen. You have probably also noticed that VO with Safari announces the initial total value before it announces the updated total value every time it makes an announcement. The `<output>` element is currently not consistently announced across browser and screen reader pairings. And not all screen readers announce the accessible name of the `<output>` when its content is updated. For example, VoiceOver with Safari announces the content of the `<output>` element in the example above but it does not announce its accessible name. NVDA with Firefox does not announce the accessible name either. Whereas paired with Chrome, VoiceOver announces the contents of `<output>` with its accessible name as it is intended. There are also other quirks and some inconsistencies in the way `<output>` is currently announced across browser and screen reader pairings. Accessibility engineer Scott O’Hara has written a great article about the `<output>` element that I recommend reading if you want to learn more details about `<output>` and its quirks. Scott shares the current state of support, as well as suggestions for working around some of the support gaps. ## Summary and outro So, to quickly sum up: * ARIA live regions are a specific type of notification system primarily surfaced for screen reader users. * You can create a live region using the `aria-live` attribute. The value of the attribute depends on the type and urgency of the updates you’re communicating. * The `aria-relevant`, `aria-atomic`, and `aria-busy` attributes allow you to configure when an announcement should be made and what the announcement should contain. But support for these attributes is currently poor. * ARIA provides five roles that represent five different types of updates. Of these five roles, `alert` and `status` have the best support and can be used to represent status messages in web applications. * The `<output>` element is currently the only native HTML live region. The `<output>` element has a few quirks and some support gaps that, depending on your use case, you may be able to work around today. As you might imagine, the current state of support for live region features and properties limits your uses of live regions quite a bit. Furthermore, the inherent behavior of live regions also makes them unsuitable for certain types of updates. We’re going to elaborate more on this in the second part of this chapter. Fortunately, for many (if not most!) common UI patterns, there’s often a more robust way to make users aware of content updates when they happen. And for the few instances when you do need to use live regions, you can make them work by following a few implementation best practices. We will discuss all of that in more detail in the second part of this chapter. Many thanks to **James Edwards** (@siblingpastry) for reviewing this chapter.
www.sarasoueidan.com
September 25, 2025 at 11:47 PM
CSS to speech: alternative text for CSS-generated content
Changelog * Despite the fact that Chrome exposes the alt text of a CSS image as part of the accessible name of the element it is used on, James Scholes shared that the alt text is not announced by NVDA, which means that screen reader users using this popular browser-screen reader combination will miss out on meaningful information. * Removed the phrase that said that implied that CSS psuedo-elements are not exposed the same way that HTML elements are exposed in the accessibility tree. Chrome and Firefox now do expose pseudo-elements in the accessibility tree. This post is sponsored by everyone who bought my Practical Accessibility course. 💐 The CSS `::before` and `::after` pseudo-elements are used to insert presentational content before and after (respectively) existing content in an HTML element. The `content` property is used to define _what_ content is inserted in these elements. For example, the following CSS adds the text "Error: " before the content of an error message container: <div class="error-message">The value you entered is invalid.</div> .error-message::before { content: "Error: "; } This example adds an SVG chevron icon to a button that toggles the display of some content: <button class="disclosure-widget">License Agreement</button> button.disclosure-widget::before { content: url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m8.586 5.586c-.781.781-.781 2.047 0 2.828l3.585 3.586-3.585 3.586c-.781.781-.781 2.047 0 2.828.39.391.902.586 1.414.586s1.024-.195 1.414-.586l6.415-6.414-6.415-6.414c-.78-.781-2.048-.781-2.828 0z'/%3E%3C/svg%3E"); width: 1em; height: 1em; } This example adds the ↗ unicode character to indicate that the anchor links to an external resource or opens in a new tab or window: <a href="..." target="_blank" class="external">CSS Generated Content specification</a> a.external::after { content: " \2197" ; /* ↗ = North East Arrow */ } The contents of CSS pseudo-elements is exposed as part of the content of the element it is inserted into. And, importantly, **CSS content takes part in the accessible name computation of the element they are used on.** ## CSS pseudo-content in the accessible name computation algorithm When the browser needs to determine the accessible name of an element, it uses an algorithm called “Accessible Name and Description Computation” algorithm. According to the specification: when an element’s name is derived from its contents, the browser must include the textual contents of CSS pseudo-elements in the accessible name. This includes the contents of the `::before` and `::after` pseudo-elements, as well as the `::marker` when an element supports `::marker`. > Check for CSS generated textual content associated with the current node and include it in the accumulated text. The CSS `:before` and `:after` pseudo elements can provide textual content for elements that have a content model. > > * For `:before` pseudo elements, User agents MUST prepend CSS textual content, without a space, to the textual content of the current node. > * For `:after` pseudo elements, User agents MUST append CSS textual content, without a space, to the textual content of the current node. > For example, if we look at the link from the example in the previous section: <a href="..." target="_blank" class="external">CSS Generated Content specification</a> a.external::after { content: " \2197" ; /* ↗ = North East Arrow */ } …the accessible name for the link is going to be “CSS Generated Content specification ↗”. If you open the browser DevTools and check the accessibility information of the link, you will see the unicode character included in the accessible name. The name of the link will be read by a screenreader as “CSS Generated Content specification North East Arrow” or “CSS Generated Content specification Upright Arrow”. The screen reader announces the unicode character by its default alternative text, which does not commnunicate the _intended_ meaning of the character. **Where do screen readers get the alternative text for unicode characters from?** Steve Faulkner shares that "the text alternatives for Unicode symbols are usually contained within a text file in screen reading software’s program files directory." Emojis, like other unicode characters, also have default text alternatives that may differ slightly across platforms, as Steve demonstrates in his article. The text alternatives of emojis usually describe what the emoji is, not necessarily what you might be using it for. As Craig Abbott mentions in his recent writeup about integrating AI into screen readers, "the “red flag” emoji is actually announced as “triangular flag on post”, which does not usually provide enough context for the way that emoji is used in our culture". Not to mention that the same emoji may be interpreted differently by different sighted users! This is why using emojis to communicate meaningful information is generally not recommended. Images and characters inserted in CSS should be treated the same way you treat HTML images: **when an image (or character) is meaningful, it must be described to assistive technology (AT) users using alternative text that communicates its purpose.** This ensures that AT users are getting the same information as sighted users. Providing descriptive alternative text to meaningful images is a WCAG requirement. In HTML, you provide the alternative text of an image in the `<img>`'s `alt` attribute: <img src="/path/to/meaningful/image.jpg" alt="[ the text describing the purpose of the image to someone who can't see the image ]"> On the other hand, when the image is purely decorative, it should be hidden from screen readers and excluded from an element’s accessible name to avoid unnecessary or confusing announcements. In HTML, an image is marked as decorative by giving it an empty `alt` text. The empty `alt` value ensures that the image is not exposed and announced by screen readers: <!-- When the image is decorative, leave the alt text empty. --> <img src="/path/to/decorative/image.png" alt=""> But how do you provide alternative text to CSS-generated content to make sure it is announced (or not announced!) as expected? ## Alternative text for CSS generated content Historically, there hasn’t been a standard way to provide alternative text for CSS generated content and images. So, we resorted to creating placeholder `<span>`s in the markup for this purpose. For decorative CSS content, we inserted the content into a `<span>` and then used `aria-hidden="true"` to hide that `<span>` from screen reader users. <a href="..." target="_blank" class="external"> CSS Generated Content specification <span aria-hidden="true" class="icon"></span> </a> a.external .icon::before { content: " \2197"; } And to provide descriptive alternative text to otherwise meaningful graphics, we created an additional, visually-hidden `<span>` that included the alternative text of the graphic and that is exposed to screen reader users only: <a href="..." target="_blank" class="external"> CSS Generated Content specification <span aria-hidden="true" class="icon"></span> <span class="visually-hidden"> (Opens in a New Window)</span> </a> But there’s a better way to handle alternative text for CSS content today. We can now provide alternative text for CSS-generated content directly in CSS, after a slash following the content. According to the CSS Generated Content Module Level 3 specification: > Content intended for visual media sometimes needs alternative text for speech output or other non-visual mediums. The `content` property thus accepts alternative text to be specified after a slash (/) after the last `<content-list>`. If such alternative text is provided, it must be used for speech output instead. > > This allows, for example, purely decorative text to be elided in speech output (by providing the empty string as alternative text), and allows authors to provide more readable alternatives to images, icons, or text-encoded symbols. This means that for meaningful icons, it looks like this: a.external::after { content: " \2197" / "Opens in a New Window" ; } For decorative icons, it looks like this: .disclosure-widget::before { content: url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m8.586 5.586c-.781.781-.781 2.047 0 2.828l3.585 3.586-3.585 3.586c-.781.781-.781 2.047 0 2.828.39.391.902.586 1.414.586s1.024-.195 1.414-.586l6.415-6.414-6.415-6.414c-.78-.781-2.048-.781-2.828 0z'/%3E%3C/svg%3E") / ""; } In non-supporting browsers, the slash syntax will invalidate the entire declaration, which means that you would need to precede the new syntax with a fallback for non-supporting browsers. _That being said_ , support for alt text in the `content` property is very good today. So unless you need to support Internet Explorer or other older non-supporting browsers, you shouldn’t need to worry about the fallback. ## CSS alt text in browsers today I created a test page (embedded below) to test and demonstrate how browsers handle alt text for CSS generated content today, and how the content is exposed and announced by screen readers. The page contains two tables: The first table contains examples of different types of CSS generated content,some of that content is intended to be decorative and some is intended to be meaningful. For each type, I created three instances: one without alt text (alt text not included in CSS declaration), one with non-empty, descriptive alt text, and one with empty alt text (alt text is empty but not omitted). In the second table, I provided an HTML image for comparison. The HTML image table contains valid and invalid image references to demonstrate if and how the image’s alternative text is rendered in the browser. It also contains two instances of each of these images: one with non-empty alt text, and one with empty alt text. Here is a video demonstrating how VoiceOver on macOS announces each of the examples in the table. Sorry, your browser doesn't support embedded videos. Navigating the buttons and links with CSS generated content using VoiceOver on macOS paired with Safari v18.4. I got the same results with VoiceOver on iOS 26, and with VoiceOver on macOS when I paired it with both Chrome and Firefox on macOS. Some observations from the above test: * CSS images inserted using the `url()` function that don’t have an alt text provided are excluded from the browser’s accessibility tree by default and are, therefore, not announced by screen readers. These are images with _no_ alt text at all. * All images with an empty alt text value are also excluded from the accessibility tree. This is the expected behavior. * Images with non-empty alt text are announced by their alternative text. And the alt text of the image is announced as part of the accessible name of the element. * Images with broken references and non-empty alt text are also exposed and announced by their alternative text, and the alternative text of the image is announced as part of the accessible name of the element. This is the expected behavior. * Like images with valid references, images with broken references are not exposed when they don’t have any alt text provided. * Emojis and unicode characters are announced by their default alternative text, except when you override the alternative text in CSS. And finally, the alt text of a broken CSS image is not shown in place of the image when the image is not shown. The only exception on desktop is Safari on macOS which _does_ show the alt text of the image in a large square placeholder **but only when VoiceOver is turned On, or upon turning it Off**. In other words, Safari does not display the alt text of a broken CSS image by default. But if you enable VoiceOver—say by pressing `CMD + F5`, the broken image is replaced with a large, full-width square that contains the alternative text of the image. You can see that in action in the video recording above. Safari on iOS 26, on the other hand, did show the alt text in the large square placeholder in my tests whether VoiceOver was On or Off. I got very similar results with JAWS (2025) paired with Chrome (v140) and with NVDA (2025) with Firefox (v142) on Windows. The main difference from VoiceOver macOS that stood out is that NVDA announces “graphic” after the alt text of the CSS image inserted using the `url()` function. NVDA paired with Chrome, however, currently does not announce the CSS alt text, as James Scholes shares in the comments. Despite the fact that Chrome exposes the alt text of a CSS image as part of the accessible name of the element it is used on, the text is not announced with NVDA, which means that screen reader users using this popular browser-screen reader combination will miss out on meaningful information. I haven't tested on Android because I don't have access to an Android device. If you do, please feel free to share your results in the comments! Support for alt text for CSS generated content has improved notably since I did the same tests last year ahead of my CSS Day talk. Last year, Chrome exposed CSS images (inserted using `url()`) even when they had empty alt text. I was pleasantly surprised to see that all browsers now behave the same way in terms of exposing alt text in CSS. Despite that, screen readers don’t all announce this alt text consistently at the time of writing. ## Don’t use CSS to insert meaningful content and images While it’s good and certainly useful that we can now provide alt text for CSS generated content, **I do not recommend using CSS to insert _meaningful_ content into the page.** Not only will some of your screen reader users miss meaningful information when the alt text is not announced by their screen reader (looking at NVDA with Chrome 👀), but there are also other reasons why inserting meaningful content in CSS is not yet recommended… ### The CSS alt text is not shown when a CSS image is not rendered The alt text of a CSS image is not shown in place of the image when the image itself doesn’t load. If the image is meaningful and it is not shown for some readon, then its meaning will be lost on most users. And the missing alt text can make interactive elements unusable by speech control users. As I mentioned in the previous section, the only browser that currently shows the alt text of a broken CSS image is Safari on iOS and Safari on macOS with VoiceOver enabled. The non-empty alt text of a CSS image still contributes to the accessible name of the element it is used on even when it is not rendered visually. This creates a mismatch between the visible label of the element and its accessible name. When there’s a mismatch between the visible label and the accessible name of an element, particularly when the element is interactive, it becomes more tedious for speech control users who rely on visible labels to activate controls. Furthermore, depending on the position of the CSS image’s alt text in the accessible name, this mismatch may create an immediate violation of WCAG Success Criterion 2.5.3 Label in Name. The alt text of an HTML `<img>`, on the other hand, is _more likely_ to be shown in most browsers. ### CSS Generated content does not currently translate via automated tools CSS generated content does not currently translate via automated tools. As Adrian Roselli notes in his article on alternative text for CSS generated content, "in the future, `attr("data-alt")` can potentially get around that unless you rely on automated translation tools. If you need your content to auto-translate, then the CSS approach is not for you." ### CSS content is only accessible when your CSS is used CSS content is only accessible when your CSS is used. But **not all assistive technologies will render your stylesheets when displaying the page’s content.** An example of an assistive technology that replaces your style sheets with custom ones is Reader Mode. If you’re using CSS to insert meaningful content, this content will not be presented to users viewing the page in the browser’s Reader Mode or in a reading app. ### CSS content is (currently) not searchable and selectable At the time of writing of this post, CSS content is not searchable using the browser’s Find in Page functionality, nor is the generated text selectable. If you try searching for CSS-generated text on a page, you’ll notice that the browser won’t “find” or highlight the text, even if it is present on the page. And if you try to select CSS-generated content to copy it, for example, you’ll find that you can’t. This makes CSS generated content less usable. Note that the CSS Generated Content specification states in the Accessibility of Generated Content section that "generated content **should** be searchable, selectable, and available to assistive technologies" (emphasis mine). So if browsers follow the spec recommendation in the future we might get different results. ## So, what should you do instead? If the content is integral to the understanding of the page, it should be in your HTML. For text content, use HTML text. For images, use an HTML `<img>`, and provide a descriptive alternative text in the image’s `alt` attribute. If you _do_ insert meaningful content in CSS, make sure you give it a descriptive alt text that describes its purpose. ## Does this mean that alt text in CSS isn’t useful? No, it doesn’t. Not at all! I’ve seen arguments that because meaningful content belongs in HTML, and since you should use CSS-generated content for decorative content only, then you’ll probably never need the alt text feature. I find the contrary to be true! _Because_ you should only provide _decorative_ content using CSS, and _because_ CSS-generated content contributes to the accName of an element, and because you don’t want that content to cause unwanted screen reader announcements, you will want to hide that content from assistive technologies in order to improve the user experience. This is where CSS alt text provides the most utility: hiding decorative CSS images and text from screen readers. You can get really creative with CSS-generated content. CSS pseudo-elements have long been used to create visual text effects to pages, and Mandy Michael discusses an example of such effects in a blog post she wrote. As Mandy shows, hiding the duplicate text in CSS is essential, otherwise the text will be announced multiple times, resulting in a bad screen reader user experience. Just keep in mind not to leave the alt text out when the content is decorative, otherwise it _will_ be announced (except in the case of an image added using `url()`), resulting in unnecessary and unhelpful screen reader announcements. ## Summary and outro 1. Ideally, avoid using CSS pseudo-elements for meaningful content. Prefer using HTML. 2. If you do insert meaningful content in CSS, ensure it has descriptive alt text that communicates its purpose. 3. ‘Hide’ decorative or redundant CSS-generated content by giving it an empty alt text. And remember that support may change, bugs may be fixed, others may appear, heuristics may be introduced, and things may stop working the way they have always worked. So make sure to **always perform your own testing** to ensure that your content and component works as expected. ## Recommended reading I wanted to keep this post short and only discuss the current state of alternative text for CSS generated content, as well as general recommendations to get the most out of this feature. If you’re interested in learning more about any of the topics I mentioned throughout the post, consider reading these articles: * Alternative text for CSS generated content * alt text for CSS generated content * The problem with data- attributes for text effects * Voice Control Usability Considerations For Partially Visually Hidden Link Names ## Continue learning Did you know that the `alt` text is only one of _five_ different ways the browser computes the accessible name of an image? For example, did you know that if the image has no `alt` attribute (i.e. it is omitted) and no `title` attribute, and the `<img>` is contained in a `<figure>` that only contains the `<img>` and a `<figcaption>`, then the browser will use the text equivalent of the `<figcaption>` to give the image an accessible name? Understanding how browsers compute the accessible name of an element, and understanding what _you_ need to do to give your images accessible names is like having accessibility superpowers. Not only will you know what to do to make your images accessible, but you will also have a toolkit of techniques at hand that you can reach for when you need to _fix_ existing images in your codebases. There are least three different ways to provide an accessible name to elements **that are allowed to have one**. What happens when multiple naming methods collide? HTML label? ARIA attribute? CSS content? And what happens when multiple naming methods collide? Which one “wins” in the accessibility tree? ⚖️ If you’re ready to stop guessing and to _really_ understand web accessibility, then I highly recommend enrolling in the Practical Accessibility course. Practical Accessibility is a structured curriculum that will equip you with the foundational knowledge you need to start creating more accessible websites and web applications today. Learn more about the course and enroll at practical-accessibility.today. Thank you for reading!
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
In Quest of Search
**Update:** There now exists a native HTML `<search>` element that maps to the ARIA `search` role. 🎊 As of March 24th, 2023, the HTML specification added a new grouping element: The `<search>` element. Read more about the element in Scott’s introductory blog post. There’s been a recent discussion on Twitter about the idea of adding a new element in HTML that represents a search interface. A search form, basically. The idea is to create a semantic HTML element for the ARIA `search` role, which represents a landmark region “that contains a collection of items and objects that, as a whole, combine to create a search facility.” Opinions have been shared in the Twitter thread about whether adding a new HTML element is necessary. Many have argued that it was unnecessary because we can use the ARIA `search` role and repurpose a `form` element to create the same semantics. I disagree. And this article is the longer version of **my personal opinion** on the subject. ## tl;dr I do strongly encourage the addition of a new HTML element that represents—and can consequently obviate the use of—the ARIA `search` landmark role. A search element would provide HTML parity with the ARIA role, and encourage less use of ARIA in favor of native HTML elements. The suggested element would be syntactic sugar for `<div role="search">` like `<main>` is syntactic sugar for `<div role="main">`. This means that it would an HTML sectioning element, not a replacement for another element. I would choose `<search>` as a name for that element. In my mind, `<search>` would be to `role="search"` what `<nav>` is to `role="navigation"`. But any other appropriate name would, of course, also work. The rest of this article is my reasoning for encouraging the idea of adding a semantic HTML element for search. ## HTML and ARIA landmark roles The ARIA specification includes a list of ARIA **roles** that are used to define regions of a page as landmarks: * `banner` * `complementary` * `contentinfo` * `form` * `main` * `navigation` * `region` * `search` HTML currently contains 112 elements. Eight of those elements are sectioning elements: `main`, `nav`, `aside`, `header`, `footer`, `article`, `section`, `form`. Seven of these HTML sectioning elements are mapped to ARIA landmarks, which are used by assistive technologies (ATs). * `header` is the HTML native equivalent for ARIA’s `role="banner"` (when it is scoped to the `body` element. See HTML Accessibility API Mappings for more information.) * `footer` is the HTML native equivalent for ARIA’s `role="complementary"` (also in the context of the `body` element) * `nav` is the HTML native equivalent for ARIA’s `role="navigation"` * `main` is the HTML equivalent for ARIA’s `role="main"` * `form` is the HTML equivalent for ARIA’s `role="form"` * `aside` is the HTML equivalent of ARIA’s `role="complementary"` * `section` is the HTML native equivalent for ARIA’s `role="region"` (when it has an accessible name) It is because these elements exist that we often don’t need to use ARIA’s equivalent roles (unless we absolutely _have_ to repurpose another element using those roles, or expose an element to ATs when it is outside of its expected context). If `<nav>` exists, why should a `<search>` (or whatever other name it gets) not? If `<search>` is to be deemed unnecessary because `role="search"` exists, wouldn’t this also mean that `<nav>` (and other landmark elements) would be considered _redundant_ because `role="nav"` (and other ARIA roles) exists? ## HTML and ARIA landmarks, beyond semantics ARIA landmark roles are roles assigned to regions of a page that are intended as **navigational landmarks**. Using ARIA landmarks (or their equivalent native HTML elements when they exist) is meant to also facilitate user navigation. From the W3C WAI-ARIA Editor’s Draft: > Assistive technologies SHOULD enable users to quickly navigate to elements with role search. User agents SHOULD treat elements with role search as navigational landmarks. User agents MAY enable users to quickly navigate to elements with role search. When HTML sectioning elements (and/or ARIA landmark roles) are appropriately used on a page, assistive technology users such as screen readers users could use those landmarks to navigate the page more efficiently, allowing them to jump to the area of the page that they want. For example, if the `<nav>` element (or, equivalently, the `role="navigation"` ARIA role on a qualifying element) is used to wrap a page’s navigation, the navigation shows up in the VoiceOver Rotor on macOS. Similarly, using the `main` element will make the main section of the page show up in the landmarks menu. The user can then quickly jump straight to the navigation section or to the main content area of the page if they want to, bypassing other regions of the page. This increases the user’s efficiency and improves their navigation experience. Similarly, when you use `role="search"` on a `form` element, that form will show up as a search region in the landmarks menu. The user can then jump to the search form if they need to quickly search for something. The search form on WebAIM's Web site shows up in the Landmarks menu by VoiceOver on macOS because `role="search"` ARIA role is present on the `form` element. The search form on Smashing Magazine's Web site is not recognized as a search landmark by VoiceOver on macOS because `role="search"` ARIA role is absent on the `form` element. _If HTML sectioning elements are used without understanding the associated landmark structure, assistive technology users will most likely be confused and less efficient in accessing content and interacting with web pages._ ### But is a native search landmark worth it? Yes, it is. Search is one of the most common and most used sections of many Web sites. Of course, a “It Depends” is warranted here, too. Depending on the Web site, search might be the first thing a user looks for and uses on a given site. E-commerce Web sites are a great example of where search forms are essential and heavily used. Educational and documentation sites are another example. Take MDN, for example. Search is so important and on MDN that the site even includes a Skip Link that enables keyboard users to skip straight to the search field. Now I don’t have any user research data or anything, but I would assume that the skip link was added because of how frequently users reach for the search field to look up documentation about specific topics they’re searching for. ## Just because an ARIA role exists, it doesn’t eliminate the usefulness of a native HTML equivalent I’ll just say it again: ust because an ARIA role exists, it doesn’t eliminate the usefulness of a native HTML equivalent. ## The purpose of ARIA …is to provide parity with HTML semantics. It is meant to be used to **fill in the gaps** and provide semantic meaning where HTML falls short. ARIA is **not meant to _replace_ HTML.** If anything, the need to use ARIA as ‘polyfill’ for HTML semantics could be considered as a sign and a constant reminder of the fact that HTML falls short on some semantics that benefit users of assistive technologies. This is due to the lack of native HTML elements that provide the meaning (and sometimes, by extension, the behavior) that these ATs need to convey to their users. If we can get an HTML element that fills a part of the gap, it’s only going to be a win—no matter how small of a win it might seem. > > ARIA is not meant to replace HTML > > this! In fact, I think we might want it to go the other way around, with HTML replacing ARIA bit by bit until its services are no longer required > > — Hidde (@hdv) September 15, 2021 ## The first rule of ARIA The first rule of ARIA use in HTML states that you should **avoid using ARIA if there is a native HTML element with the semantics of behavior that you require already built in.** If such an element exists, you should reach for that element instead. This means that ARIA should be **a second resort, not a first approach.** By providing HTML elements that are implicitly mapped to ARIA roles, we can encourage the use of proper HTML markup to convey semantic meaning, and spread more awareness to help avoid both overuse and misuse of ARIA in general. If we can get an HTML element that enables us to use ARIA less, then that element should, in my opinion, be a welcomed addition. ## Outro A native search element might feel like a _small_ technical win to many, but the consistency it provides, the HTML semantics gap it fills, and the awareness it could potentially help spread would all make it a useful and welcomed addition. 112 to 113 HTML elements? I hope so.
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
CSS-only scrollspy effect using scroll-marker-group and :target-current
✨ This post is sponsored by everyone who has bought my Practical Accessibility course. ✨ The _Bootstrap Scrollspy_—now commonly known as just “Scrollspy”—is a feature that automatically updates navigation links based on the user’s scroll position to indicate which link is currently active in the viewport. It is popular because it aims to enhance the user experience by providing visual cues about which part of the content is currently being viewed. Sorry, your browser doesn't support embedded videos. The Scrollspy effect demonstrated in the Bootstrap documentation shows the navbar links are highlighted when their respective target sections are scrolled into view. By default, in-page navigation links (`<a href="">`) don’t get highlighted when their targets are scrolled into view. Historically, the Scrollspy effect has required us to use JavaScript to ‘spy’ on sections of content in a page (such as in an article) and then programmatically update the navigation links and indicate which one is “active”. That typically involved adding a CSS class name (e.g. `.active`) or an HTML attribute (e.g. `data-active`) to style the active link. Today, a new CSS property and pseudo-selector are available that are meant to enable us to create the Scrollspy effect with just two lines of CSS and no JavaScript. ## Scrollspy with CSS scroll markers If you’ve read my previous article discussing the accessibility of CSS-only carousels, then you’re already familiar with the CSS Overflow Module Level 5, and the concept of “scroll markers”. You will also be familiar with the fact that there are CSS-generated scroll markers (`::scroll-marker`), as well as HTML (and SVG) scroll markers (`<a href="">`). If you haven't read the CSS Carousels article yet, I highly recommend you pause here and go give it a read. There's information in there that provides context for some of the technical concepts discussed in this post. In the carousels article, I examined a CSS-only Scrollspy example from the ‘CSS Carousels’ gallery. The Scrollspy example in the Carousels gallery is created using CSS-generated scroll markers. By default, CSS-generated scroll markers are semantically exposed as tabs, not links, which introduced a bunch of usability issues with that pattern that I outlined in the previous post. As I noted in that post: > Instead of using `::scroll-marker`s to implement [the Scrollspy] example, I would instead expect to be able to create a semantic table of contents using an HTML list of `<a href="">`, and then use the `:target-current` pseudo-class to apply active styles to a link (the native scroll marker!) when its target is scrolled into view. […] However, that doesn’t seem to work at the moment. > > Unfortunately, even though the specification states that it "defines the ability to associate scroll markers with elements in a scroller", **the current implementation of the`:target-current` pseudo-class seems to work only for CSS-generated `::scroll-markers`, but not for native HTML ones.** > > Personally, I think `:target-current` is one of the most useful additions to the specification. It’s unfortunate that its current implementation is limited to the new pseudo-elements. Since I published that post, a new property has been proposed and added to the specification. This property is named `scroll-target-group` and it “ _enriches HTML anchor elements functionality to match the pseudo elements one_ ”, which makes it possible to use the `:target-current` selector to highlight links when their respective targets are in view. 🙌🏻 This post is about the `scroll-target-group` property and how to use it with the `:target-current` pseudo-selector to create the Scrollspy effect with CSS. ## Enriching HTML anchors to become scroll markers Using the `scroll-target-group` property (we’ll demonstrate how shortly), you can “promote” HTML anchors to become ‘scroll markers’. When **a group of HTML anchor elements** becomes scroll markers, the browser will run a specific algorithm to determine which anchor in the group is the active anchor, just like it determines the active scroll marker in a group of CSS `::scroll-marker`s. The active scroll marker is determined when its target element is scrolled to an ‘eventual scroll position’ chosen by the browser. According to the specification, the browser chooses an ‘eventual scroll position’ to which the target of a marker (an `<a href="">`) will reach. This ensures that the relevant marker is activated immediately. The active scroll marker then matches the `:target-current` pseudo-class, which you can use to visually highlight the active anchor. **All this requires no JavaScript on our part** , which is pretty impressive. Now, in order to use the `scroll-target-group` property, you will want to use it **not to the anchors themselves, but to an element containing the anchors.** Let’s demonstrate how. ### Using scroll-target-group and :target-current In the CSS Carousels article we talked about how the `scroll-marker-group` property is used to _generate_ a grouping container for a group of `::scroll-marker`s. When you use the `scroll-marker-group` property, both the scroll marker group container and the scroll markers themselves are generated by the browser as CSS pseudo-elements. On the other hand, the `scroll-target-group` property is meant to be a used on _an HTML element_ which _contains_ the HTML scroll markers (the links / anchors). For example, say you have a Table of Contents (TOC) on an article page and you want to style the active link within the TOC when its target section is scrolled into view. To use this property, you’ll want to start by setting up the semantic structure of the links: <nav aria-labelledby="toc-label"> <span id="toc-label" hidden>Table of Contents</span> <ol role="list"> <li><a href="#one">Section One</a></li> <li><a href="#two">Section Two</a></li> <li><a href="#three">Section Three</a></li> <li><a href="#four">Section Four</a></li> <li><a href="#five">Section Five</a></li> </ol> </nav> We have a named navigation landmark that contains an ordered list of anchors pointing to sections of content on the page. These links are, by default, keyboard-operable and they come with default link behavior and accessibility built in. To make these links behave like scroll markers, you will then use the `scroll-target-group` property on their container—this can be the `<ol>` or the `<nav>` element. nav[aria-labelledby=toc-label] { scroll-target-group: auto; } The `scroll-target-group` property specifies whether the element it is used on is **a scroll marker group container.** It accepts one of two values: `none` and `auto`. When the value of `scroll-target-group` is `auto`, "the element establishes a scroll marker group container forming a scroll marker group containing all of the scroll marker elements for which this is the nearest ancestor scroll marker group container.". Now as the user scrolls through the sections of content, the browser will determine which link is currently active. The active link will automatically match the `:target-current` selector, which you can use to highlight the link by giving it distinctive styles within the group: a:target-current { font-weight: bold; text-decoration-thickness: 2px; } ### Live demo Here’s a live example of the CSS Scrollspy effect in action: At the time of writing, you need to use Chrome 140+ to see the HTML scroll markers in action. And here is a Codepen for you to tweak at. Here’s a video recording of the example in action: Sorry, your browser doesn't support embedded videos. #### A note on accessible active anchor styling WCAG Success Criterion 1.4.11 Non-text Contrast Level AA requires that **visual information required to identify user interface components and states** (except for inactive components or where the appearance of the component is determined by the user agent and not modified by the author) **has a contrast ratio of at least 3:1 against adjacent color(s)**. If you’re using color alone to visually indicate the active link, make sure the color contrast is high enough to make the color change discernible by people with vision disabilities. I recommend that you don’t rely on changing the text or background color of the link alone to indicate that it is active, as these colors will be overridden in forced colors modes like Windows Contrast Themes. So the active link styles may no longer be conveyed to the user, _unless_ you use the `forced-colors` feature query and system color keywords to adapt the colors of the active link to the user’s chosen color scheme. This is outside the scope of this article. I personally prefer adding a border, an outline, or an underline, or increasing the thickness of the text (or a combination of any of these) to highlight the active link. So, using just a couple of lines of CSS, you can now create a scrollspy effect without needing a single line of JavaScript. One more thing! Usually when you load a page that has a fragment identifier in the URL, the browser scrolls the page to the ‘target element of the document’ and you can use the `:target` pseudo-class to apply custom highlight styles to that element. Until today, there hasn’t been a way to style/highlight the (in-page) _link_ that points to that target element. Today, if a link is a scroll marker, the `:target-current` styles will be automatically applied to the link when the browser scrolls to the document’s target element. This means that you can now combine `:target` and `:target-current` to style the target element identified in a URL fragment identifier _and_ the link to that element. ## The semantic accessibility of HTML scroll markers HTML anchor elements (`<a href="">`) come with default link accessibility and behavior built into them: they are exposed as `link`s to screen readers, and they come with keyboard interactions built into them by the browser. But an `<a href="">` element does not come with a built-in way to communicate that it is “active”, or that it is “the current” link within a group of links. A scroll marker, by definition, has a meaningful purpose: it lets the user know which part of content is currently being viewed. What this means is that when you visually highlight an active link, you’re communicating meaningful information to the user. To ensure that you are not excluding any of your users, you want to make sure that this information is communicated to _all_ your users, including screen reader users. **This is a baseline accessibility requirement.** WCAG Success Criterion 1.3.1 Info and Relationships (Level A) states that "information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text." So, how do you communicate that a link is active to screen reader users? How do you provide the same meaningful affordance that you’re creating with CSS to someone who can’t see? Since there is no native HTML way to indicate that a link within a group is currently “active”, you can use ARIA to communicate this information. Affordance is “the quality or property of an object that defines its possible uses or makes clear how it can or should be used”. As I mentioned in the CSS Carousels accessibility post, ARIA is similar to CSS in that it creates user interface affordances to the assistive technologies that rely on it. Using CSS, we provide visual affordances to our interfaces (using color, layout, spacing, and more). ARIA provides what “ _semantic_ affordances” to screen reader users. ARIA attributes “paint” a (non-visual) “picture” of the page to screen reader users. To indicate which anchor is currently active, ARIA provides a conveniently-named attribute: `aria-current`. > [The `aria-current` state attribute] indicates the element that represents the current item within a container or set of related elements. … The `aria-current` attribute is used when an element within a set of related elements is visually styled to indicate it is the current item in the set. Setting `aria-current` to `true` on the “active” anchor ensures that screen reader users get the same information that sighted users get about which part of the content is currently shown. <nav aria-labelledby="toc-label"> <span id="toc-label" hidden>Table of Contents</span> <ol role="list"> <li><a href="#one">Section One</a></li> <li><a href="#two" aria-current="true">Section Two</a></li> <li><a href="#three">Section Three</a></li> <li><a href="#four">Section Four</a></li> <li><a href="#five">Section Five</a></li> </ol> </nav> You may already be familiar with this attribute as you may already using it to indicate the active link within your website navigation. `aria-current` accepts one of seven values. For website navigation, the `page` value is appropriate as it indicates **the current _page_** on the website. For nested or in-page navigation links, the `true` value is sufficient to indicate the current link within the group. When the user scrolls through the sections of content and the active link is visually highlighted, this link must have `aria-current=true` set on it. Because the purpose of the `scroll-target-group` property and the `:target-current` selector is to allow us to create JavaScript-_free_ native HTML scroll markers, we should expect the browser to add and manage the necessary ARIA attribute(s) required for scroll markers to be inclusive. (After all, that’s the whole premise of this feature: to write a few lines of CSS and let the browser handle all the behavior for us.) However, at the time of writing of this post, Chrome (currently the only browser that has implemented this feature) doesn’t add `aria-current=true` to the active anchor yet. If you inspect the accessibility information of the links in the demo from the previous section you can see that the state of the active anchor is not communicated to assistive technologies when the anchor becomes active (see screenshot below). This is unfortunately akin to some of the accessibility issues with CSS Carousels that I discussed in the previous post. I filed a Chromium issue and I’m hoping this will be resolved soon enough to make this feature usable. I will update this post when the issue is resolved. **Update (2025-08-19)** : In response to the issue I filed a couple of days ago, there is now an active, work-in-progress patch to set `aria-current` for :target-current html anchor element. If you want to use this feature as a progessive CSS enhancement today, keep in mind that you _will_ , for the time being, need to use JavaScript to add `aria-current` to the active anchor when its corresponding target scrolls into view, **otherwise you risk an instant WCAG 1.3.1 violation.** I’ll personally wait till the issue is resolved and the feature becomes ready for production. When it does, I’ll be among the first to add it as an enhancement in my CSS. ✌🏻
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
Setting up a screen reader testing environment on your computer
When you’re designing and developing for accessibility, performing manual testing using a screen reader is important to catch and fix accessibility and usability issues that cannot be caught by automated accessibility checkers. You can catch the majority of issues by performing testing **using the screen readers that your users rely on the most.** If you haven’t already, you want to set up a screen reader testing environment on your computer, and invest a little learning time to get acquainted with the most relevant screen reader commands and shortcuts that you will need to perfom basic manual testing with a screen reader on a day-to-day basis. In this chapter, we will walk through setting up a screen reader testing environment on your computer. We will discuss software options you have to do that (both free and premium), and **what screen reader and browser combinations to test with.** We will also go through enabling accessibility testing on a Mac (which requires a little manual work to do). And finally, we will learn about a few useful features and cheasheets that make testing a little friendlier when you’re just getting started. ## macOS vs Windows screen readers Both Windows and macOS come with screen readers built into them that are available for free. The built-in Windows screen reader is called **Narrator**. The macOS built-in screen reader is **VoiceOver**. According to WebAIM’s screen reader user survey, **more than 90% of screen reader users reported being on Windows**. And according to the same survey, **the two most popular screen readers areJAWS (Job Access With Speech) and NVDA (NonVisual Desktop Access)** (which are both Windows screen readers), followed by VoiceOver. If you’re already on Windows or if you own a Windows machine, you’re already halfway through setting up your screen reader-testing environment. If you’re on a macOS computer, **you shouldn’t test solely with VoiceOver.** It is more opinionated and does not always reflect what the majority of screen reader users experiences. If you’re on macOS and you have no access to a Windows machine (whether an actual machine or a virtual one), you can test your work with Windows screen readers using any modern browser instead. We’ll get back to this in another section. ## Setting up Windows screen readers JAWS is the most popular and feature-rich screen reader. a JAWS license isn’t free and is faily expensive. But you can still use it to perform testing for your work. **JAWS will run in full in demo mode for 40 minutes at a time, until it is activated on your computer.** While this is a limitation for longer testing sessions, the 40 minutes are usually more than enough to perform basic testing. **NVDA is a feature-rich,_free_ alternative to JAWS**. We will install and set up NVDA in the following sections. ### Download NVDA screen reader on Windows Go to the NVAccess Web site. Click the Download link. That will take you to the NVDA download page. NVDA is available for free, but a donation is strongly encouraged. Click the **Download** button. Wait for NVDA to download. And go through the installation wizard when it’s done. #### Visualize NVDA’s current focus target with Visual Highlight To make testing with NVDA more convenient (especially if you’re new to screen reader testing), I recommend enabling NVDA’s Visual Highlight feature. To enable it, go to **Preferences** > **Settings** > **Vision** > **Visual Highlight** , and check the **Enable Highlighting** option. What this does is it shows a focus highlight around the element that NVDA is currently focused on — whether it’s in a webpage or anywhere on your system. This feature is useful for partially-sighted screen reader users who want to track the location of the NVDA navigator object and the currently-focused element. Seeing where the screen reader’s current focus is at is also helpful for _you_ when you’re performing testing, especially if you’re recording your screen for an educational video, for example. ^^ #### Enable NVDA speech viewer Another helpful feature you can enable is the **NVDA Speech Viewer** log window. Click the NVDA icon in your taskbar (on the bottom right of your screen by default), and go to **NVDA** > **Tools** and enable **Speech Viewer**. You also have the option to open the speech viewer log window by default on NVDA startup. The speech viewer log window contains the text that NVDA speaks, which can be helpful when you’re just getting started with screen reader testing. Just keep in mind that its usefulness of sometimes limited because the log often does not fully represent what is announced. #### Setup keyboard layout for testing with NVDA on a Mac If you’re on a Mac, go to **NVDA** > **Preferences** > **Settings** > **Keyboard** and Choose “**Laptop** ” Keyboard layout instead of the default Desktop option. The desktop layout relies on many keys which do not exist on some Mac keyboards. You can also set this preference in NVDA’s start popup menu. If you're using JAWS, there is a similar option in the JAWS startup wizard to choose the Laptop keyboard layout instead of the default Desktop layout. ### Map the Insert key to another key on Mac The `insert` key is the default modifier key used by most screen readers on Windows. If you don’t own an external keyboard that has an `insert` key, you might need to use a software work-around to make up for the lack of the `insert` key on your keyboard. NVDA settings include an option to set the `caps lock` key as the NVDA modifier key. You can do that if you prefer. I personally prefer to not do that because it interferes with typing when the `caps lock` is On. Alternatively, you can use a software program to map one of your less-used keyboard keys to the missing `insert` key. I use Karabiner Elements. #### Setting up Karabiner Elements on macOS Karabiner is a free app. To use it: 1. Download the app from the Karabiner Elements Website. **You want to download it on your Mac, not in your virtual machine.** 2. Run through the setup, and make sure to enable access in your **System Preferences** settings if it is blocked by macOS (which it probably will be by default). 3. Once it is installed and your keyboard is recognized, go to **Simple Modifications**. 4. Choose the device(s) you want to create a mapping for, and then click **Add Item** to map an unused key to the insert key. In my Karabiner, I mapped the right `option` key to the Windows `insert` key. And I also mapped the right `cmd` key to the `print screen` key, which can be used in combination with other keys to quickly turn Windows High Contrast mode On and Off (which is a shortcut that will come in handy in another chapter). That’s it. Now if you open your VM and fire up a screen reader, you can use the right `option` key (or the key of your choice) as a modifier key in place of the `insert` key. ## Virtual accessibility testing in your browser If you’re on macOS and you have no access to a Windows machine, you can test your work with Windows screen readers using any modern browser instead. You can do that using a service called AssistivLabs. **AssistivLabs is to screen reader testing what BrowserStack is to cross-browser testing.** It **remotely connects you to real assistive technologies** (like NVDA, JAWS, and Windows High Contrast Mode) using any modern web browser. AssitivLabs _currently_ only offers testing with _Windows_ screen readers and assistive technologies (like Windows High Contrast Mode and Windows Magnifier) for most accounts; testing using macOS assistive technologies will be available in the future. AssitivLabs is a paid service — it’s not available for free by default. But it is very helpful for when and if getting access to a Windows machine is otherwise not possible. Note that Practical Accessibility course enrollees will get **a 6-months unlimited free trial** to Assistivlabs. 🎁 ## Enable keyboard accessibility on a Mac To complement your screen reader, you should enable keyboard accessibility on your Mac. Keyboard accessibility is not enabled by default on macOS. If you’ve ever tried to tab your way through interactive and focusable elements on webpages and couldn’t, that’s why. (Frustrating, I know.) **You need to manually enable keyboard accessibility on macOS** by going to **System Preferences** > **Keyboard** , and enabling the “**Use keyboard navigation to move focus between controls** ” option in the **Shortcuts** tab. On macOS 13+, you’ll go to **System Preferences** > **Keyboard** , and then enable the **Keyboard Navigation** (Use keyboard navigation to move focus between controls. Press the Tab key to move focus forwards and Shift Tab to move focus backwards) option. Once you’ve enabled system-wide keyboard accessibility, you want to also enable keyboard tabbing in Safari. In Safari, go to **Preferences** > **Advanced**. And enable the “**Press tab to highlight each item on a webpage** ” option. Now you can tab your way through webpages as you should. If you're on an older version of macOS or you want to enable these settings in Firefox and Chrome, I've added a couple of resources to help you do that in the recommended resources section at the end of the chapter. ## Which browser and screen reader pairings should you test on? **Screen readers work best when they are paired with the browsers they are the most compatible with.** When performing testing, you can catch most accessibility issues (sometimes even all of them) by pairing each screen reader with the browser it is most commonly used with. ### On macOS **VoiceOver works best with (and should, therefore, be paired with) Safari.** If you use VoiceOver with Chrome or Firefox, for example, you might get unexpected results because VoiceOver is **optimized** to work with Safari not with other browsers. ### On Windows **Narrator works best with Edge** , and has difficulty interfacing with other browsers. But Narrator isn’t most users’ first choice. **JAWS** — the most popular of all screen readers on Windows — works best with Chrome and Firefox. When perfoming testing, **pair it with Chrome.** **NVDA works best and is commonly paired with Firefox.** ### Mobile screen readers Throughout this course, we will focus mainly on desktop screen reader testing. But you should test your work using mobile screen readers as well. In WebAIM’s ninth screen reader user survey, **90% of respondents reported using a screen reader on a mobile device.** According to WebAIM, this number has increased over the last 12 years. WebAIM also notes that participants with disabilities (91.6%) are more likely to use a mobile screen reader compared to individuals surveyed without disabilities (71.4%). So it is very important that you test your work on mobile to ensure that it works for a large group of screen reader users. VoiceOver on iOS/iPadOS is the most popular mobile screen reader. VoiceOver comes bundled with iOS/iPadOS. Like its desktop version, you want to **use it in conjunction with mobile Safari.** On Android, **Talkback (the built-in screen reader) is best paired with Chrome.** ## Guides to browsing and navigating content with a screen reader Make some time to learn how to navigate and browse web content with each screen reader. It might take some time and feel like a steep learning curve at first, but by doing that you will gain an invaluable skill for your accessibility work. Here is a list of official user guides that are helpful for getting started: * NVDA User guide. Most read-only webpages are browsed in NVDA using Browse mode. * JAWS documentation (Shortcut to JAWS Hotkeys) * Complete guide to Narrator (Shortcut to Narrator keyboard commands and touch gestures) * VoiceOver Guide * Use VoiceOver to browse webpages on Mac * Apple VoiceOver Command charts * Talkback user guides * Turn on and practice VoiceOver on iPhone And to get a high-level (yet practical) overview of how someone using a screen reader browses the Web, I recommend watching the Browsing with assistive technologies video series by Tetralogical: * Browsing with a desktop screen reader * Browsing with a mobile screen reader ## Screen reader keyboard shortcut cheatsheets When you’re just getting started with screen reader testing, and you want to test with at least three screen readers across different platforms and devices, it can be difficult to remember all the keyboard shortcuts for each screen reader right away. Deque University provides useful screen reader keyboard shortcuts and gestures cheatsheets, that you can either reference on your computer, or print out and have them handy during your testing. * Desktop Screen Readers Survival Guide - Basic Keyboard Shortcuts * Desktop Screen Readers Forms Guide * NVDA keyboard shortcuts * JAWS keyboard shortcuts * Narrator keyboard shortcuts * VoiceOver keyboard shortcuts ## Resources and recommended reading * Efficiency in Accessibility Testing or, Why Usability Testing Should be Last * Checking Windows High Contrast mode on a Mac for free (inculdes instructions to download and set-up VirtualBox) * Using Windows Screen Readers on a Mac * The WebAIM screen reader user survey * No, tabbing is not broken. Yes, I was confused too. * Browser keyboard navigation in macOS * The Importance Of Manual Accessibility Testing * Your Accessibility Claims Are Wrong, Unless… * Relevant combinations of screen readers and browsers Hat tip and thanks to Adrian Roselli for pointing out that the Focus Highlight NVDA add-on is no longer necessary since focus highlighting has been built into NVDA 2019. And thank you to Corentin H. for providing the screenshot of keyboard accessibility preferences on macOS 13.
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
Accessible notifications with ARIA Live Regions (Part 2)
In the first part of this chapter we discussed what we might need live regions for, and how to create them using HTML and ARIA. In this part, we’re going to discuss what live regions are _not_ suitable for and why, and we’re going to discuss more robust ways to implement some common UI patterns that you might otherwise consider using live regions for. After that, we’re going to go over some best practices for implementing live regions for when you do need to use them to represent things like status messages in your web applications. Live regions are easy to misuse and to _overuse_. Aside from inconsistent browser and screen reader support, live regions’ inherent capabilities are limited _by design_ , which makes them unsuitable for certain types of content updates. ## Live regions don’t handle rich text Live regions don’t handle rich text. This means that the semantics of elements like headings, lists, links, buttons, and other structural or interactive elements are not conveyed when the contents of a live region are announced. If a live region contains a button, for example, the screen reader will announce the text of the button when it is injected into the live region without any mention of the button’s role: <div aria-live="polite"> <!-- The semantics of this button are not conveyed in the live region announcement --> <button>You'll have to guess what I represent!</button> </div> The fact that the text represents the label of a button will not be communicated by the screen reader in the live region announcement. (Example borrowed from Scott O’hara’s article) Here is how VoiceOver with Safari announces a button when I add the button to a live region using JavaScript: Sorry, your browser doesn't support embedded videos. The screen reader will announce the entire contents of a live region as one long string of text, without any of the structure. This is why **you should not wrap entire sections of content in a live region.** Otherwise the entire section’s content will be announced as one long string of text, which would result in a bad user experience. When content updates happen in large sections of content, there is often a better way to communicate these updates to screen reader users. For example, say you’re building a filtering component for an e-commerce website or any website that offers the ability to filter content within a main section of the site. In most web applications, the content in the main section will filter dynamically as soon as the user selects one of the available filters. But **just because the content within the section gets updated does not mean that the section should be a live region.** So, how _do_ you let the user know that the content in the section is updating? **Providing simple instructional cues that set the user’s expectation of what will happen when they interact with an element is sometimes more than sufficient to let the user know of content updates even before they happen.** For the filtering component, what this means is that you can include an instructional cue at the top of the group of filters to let the user know that changing the filters will change the content in the main area. This way you’re setting the user’s expectation of what will happen when they select a filter, so you no longer need to make any announcements when the content updates. The user knows that they can just navigate to the main area and start exploring the filtered content. The WCAG Quick Reference website provides a live implementation of this approach. Preceding the content filters in the left sidebar of the Quick Reference, there is a note that lets the user know that “Changing filters will change the listed Success Criteria and Techniques”. No live region is necessary when this persistent cue is shown to all users, including screen reader users. Another very common UI component that can benefit from a similar implementation approach is a dynamic search component — particularly one where you have a search field that filters and displays results as you type into the field, like the search component you can find on the Smashing Magazine website. Implementing a dynamic search component like this one was one of the most popular requests that I got when I asked many of you what you'd like me to discuss in this course. So, here goes! What many developers will do when they build a similar component (and, admittedly, it’s a mistake I also made early in my career) is they will designate the search results container as a live region (like Smashing Magazine currently does) just because the content in the container is dynamically updated while the user is typing in the field. This results in a very noisy user experience. Here’s how VoiceOver on macOS starts announcing the contents of the results container after I type a search keyword in it: Sorry, your browser doesn't support embedded videos. A simpler, more robust, and much more user-friendly approach to implementing this pattern is to provide an instructional cue (i.e. an accessible description for the input field) that tells the user what will happen when they start typing in the field. The description might say “Results will filter / display / etc. ] as you type”. For example, the search component on [the a11ysupport.io website provides a live implementation of this approach. The accessible description of the search field lets you know that “Features will be filtered as you type”. The cue must be associated with the search field using `aria-describedby` so that screen readers announce it to their users. Providing instructional cues is helpful for all users. But if you don’t want the description to be visible, you can visually hide it using the `visually-hidden` utility class. Just make sure it is properly associated with the input field using `aria-describedby`. By letting the screen reader user know that results will be shown as they type, you no longer need to announce when results are shown, and the user knows that they can navigate to the search results once they’re done typing their keyword in the field. That being said, **you do still want to use an assertive live region to inform the user when _no_ results are found.** "While we don’t want to constantly interrupt people while typing, we need to interrupt when things go afoul", says accessibility engineer Scott O’Hara in his article Considering dynamic search results and content. "Specifically, let someone know immediately when they have entered a query that returns no results. Delaying such an announcement would result in wasted time, and potential uncertainty about “when” someone’s query stopped working. […] People who can see the UI are likely going to notice right away when the dynamic content dries up. They can then immediately correct for this by adjusting their query. This same affordance must be provided to people with disabilities." In his article, Scott elaborates more on all the usability considerations you should keep in mind when you’re implementing a dynamic search component. He then proposes a solution for how you might go about implementing it in a more robust and inclusive manner. I highly recommend checking the article out and following his implementation pattern if you can. In addition to using an accessible description for the search field, and a live region to announce when no results are found, Scott also suggests moving the user’s keyboard focus to the heading that introduces the results when the `Enter` key is pressed. Of course, if your search component doesn’t provide a heading, you wouldn’t need to do that. But it is a nice addition that improves the user experience for keyboard users, and screen readers will announce the number of results found when focus is moved to the heading. Here is how Scott’s demo works with NVDA on Firefox: Sorry, your browser doesn't support embedded videos. In this video, NVDA announces the search field followed by the accessible description when my focus moves to the field. The accessible description indicates that results will be shown as I type. I first type a keyword "one" into the search field. Then I press Enter. Pressing Enter moves keyboard focus to the heading which introduces the results and communicates the number of results found. Then I press the Escape key. Pressing the Escape key moves my focus back to the search field. When I type a keyword that's more than five characters long, the dummy example says that No results are found. Since Scott is using a live region to communicate when no results are found, NVDA announces "No results found!". Here’s an embed of Scott’s demo: See the Pen quick demo of showing / informing about dynamic results by Scott (@scottohara) on CodePen. ## Live regions are not suitable for notifications with interactive elements Live regions should not be used for messages or notifications that contain interactive elements, particularly if the user may need to act on those notifications. As we mentioned earlier, when a screen reader announces the contents of a live region, **it will announce the raw text content within the region without any of the structure or semantics. This means that the semantics of any interactive elements will not be conveyed.** Furthermore, when an update happens in a live region, screen readers will only announce the contents of a live region, but **the user’s focus does not move to the region.** And there is no mechanism available to allow the user to easily navigate to a live region to interact with any content that might be in it. So unless you provide a clear path for screen reader and keyboard users to get to the notification that contains interactive elements (like a well-documented keyboard shortcut, for example), then, depending on the position of the live region in the DOM, it can be difficult—if not impossible—for the user to get to the interactive content in the notification, especially if the notification dismisses itself after a short timeout. This is why toast messages that contain interactive elements are problematic. Unfortunately, toast messages that contain interactive elements are pretty common. You can see examples of them documented in Google’s Material Design system. But these messages come with usability and accessibility problems for screen reader users, as well as other users of assistive technologies like users browsing the web using a magnifier, and keyboard users as well. Adrian Roselli has documented and listed the most relevant WCAG failures that toast messages will typically be in violation of, particularly if they contain interactive elements. I highly recommend pausing here and taking a couple of minutes to read Adrian’s article, especially if you’re considering using toasts in your applications. If a notification contains an interactive element, you need to ensure that the user can easily navigate to it. And the best way to do that is to move the user’s focus to it. The `alert` and `status` live region roles are meant to represent short messages that do not require moving the user’s focus to (i.e. that do not contain interactive children). > Authors SHOULD ensure an element with role `status` does not receive focus as a result of change in status. > > […] > > Neither authors nor user agents are required to set or manage focus to an alert in order for it to be processed. Since alerts are not required to receive focus, authors SHOULD NOT require users to close an alert. If an author desires focus to move to a message when it is conveyed, the author SHOULD use `alertdialog` instead of alert. > > — The ARIA Specification If a notification contains an interactive element, it should not be a live region. And it should also not be a toast. You should move the user’s focus to it instead, and make it persistent. For interactive alert notifications, instead of using a toast message, consider using an alert dialog. The ARIA `alertdialog` role is used to represent a type of dialog that contains an alert message. As the name implies, `alertdialog` is a mashup of the `dialog` and `alert` roles. This means that it also expects similar keyboard interactions as modal dialogs do. Implementing an alert dialog is outside the scope of this chapter, but you can find the semantic and keyboard interaction requirements for implementing accessible alert dialogs documented on the APG website. Using `alertdialog` to alert the user of the presence of errors is an advisory technique to meet SC 4.1.3 Status Messages. For status type notifications that contain interactive elements, you may use a modal or non-modal dialog instead, and manage focus within these dialogs when they appear, as documented on the APG modal dialog example page. Keep in mind that **moving focus should be done as an immediate response to the user’s action**. If something happens async or after a delay like if a toast appears that the user hasn’t called up, then you shouldn’t move their focus to it, otherwise it would be disruptive to the user experience. **Decide if you should move focus or not based on what users are expecting.** ## Live regions are not a substitute for ARIA state properties Don’t use live regions to convey state changes when there’s an ARIA attribute to do that. For example, if a button toggles the visibility of some content on a page (like a ‘dropdown’), use the `aria-expanded` attribute to communicate the state of the content (whether it’s expanded or collapsed) to the user. You don’t need a live region to announce that the content has been expanded or collapsed. <!-- The aria-expanded attribute communicates the state of the disclosure widget to the user. No live region is needed to do that. --> <button aria-expanded="[ true | false ]">Terms of use</button> Similarly, if you’re building a dark theme switcher using a toggle button, for example, use the `aria-pressed` attribute to communicate to screen readers that the dark theme is currently ‘On’ or ‘Off’. <!-- The aria-pressed attribute communicates whether or not the [Dark Theme] is On or Off. No live region is needed to announce when the dark theme is applied. --> <button aria-pressed="[ true | false ]">Dark theme</button> When `aria-pressed` is declared on a `<button>`, the button’s ARIA role mapping will change in most accessibility APIs, and it will be exposed as a `toggle button` (not just a regular button), indicating that this `<button>` toggles a certain functionality On and Off. When the value of `aria-pressed` is true, it communicates to screen readers that the functionality is currently ‘On’. Combined with the button’s accessible name, the state attribute lets the user know what will happen when they activate the button. You don’t need a live region to announce that the dark theme has been applied to the page. Here’s a live demo of a simple theme switcher using two toggle buttons that you can try using a screen reader: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. When a toggle button is activated, the screen reader announces the state of the button (whether it’s pressed or not) when it announces the button’s role and accessible name. The state of the button is sufficient to communicate when a theme is ‘On’ or ‘Off’. You can try it for yourself in the debug version of this theme switcher. State property changes may not be announced to the user the same way live regions are, but not every change _needs_ to be announced. Using appropriate semantics, providing meaningful accessible names, and using the appropriate state attributes is sometimes sufficient for screen reader users to understand what will happen when they interact with an element. So before considering using a live region, ask yourself if there is a state attribute that does what you need. And if there is, use that attribute. ## Best practices for implementing (more robust) status messages with live regions Live regions are most suited for implementing short, non-interactive status messages that do not cause a change of context (like moving focus) and that cannot be communicated to screen reader users in another way. Live regions are currently the primary way to conform with Success Criterion **4.1.3 Status Messages (Level AA)**. If you’re implementing status messages in your web application(s), there are some best practices that most accessibility professionals agree on that can help you achieve maximum compatibility across browser and screen reader pairings: ### Make sure the live region container is in the DOM as early as possible The element that is designated as a live region **must exist on the page when the browser parses the contents and creates the accessibility tree of the page.** This ensures that the element will be monitored for changes when they happen and that these changes are communicated to the screen reader and the user. So when you create a live region, insert it into the DOM as soon as possible (ideally, when the page loads), **before you push any updates to it.** If you insert a live region into the DOM or convert a container into a live region _when you need it_ , there’s a high chance that it won’t work. ### Choose an appropriate hiding technique if the live region isn’t visible If the status message is not visible to all users, hide it visually using the `visually-hidden` utility class. Don’t hide the live region using using `display: none;` or `aria-hidden="true"`, or any other hiding technique that removes it from the accessibility tree. **Hidden live regions are not announced.** ### Limit the number of live regions on the page While there is no rule as to how many live regions you can have on a page, you should limit the number of live regions you create. As accessibility engineer Scott O’Hara says in his article “Are we live?”: > please do keep in mind that something just as bad as live regions being injected into a web page and then making no announcements, is a web page with a bunch of live regions that all start barking at assistive technology users at the same time. A good practice is to have only two live regions on the page: **one assertive region and one polite region** that get inserted to the page on page load. Then you insert updates into these two regions and manage the message queue in them via JavaScript. If you have multiple live regions on a page, they may interfere with each other, and some messages might not be announced at all. According to the specification: > Items which are assertive will be presented immediately, followed by polite items. User agents or assistive technologies MAY choose to clear queued changes when an assertive change occurs. (e.g., changes in an assertive region may remove all currently queued changes) What this means is that **the politeness level** indicated by the `aria-live` attribute **works as an ordering mechanism for updates**. In instances when you have multiple live regions on a page, `polite` updates take a lower priority; and `assertive` updates take a higher priority and could even potentially clear or cancel other updates that are queued for announcement. This causes some messages to get lost, or just partially announced. Carefully choreographing the sequence of events in a couple of live regions on the page will be your best approach to achieve maximum compatibility. This is one of the reasons why, in the previous chapter, we said that providing a summary of errors at the top of a form is a far more robust approach than using inline validation. ### Compose and insert your message into the live region in one go You should **pre-compose the notification message’s content and insert it into the region in one go.** Don’t make multiple DOM insertions to create one message, otherwise the screen reader may make multiple _separate_ message announcements, which is not what you want. ### Keep the content short and succinct and avoid rich content **Keep the message content concise.** And keep it as short as possible. There is no character limit on the notification message, but remember that live region announcements are transient and can’t be re-played. So make sure the message is easy to understand when it is announced the first time. Keeping it short also ensures it is less disruptive to the user flow. **Avoid rich content, interactive elements, and non-text elements** like images as these are also not conveyed to screen reader users. ### Empty the live region and wait a bit in between updates If the status message is not visible to everyone or it is removed after a short timeout, set a timeout (e.g. 350ms–500ms) to remove the notification text from the live region. You are not required to empty a live region after its contents have been announced because announcements are triggered on content additions by default, but emptying the live region between updates ensures that you don’t end up with weird or duplicate announcements. /* set a timeout to empty the live region */ setTimeout(() => { //empty live region }, 350); Emptying the live region when it’s no longer visible also ensures that screen reader users will not be able to navigate to it when they are not intended to. So make sure the live regions are cleared up in between updates, and wait a little bit before inserting new updates to them. And when you do insert a new update, insert the new message in one go. ## Debugging Live Regions If you use live regions, you’re going to want to debug them when they don’t work as expected, like when the screen reader announces something unexpectedly. Part of debugging live regions is seeing what goes inside in them and when. To debug live regions on a page, you can use the NerdeRegion browser extension. NerdeRegion is a developer tools extension for debugging live regions on a Web Page. When activated, it lists all active ARIA live regions, and keeps a record of all mutations that has happened on the region. You can use NerdeRegion to: * Check if a live region is being updated properly, * Check if accessible name computation (beta) is done correctly, * Check if live region is being re-used correctly. To use the extension, open your browser’s Developer Tools, and navigate to the NerdeRegion tab. There, you can keep track of timestamped announcements and the source element they originate from. Sorry, your browser doesn't support embedded videos. In this video, I have an assertive live region on the page. When I go to the NerdeRegion panel in the Edge DevTools, it lists the number of live regions on the page in the left sidebar, as well as the type of the live region (assertive or polite) in the main area of the panel. Then when I activate the button that populates the live region with a new message, NerdeRegion shows when the live region has changed, along with a time stamp of when it did. Since there can be bugs and inconsistencies with how ARIA live regions are announced with different screen readers, you should constantly be reviewing your live regions to ensure they are continuing to work as necessary. As we mentioned earlier, you should try to limit the number of live regions you use ideally to two or less. But if you absolutely have to use more than that, NerdeRegion can help you figure out if an issue is potentially caused by your code or by the device combination. ## Avoid live regions if you can I know this isn’t the advice you’d expect at the end of a whole chapter about live regions. But hear me out. Live regions are inconsistent and unpreditcable. It’s easy for their implementations to go wrong. There’s a lot of manual work involved to get them working properly. Furthermore, the design of live regions is intended to give maximum flexibility to screen readers to implement an experience that is best for their users. What this means is that ARIA live region properties are only **strong suggestions** as to how you want live region announcements to be made, but the value of these properties (and by extension: the behavior of live regions) **may be overridden by browsers, assistive technologies, or by the user.** This along with current bugs and implementation gaps means that you can’t guarantee that a live region will always work the way you designed it to. This is one of the reasons why you should try to rely on live regions as little as possible, and use alternative and more robust approaches whenever you can. Remember that live region announcements are transient. If the user misses an announcement, they miss it. Depending on the importance and urgency of the announcement, this can easily degrade the usability of your web application and result in a frustrating user experience. So if you can make your users aware of updates using other more persistent methods like moving focus or providing instructional cues, then you should consider doing so. **Not everything that updates in the background needs to be a live region.** For example, chat interfaces are typically a great candidate for live regions and would be implemented using the `log` ARIA role, but they don’t always _need_ to be implemented as live regions. Unless the chat is the main interface on the page, then it probably _shouldn’t_ even be a live region. For your day-to-day work, you’ll need live regions less often than you think, even if you’re building dynamic web applications like SPAs. For example, let’s say you’re building the navigation for a SPA. In most SPA navigations, activating a link will load a new page without causing a page refresh. Normally when the user activates a link and the link takes them to a new page, screen readers will announce the title of the new page first, which lets the user know where they have landed. But this doesn’t happen in SPAs. So what many developers will do is they will use live regions to announce when new content has been loaded. But this is not only unnecessary, but you can even let the user know that new content has loaded in a more efficient way. Instead of relying on a live region to announce the page change, you could send keyboard focus to the main `<h1>` of the page which, as we mentioned in the heading structure chapter, should describe the primary topic of the contents of the page and ideally be identical to the page’s `<title>`. By moving the user’s focus to the heading, the screen reader announces the heading’s content to the user, which gives them the same context that the page’s `<title>` would have given them if it had been announced after a page refresh. (But don’t forget to change the page’s `<title>` when a new page is loaded, too.) Moving focus to the primary heading of the page is also helpful for keyboard users. Usually when the page refreshes and the user starts tabbing through the page, a skip link should be the first element they focus on, and they can use that link to skip directly to the main content of the page. But if the page doesn’t refresh, they may have to tab their way through many elements before they reach the new content. So moving their focus to the main heading makes their navigating through the page more efficient. Live regions have their (limited) use cases — particularly for status messages as described in WCAG. But as accessibility engineer Scott O’Hara says: > if you can create an interface that can limit the number of live regions necessary - none being the ideal - then that’d be for the better. Your main purpose as a designer or developer is to make users aware of new content updates when they happen. But for most UI patterns, there are other ways you can achieve that, and those ways are often more robust and more reliable than live regions, and result in an overall better experience for your users. Another example that _could_ use live regions but that can also be implemented in a more robust manner is a shopping cart on an e-commerce website. A common pattern on many websites today is to show an overlay of the full cart when a new item is added to the cart. The modal cart overlay pattern used on the A Book Apart website. Instead of using live regions to announce that a new item has been added to cart, you can use this pattern and move the user’s keyboard focus to the cart when it is shown. This approach has a couple of benefits, one of them is that it makes it easier for keyboard users to get to the cart, see and/or edit what’s in it, and continue to checkout if this is what they want to do. Keep in mind that you must treat the cart as a modal dialog in this case and manage focus accordingly, particularly when the contents of the page are dimmed after the cart is shown. You can find the requirements for keyboard focus management for modal dialogs in the Modal Dialog page on the APG website. As a general rule of thumb: if you can achieve the same result without using live regions, then probably do so. The less ARIA you use, the better. Remember: No ARIA is better than bad ARIA. If you do use live regions in your web applications, make sure you **thoroughly test** across all browsers and screen reader pairings. And don’t forget to test with Braille displays, too. And perform usability tests with screen reader users. Not only will usability testing give you insights into what your users are expecting from the application and what they aren’t, but it will also help you understand the different ways screen reader users are using your application and how that will affect the announcements you’re trying to make with live regions. ## References, resources and recommended reading * The Many Lives of a Notification * Designing for Screen Reader Compatibility * output: HTML’s native live region element * Are we live? * We’re ARIA Live * Live Region Playground * (Test case demo) aria-atomic and aria-relevant on aria-live regions * Accessibility (ARIA) Notification API * More accessible skeletons * Defining ‘Toast’ Messages * A toast to an accessible toast… Many thanks to **James Edwards** (@siblingpastry) for reviewing this chapter.
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
Tag, You're It
## Why did you start blogging in the first place? I started blogging when I was still learning front-end development—specifically CSS—back in 2012. I was learning a lot and writing what I was learning as a way to organize my thoughts and solidify my learnings, and at some point **I realized that my notes can help others learn and understand the same.** So, I created my blog a few months later **in order to share my knowledge with the community**. I love writing and I love teaching, and **I found blogging to be one way I can bring together two things I love** , and to give a little back to the web community. ## What platform are you using to manage your blog and why did you choose it? Have you blogged on other platforms before? I use Eleventy. Before Eleventy I used Jekyll. I don’t choose my tools based on what’s most popular at any given moment. I choose the tool or platform that works for me and stick with it until I find myself _in need_ of something else. I moved from Jekyll to Eleventy when I needed more flexibility with how I wanted to structure my website’s source code and with how I style the different pages on the website. Eleventy still fits my needs perfectly until this day. ## How do you write your posts? For example, in a local editing tool, or in a panel/dashboard that’s part of your blog? I collect ideas, plan blog posts, and start drafting them in Obsidian. When a draft becomes close enough to publishing, I move it into my code editor where I will refine and polish it before I finally hit Deploy. I write in Markdown (`.md`) files in both Obsidian and my code editor, so moving from one tool to the other is quite effortless. ## When do you feel most inspired to write? **When I want to turn the chaos in my head into order.** This typically happens when I’m learning a new topic or idea, or when I’m teaching and helping others understand new topics or ideas. I’m most inspired by helping others understand what is otherwise a complex or confusing topic, and seeing other people’s eyes light up when they finally get that “ah-ha” moment. ✨ I mostly get to see this when I speak or run workshops in person, but the same also happens through writing as well. ## Do you publish immediately after writing, or do you let it simmer a bit as a draft? I publish immediately. Once a post is out there, it’s out there; and then I can move on and start working on the next thing. ## What are you generally interested in writing about? HTML, CSS, SVG, web accessibility, progressive enhancement, and how to use Web platform features to create **inclusive Web user interfaces**. ## What’s your favorite post on your blog? Understanding SVG Coordinate Systems and Transformations (Part 1) — The viewport, viewBox, and preserveAspectRatio, _hands down_! This post was the result of two weeks of deep-diving into the gnarly SVG specification, which resulted in me creating the interactive demo by which I got my first SVG epiphany! 💡 SVG became my niche for years after that. This post is also the reason hundreds (probably thousands!) of developers finally grapsed SVG coordinate systems and got their own SVG lightbulb moment. 💡 This one will always hold a special place in my heart. ## Who are you writing for? I started blogging for myself and for my future self. Now, I also write more for the community. I _start_ the writing process for myself; but I publish what I write for the community. ## Any future plans for your blog? Maybe a redesign, a move to another platform, or adding a new feature? I’m taking a comprehensive design course at the moment (ShiftNudge, in case you’re wondering) and am planning on redesigning my website as I go through the course. I will also be refactoring the code from the ground up this year. I’ll add new features during all of this, and will announce them in due time. ## Next? The format dictates I nominate other people to write a post like this. However, instead of nominating someone who may or may not be interested in joining the chain letter and who may instead feel pressured into doing so, I’m going to nominate _you_ , dear reader, if you have a blog are keen to join us and share some about it. If you _don’t_ have a blog, I hope this series of posts by people like you will inspire you to start writing, and sharing what you write with the world. Just write, and great things may follow.
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
The CSS prefers-color-scheme user query and order of preference
I spent some time in Reeder app this morning, catching up with RSS and the latest articles published by my favorite blogs. I was reading Scott O’Hara’s article about using JavaScript to detect high contrast and dark modes, which includes a small, very useful script to do exactly what the title says. The output of that script at first looked like it was a “false positive”. But some further investigation led me to learn something new about the `prefers-color-scheme` CSS user query. Scott’s article includes a Codepen to demonstrate the output of the script. The script will check and detect if you currently have high contrast mode or dark mode enabled, and will output the result of the check. See the pen (@scottohara) on CodePen. Since JavaScript doesn’t run in Reeder app, I clicked to open the original article on Scott’s Web site. That’s when I saw that the script was reporting that I had dark mode ON, even though I don’t have dark mode enabled on my phone. Having just recently updated to iOS 15, my first thought that this might be a browser/OS bug or something. But then it hit me: I _do_ have dark mode enabled… _in Reeder app_. (Reeder has a nice dark mode which I enjoy reading in.) This instantly led me to question whether the media query was picking up _that_ dark mode, instead of the OS-level preference. When I opened the article on Scott’s Web site, I opened it in Reeder’s in-app browser. Which means that the script was running in that context when it reported that dark mode was ON. So to test my assumption further, I opened the article in iOS Safari, which is running in the Light scheme mode (set on the OS-level). The script does not report that dark mode is ON in that context. In order to confirm this behavior, I checked the results of the test in Reeder app on my Mac, which is running dark mode on OS-level. I toggled the theme in Reeder app between Light and Dark to verify the results. Sure enough, the script detected dark mode ON when the app theme was set to Dark, but not when the app theme was set to Light. The `prefers-color-scheme` media query picks up the dark mode set in the app. Note that dark mode is also enabled on the OS level, but the media query is picking up the color theme from the app context. App color theme taking precedence over OS-level theme. Even though dark mode is enabled on the OS level, the `prefers-color-scheme` media query picks up the light mode set in the app when the app’s theme is the classic light. In an attempt to verify whether this was a bug or a feature, I checked the specification. The spec includes these two paragraphs: > The method by which the user expresses their preference can vary. It might be a system-wide setting exposed by the Operating System, or a setting controlled by the user agent. […] User preferences can also vary by medium. […] UAs are expected to take such variances into consideration so that prefers-color-scheme reflects preferences appropriate to the medium rather than preferences taken out of context. That explains it. **UA preference > OS-level preference.** Something to keep in mind for when an “unexpected behavior” happens. A good reminder to always test and check the specifications. Had this not been in the spec, then further investigation might have led to an existing bug report or to the creation of one. Who knows. * * * And _that_ was my first #TIL moment of the day. **Stay curious.** (Oh and also: **RSS is awesome.** Thank you to everyone providing an RSS feed for their content. _You_ are awesome.)
www.sarasoueidan.com
September 25, 2025 at 11:46 PM
Accessible notifications with ARIA Live Regions (Part 1)
In this chapter, we’re going to learn about ARIA live regions — the accessible notifications system that enables us to make **dynamic** web content more accessible to screen reader users. Without live regions, some rich web applications would be more challenging to use for screen reader users. So if you’re building web applications such as Single Page Applications (SPAs), you need to understand live regions so that you can utilize them **where appropriate**. This chapter is split into two parts. In **this first part** , we’re going to learn about why ARIA live regions are important, and the different ARIA attributes and roles that you can use to create them. We’re going to get an overview of these attributes, as well as learn about their current support landscape and limitations. In the second part, we’re going to get more practical and discuss why you should _not_ use live regions as much as you might think that you do, and we’ll talk about alternative approaches you should use instead when you create some common UI patterns. And then we’ll discuss best practices for implementing more robust live regions when you need them today. But first, in order to understand _why_ live regions are important, **we must first understand how a screen reader parses web content and presents it to the user.** We won’t get into much detail (not at all, really!), just enough to get a good understanding of what problem live regions solve. ## How screen readers parse web content The way screen readers parse and present web content to their users is very different to how sighted users see that content. Screen readers work by **linearizing web content.** Linearizing a page’s content means converting the page’s two-dimensional content into **a one-dimensional string** (that is then either spoken to the user using text-to-speech, or delivered to them via a refreshable braille display). When content is linearized, it is presented to the user **one item at a time.** "You can think of it like listening to a cassette tape", says Web accessibility specialist Ugi Kutluoglu, "which you can rewind, fast forward, pause and play." This means that a screen reader user can skip to items or sections they want, and they can tab through interactive elements and Shift-tab their way back. But at the end of the day, they can only move forwards or backwards, **one item at a time** , because what they are presented with is a one-dimensional version of the page. ## Why we need an accessible notification system for screen reader users Reading content linearly works well for static webpages, but it doesn’t work so well for pages where content is altered and updated dynamically or asynchronously using JavaScript. If the user can only move in one dimension, and focus on one item at a time, how would they know when content is added, removed, or modified **somewhere else** on the page? For example, when a user sends an email in most email web apps, a status message is shown at the top of the screen, or a “toast” message pops up (typically at the bottom of the screen) to notify them of the status of their interaction — for example that the email is sending, has been sent, or maybe that the email could _not_ be sent. Some of these messages are urgent (like an error message), and some of them are not (like a success message, or a Draft Saved notification). When these status messages appear, they are intended to be communicated to all users. But while these messages may be perceivable by a sighted user, they’re not preceivable by a blind screen reader user. When the status message is shown, it is not communicated to screen reader users by default because the screen reader focus is on another element at that moment (on the ‘Send Email’ button in this case). Here is what happens when I use NVDA and activate the Send Email button and show a status message in a dummy email app demo I created. NVDA does not announce the status message when it is shown. Sorry, your browser doesn't support embedded videos. Here is a dummy email app demo you can try for yourself. See the Pen Live region status message in email web app by Sara Soueidan (@SaraSoueidan) on CodePen. If you activate the Send Email button in the debug version of the dummy email app, the screen reader will _not_ announce the status message that is shown. **A screen reader can only focus on one element or part of the page at a time.** This means that if the user presses a button and that button triggers an update somewhere else on the page, **there’s a good chance they will be oblivious to it**. So they need a way to be notified of these updates when they happen. There are two primary ways you make a screen reader announce an update when it happens: 1. By **_moving_ focus** to where the update has happened, (like we did with the summary of error messages in the accessible form validation chapter); 2. By **notifying** the screen reader of these updates when they happen. When you move the user’s focus to an element, screen readers typically announce the element to the user. But when an update happens and you don’t move the user’s focus to it, you must notify screen readers in some other way. ## Status messages in WCAG WCAG Success Criterion **4.1.3 Status Messages (Level AA)** states that: > In content implemented using markup languages, status messages can be programmatically determined through role or properties such that they can be presented to the user by assistive technologies without receiving focus. A status message is defined in the specification as "change in content that is not a change of context, and that provides information to the user on the success or results of an action, on the waiting state of an application, on the progress of a process, or on the existence of errors." Examples of a **change of context** are opening a new window, **moving focus to a different component** , going to a new page (including anything that would look to a user as if they had moved to a new page) or significantly re-arranging the content of a page. From the Understanding Status Messages page: > This Success Criterion specifically addresses scenarios where new content is added to the page without changing the user’s context. Changes of context, by their nature, interrupt the user by taking focus. They are already surfaced by assistive technologies, and so have already met the goal to alert the user to new content. As such, messages that involve changes of context do not need to be considered and are not within the scope of this Success Criterion. In other words, this success criterion aims to ensure that, unless you move the user’s focus or cause another change of context like a page refresh, you must ensure that status messages are communicated to screen reader users using the appropriate roles and properties. To do that, you currently need ARIA live regions. ## What are ARIA live regions? ARIA live regions are **a specific type of notification system primarily surfaced for screen reader users.** Using live regions, you can communicate content updates down to the accessibility layer so that screen readers are made aware of these updates when they happen. On an implementation level, a **live region** (**not to be confused with the`region` landmark**) is an element on the page that has been designated as being “live”. When an element is designated as a live region, **a screen reader is notified when any updates take place within the element (and its children), wherever its focus is at the time.** "Think of live regions as something like a livestream" says Web accessibility specialist Ugi Kutluoglu, "everything happening inside will be announced live like a news channel you’re listening to in the background." Using live regions, you can mark up status messages and other similar updates so that they are communicated to screen reader users. Here is our email notification example again with ARIA live regions working. Notice how when the ‘Send Email’ button is activated, NVDA announces the contents of the status message that is shown: Sorry, your browser doesn't support embedded videos. The screen reader announces the contents of the message because I’ve designated the message container as a live region (we’re going to learn how to do that shortly). So now the element is monitored for updates and the screen reader is notified of these updates when they happen. Then, when the button is activated, I inserted the contents of the message into the message container. And when I did, the screen reader was notified and it announced the update to the user. When an update happens in a live region, the screen reader announces that update, and it only announces it once. Toast messages and other similar status messages are the closest **visual equivalent** of a live region announcement. (Though not all toast messages are a good candidate for live regions. We’ll talk more about this later.) A toast message is used to present **timely information** — including confirmation of actions, statuses, and alerts. By nature, **toast messages are auto-expiring** — they disappear on their own after a few seconds. And once they disappear, they’re gone. The user cannot review the message again. Like toasts, **live region notifications are transient.** **Once an announcement is made, it disappears forever.** They cannot be reviewed, replayed, or revealed later. If the user misses an announcement, they miss it. It’s gone. That is, unless you provide them with a way to review it (like collecting all notifications in a notifications center, for example). Because of their transient nature, live regions have specific and limited use cases and should not be used as an alternative to other more persistent approaches. In fact, if you _can_ use another more persistent approach, you almost definitely should. We’ll talk more about how to use live regions and when _not_ to use live regions later in the chapter. ## Creating a live region Using ARIA, **almost any element can be designated as a live region**. It doesn’t need to be a structural element; and it doesn’t need to have any implicit semantics by default. You can designate an element as a live region using: 1. The `aria-live` attribute. 2. ARIA live region roles. HTML also provides one native element that has implicit live region semantics: the `<output>` element. We’re going to talk more about it in another section. ### 1. Using the `aria-live` attribute `aria-live` is the primary attribute used **to designate an element as a live region.** When used on an element, it indicates that this element may be updated, and those updates should be communicated to screen readers. The value of `aria-live` **describes the types of updates** that can be expected from the region. It accepts three values: `assertive`, `polite`, and `off` (which is equivalent to removing the property altogether). <!-- this div is now a live region! It's as simple as that. --> <div aria-live="[ polite | assertive ]"> ... </div> The value of `aria-live` you choose will depend on **the type, urgency and priority of the update.** * If the update is important enough that it requires the user’s **immediate attention** , `assertive` will tell the screen reader to _immediately_ notify the user, **interrupting whatever the user’s currently doing.** Assertive notifications are good for when users need to immediately know something and act on it, like when there’s **an error** in submitting information in a form, or something more serious like a **session timeout** or a **security alert**. Assertive notifications are very disruptive and should be limited to a few use cases where the messages are critical to the user and require their immediate attention. Otherwise, they may disorient users or cause them not to complete their current task. * `polite` on the other hand, is more… polite. It indicates that the screen reader **should wait until the user is idle** (such as when the screen reader has finished reading the current sentence, or when the user pauses typing) before presenting updates to them. Polite regions do not interrupt the user’s current task. They are more suitable for things like success messages, feeds, chat logs, and loading indicators, for example. `aria-live="off"` is the assumed default value for all elements. It indicates that updates to the element should not be presented to the user **unless the user is currently focused on that region**. So creating a live region is literally as simple as declaring the `aria-live` on an element. Here is an example where I have a `<div>` with no `aria-live` set on it. When you activate the button, the `<div>` will get populated with a message via JavaScript; but the screen reader will not announce the update. So the user will not be aware that any content has been added to the `<div>` at this point. Try adding `aria-live="polite"` or `aria-live="assertive"` to it and then activate the button again. The screen reader will announce the contents of the message even though focus is not moved to the message: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. This is pretty powerful! The live region works even if it is visually-hidden, as long as it is not hidden in a way that removes it from the accessibility tree. (We’ve learned all about choosing an appropriate hiding technique for your content in the hiding techniques chapter.) There are some valid use cases for visually-hidden live regions, but the general rule of thumb is that **if the update or message is visible to all users and the conveyed text is equivalent to the visible text (as is the case for most status messages), then you might as well use the same element for screen reader users that you are using for everyone else** , and designate it as a live region so that all users get the same update. For example, consider the dummy email example from the previous section again. To convey the same status message to screen reader users, all you would need to do is designate the message container as a live region using the `aria-live` property. Error notifications are urgent and require the user’s immediate attention, and you want the user to know that an error has occured as soon as possible. As such, the value of `aria-live` should be `assertive`. <div aria-live="assertive"></div> At this point it is important to note that you should place the live region container in the DOM as early as possible and _then_ populate it with the contents of the message using JavaScript when the notification needs to be announced. **This ensures that the live region is monitored for updates before they happen.** Otherwise, the update may not be communicated to screen readers. We will learn more about best practices for implementing live regions later in the chapter. By default, any padding, margin, and border on an element will take up space in the page’s layout even if the element is empty. Since the message container is placed in the DOM before the notification is shown, you will probably want to prevent it from taking up any visual space on the page when it is empty. To do that, you can use the `:not(:empty)` CSS selector to only apply the visual styles to it when it is _not_ empty (i.e. when the notification is shown). [aria-live="assertive"]:not(:empty) { padding: .25em 1em; background: maroon; ... } Because it is a best practice to include live region containers in the DOM as early as possible, I always use this CSS selector to visually “hide” my live regions when they are empty. And yes, yes I know that you can just apply these styles to the notification using a class name that you could add to the container via JavaScript, but why require JS for something so simple that can so easily be accomplished using CSS? Here is a live demonstration of this implementation: See the Pen Live region status message in email web app by Sara Soueidan (@SaraSoueidan) on CodePen. Fire up a screen reader on the debug version of this demo and then activate the ‘Send’ button. Try removing the `:not(:empty)` selector from the live region’s ruleset in the to see how it affects the visibility of notification container when it is empty. A live region does not need to be initially empty. Here’s another example where I have a list and I’m adding items to the list. I’ve designated the list as a assertive live region using `aria-live`. So now every time an item is added, the screen reader makes an announcement. See the Pen #PracticalA11y: Basic live region by Sara Soueidan (@SaraSoueidan) on CodePen. This means that you can use live regions to communicate different types of updates to an element, such as when content is added to the element or existing content is modified. ### Live region configuration ARIA provides three attributes that enable you to ‘configure’ when the screen reader should make an announcement, and what that announcement should contain: * `aria-relevant`, * `aria-atomic`, and * `aria-busy`. These attributes are _very_ useful and would enable you to use live regions to communicate different kinds of content updates when they are needed, but unfortunately current browser and screen reader support is inconsistent, so you can’t rely on them in your projects just yet. But we’re still going to get a quick overview of what they do because it will help you better understand the current limitations with ARIA live regions. #### aria-relevant: when should an announcement be made? The `aria-relevant` attribute is used to specify **what type of changes in the live region should trigger an announcement.** For example, should the screen reader make an announcement when a node is _added_ to the region? or when a node is _removed_? or when the text within an element changes? or maybe when any of these updates happen? `aria-relevant` accepts a space-separated list of the following values: `additions`, `removals`, `text`, and a single catch-all value: `all`. * `additions` will trigger a notification **when a DOM node is added to the region.** * `removals` will trigger a notification **when a DOM node is removed from the region.** * `text` will trigger when **text changes happen inside the region** , such as changing a text node inside the region or changing a text alternative for an image inside the region. * `all` is a shorthand for all three options. The default value is `additions text`, which means that a live region will trigger an announcement when content is added or text is changed within the region. The `removals` and `all` values should be used sparingly. Screen reader users only need to be informed of content removal when its removal represents an important change, such as when a user is removed from the list of active users in a chat room, for example. #### aria-atomic: what is contained in an announcement? The `aria-atomic` attribute determines what is contained in the announcement. It indicates whether the screen reader should present all or only parts of the changed element based on the change notifications defined by the `aria-relevant` attribute. For example, if a piece of text changes inside an element, should the screen reader announce only the changed text? or the entire contents of the live region? If text is _added_ to a live region, should only the newly added text be announced? or should the entire region’s content be announced every time? `aria-atomic` accepts two values: `true`, and `false`. * When `aria-atomic` is `false`, a screen reader should only announce the parts of the element that have changed. **This is the default value.** * When `aria-atomic` is `true`, the screen reader should announce the entire contents of the live region when a change happens inside of it. It doesn’t matter what has changed. It’s going to read everything — the entire content of the live region, plus the region’s accessible name, if it has one. `aria-atomic="true"` is useful for when a part of the region changes but you want the entire content to be read because otherwise the updated content may not make much sense on its own. A practical example is a “Now Playing” indicator. <p> <span>Now Playing:</span> <span>[ movie/soundtrack title ]</span> </p> If a playlist of movies or soundtracks is playing while the user performs other tasks on the page, and the name of the soundtrack that is currently playing changes, you can utilize live regions to announce that a new soundtrack is playing. When the soundtrack changes, the only part of the indicator that gets updated is the soundtrack’s name. But you want the entire sentence to be announced so that the user gets the context they need. You can do that by designating the indicator as an atomic live region (using `aria-atomic="true"`): <p aria-live="polite" aria-atomic="true"><span>Now Playing:</span><span>[ movie/soundtrack title ]</span></p> Now every time the title of the soundtrack or movie changes, the screen reader should announce “Now playing” followed by the name of the soundtrack. Here is a live demo: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. Start a screen reader and try out the debug version of the playing indicator #### aria-busy: please wait until the changes are complete The `aria-busy` attribute is used to indicate that an element (typically an entire section on the page) is undergoing changes (such as a section loading new content), and that screen readers should therefore **wait until the changes are complete before exposing the content to the user**. By default, all elements have an `aria-busy` value of `false`. Meaning that they are _not_ undergoing changes and screen readers can, therefore, expose their content when the user navigates to them. To use `aria-busy`, you would set it to `true` on an element while the element is undergoing changes, and then flip its value to `false` when the changes are complete and ready to be exposed or announced to the user. `aria-busy` can be used on any element that is undergoing changes, even if that element is not a live region. If you use `aria-busy` on a live region, the contents of the live region will be announced after `aria-busy` is set to `false`. If multiple changes have been made to the element while it was busy, they are announced as a single unit of speech when `aria-busy` is turned off. ‘Skeleton screens’ in Single Page Applications (SPA) are a practical use case for the `aria-busy` attribute. A skeleton screen is a specific type of loading indicator that is shown in lieu of the content of a section being loaded until the content of that section loads. They often provide a wireframe-like visual that mimics the layout of the page and helps users build a mental model of what will be on the page when the content loads. `aria-busy` can be used to tell screen readers to ignore the section that is currently loading content until the content finishes loading. In that sense, it has a similar effect to `aria-hidden` — it ‘hides’ the contents of a busy region while it is undergoing changes. <!-- This section is updating... --> <section aria-busy="true"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> Since the busy section is effectively hidden from screen reader users, you will want to communicate the state of the loading content to screen reader users. You can do that by using a separate, visually-hidden live region. Using this region, you can communicate to the user that a screen has started loading, and then let them know when the loading is complete. So, when the content is loading, your markup would at a certain moment look like this: <div aria-live="polite" class="visually-hidden">Loading content...</div> <section aria-busy="true"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> and then when the content is loaded, flip the value of `aria-busy` to `false`, and update the content of the live region: <div aria-live="polite" class="visually-hidden">Content loaded.</div> <section aria-busy="false"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> This is an example of when a live region can be used exclusively for notifying screen reader users, but doesn’t need to be rendered visually because there is an alternative visual indicator for sighted users (the skeleton screen, in our case). So you can think of the live region like a text alternative for the skeleton screen in this case. Unfortunately, because `aria-busy` is currently not well-supported across screen reader and browser pairings, most screen readers (except JAWS) will read the contents of the busy region even before it’s done loading, which would result in a sub-optimal experience. You currently need to work your way around that by hiding the busy region using `aria-hidden`, and un-hiding it when its contents are done loading. For a detailed writeup about implementing accessible skeleton screens, check out Adrian Roselli’s article “More Accessible Skeletons”. Adrian provides a solution that doesn’t even require you to use live regions at all, and his article includes a live demo that you can tinker with and try for yourself. #### Summary and support landscape Properties for configuring live region announcements Value | Description ---|--- `aria-atomic` | _What is announced? When you update a live region, should it read all the content again or just the added content?_ If `true`: Announce the entire content of the live region, including its label, if present. If `false`: announce only the changed content. `aria-relevant` | _When is an announcement made? What types of changes to a live region should trigger the announcement? additions? removals? or all?_ If `additions`: Trigger an announcement when new elements are added to the accessibility tree of the live region. If `text`: Trigger an announcement when text content or a text alternative is added to any descendant in the accessibility tree of the live region. If `removals`: Trigger an announcement when an element, text, or text alternative is removed from the accessibility tree of the live region. If `additions text (default)`: Equivalent to the combination of `additions` and `text`. If `all`: Equivalent to the combination of `additions removals text`. `aria-busy` | Indicates that an entire section on the page is undergoing changes (such as a section loading new content), and you're telling screen readers to **wait until the changes are complete before notifying the user** If `true`: The element is being updated. If `false`: There are no expected updates for the element. As we mentioned earlier, **support for the`aria-relevant`, `aria-atomic`, and `aria-busy` attributes is currently inconsistent across browsers and screen reader pairings.** Paul J. Adam has created a test page that includes test cases for `aria-atomic` and `aria-relevant` when used on live regions, and has documented support gaps across platforms and screen readers. So, unfortunately, you can’t rely on these properties in your projects just yet. If you do, many of your content updates may be announced in ways that you did not intend them to be announced, which could result in a sub-optimal user experience. ### 2. Using live region roles When you use `aria-live` to create a live region, the element’s implicit semantics (if it has any) are retained. This means that you can use the appropriate element to represent the component you’re creating, and if the component is getting updated you can then designate it as a live region with the `aria-live` attribute. <!-- this list will be treated like any list on the page would be; since it is also designated as being live, any changes that happen to it should be communicated to screen readers and announced to the user --> <ul aria-live="polite"> <li>My list semantics are important.</li> <li>But I want you to know when new list items are added.</li> </ul> But what if you’re creating a notification or status message that has no semantic HTML element to represent it? For example, there are no semantic elements to represent (and distinguish between) different types of notifications (such as an alert notification or a status message, for example). While it is fine to use a `<div aria-live="">` for these notifications, it would be ideal if we exposed the nature or type of a notification to the user using appropriate semantics. ARIA provides five live regions roles that semantically represent five different types of updates: * **The`alert` role:** represents a live region with important, and usually time-sensitive information, such as error notifications. * **The`status` role:** represents a live region whose content is advisory information for the user but is not important enough to justify an alert, often but not necessarily presented as a status bar (such as a status or success message). * **The`log` role:** represents a live region where new information is added **in meaningful order** , and old information may disappear. Examples of logs are chat logs, messaging history, a game log, or an error log. In contrast to other live regions, **in this role there is a relationship between the arrival of new items in the log and the reading order.** The log contains a meaningful sequence and new information is added only to the end of the log, not at arbitrary points. * **The`marquee` role:** represents a live region where non-essential information changes frequently, such as stock tickers. The primary difference between a marquee and a log is that logs usually have a meaningful order or sequence of important content changes. * **The`timer` role:** represents a live region containing a numerical counter which indicates an amount of elapsed time from a start point, or the time remaining until an end point. Live region roles are **pre-configured**. They come with _implicit_ `aria-live` and `aria-atomic` values. ARIA live region roles and their implicit `aria-live` and `aria-atomic` mappings Role | `aria-live` value | `aria-atomic` value ---|---|--- `alert` | `assertive` | `true` `status` | `polite` | `true` `log` | `polite` `marquee` | `off` `timer` | `off` `alert` and `status` are the most commonly used live regions roles and have generally good support. The others have specialized uses and have **poor or no support** , and `marquee` and `timer` are even in danger of being deprecated and removed from the ARIA specification. #### Difference between using `aria-live` and live region roles The primary difference between using live region roles and using `aria-live` on its own is that **live region roles have semantic meaning.** They add explicit _semantics_ to an element ("This is an alert", "This is a status message", etc.), so some screen readers may announce “alert” before announcing the content of the message. For example, here is the dummy email app example again. Instead of using `aria-live="assertive"` on the notification container, I’m using `role="alert`. Here’s a video comparing how NVDA announces the notification, first when it is designated as a live region using `aria-live="assertive"`, and the when it is designated as a live region using `role="alert"`. Sorry, your browser doesn't support embedded videos. NVDA announces the word “Alert” before announcing the content of the notification when `role="alert"` is used. You can try it for yourself in the debug version of the demo using the role attribute. Here is another example that implements a form success message using `role="status"`: See the Pen #PracticalA11y: role status success message by Sara Soueidan (@SaraSoueidan) on CodePen. Another advantage to using a live region role over `aria-live` is that **live region roles accept an accessible name.** If you use `aria-live` to create a live region, the implicit semantics of the element you’re using it on will determine whether or not it can have an accessible name. As we learned in the accessible names and descriptions chapter, some elements are name-prohibited. For instance, a `<div>` will not consistently expose an accessible name unless you give it a meaningful role. ARIA live region roles provide meaningful roles to the elements they are used on and can therefore accept an accessible name. When a live region has an accessible name, screen readers include the name of the region in the announcement. See the Pen #PracticalA11y: shopping cart by Sara Soueidan (@SaraSoueidan) on CodePen. In this example I have a polite live region that contains the number of items in the user’s shopping cart. When the ‘Add to cart’ button is activated, the number of items is updated and the screen reader announces that number. But anouncing the number of items alone doesn’t provide the user with the same context that sighted users get when the shopping cart is visually updated. Ideally, you’d want the screen reader to announce “Shopping cart, 5 items”. Using `aria-labelledby`, you can provide an accessible name to the live region (namely: “Shopping Cart”). So now when the number of items is announced, the screen reader announces ‘Shopping cart’ before announcing the number of items it contains. You can try the live example out for yourself in the debug version of the shopping cart example. Providing an accessible name to a live region is useful for when you may have multiple updating regions on the page and you want to communicate which region the updates are coming from. A region’s name thus provides the necessary context for each announcement. ### 3. Using the HTML `output` element HTML provides one native live region element: `<output>`. By definition, `<output>` represents an element into which you can inject the results of a calculation **or the outcome of a user action.** The second part of the definition can be interpreted to mean that the `<output>` element can be used to display a feedback message as a result of user interaction (like a toast or status message!). The `<output>` element has implicit live region semantics. It maps to the ARIA `status` role, which means that it represents a polite live region. `<output>` is also a labelable element, which means that you can give it an accessible name using the `<label>` element. <label for="[ outputID ]">..</label> <output id="[ outputID ]"> .. </output> <!-- or --> <label for="[ outputID ]"> .. <output id="[ outputID ]"> </output> </label> A practical use case for the `<output>` element is using it to represent the total price of products in a cart on an e-commerce website. <label for="result">Your total is:</label> <output id="result"> </output> <!-- or --> <label for="result"> Your total is: <output id="result"> </output> </label> When the user updates the number of items in their cart, the total price is updated to reflect the new total price. Wrapping the price in an `<output>` element allows it to be announced by the screen reader when it is updated. Here is a dummy example where the total price is updated based on how many items are chosen in the select dropdown: See the Pen #PracticalA11y: The <output> live region element by Sara Soueidan (@SaraSoueidan) on CodePen. You have probably also noticed that VO with Safari announces the initial total value before it announces the updated total value every time it makes an announcement. The `<output>` element is currently not consistently announced across browser and screen reader pairings. And not all screen readers announce the accessible name of the `<output>` when its content is updated. For example, VoiceOver with Safari announces the content of the `<output>` element in the example above but it does not announce its accessible name. NVDA with Firefox does not announce the accessible name either. Whereas paired with Chrome, VoiceOver announces the contents of `<output>` with its accessible name as it is intended. There are also other quirks and some inconsistencies in the way `<output>` is currently announced across browser and screen reader pairings. Accessibility engineer Scott O’Hara has written a great article about the `<output>` element that I recommend reading if you want to learn more details about `<output>` and its quirks. Scott shares the current state of support, as well as suggestions for working around some of the support gaps. ## Summary and outro So, to quickly sum up: * ARIA live regions are a specific type of notification system primarily surfaced for screen reader users. * You can create a live region using the `aria-live` attribute. The value of the attribute depends on the type and urgency of the updates you’re communicating. * The `aria-relevant`, `aria-atomic`, and `aria-busy` attributes allow you to configure when an announcement should be made and what the announcement should contain. But support for these attributes is currently poor. * ARIA provides five roles that represent five different types of updates. Of these five roles, `alert` and `status` have the best support and can be used to represent status messages in web applications. * The `<output>` element is currently the only native HTML live region. The `<output>` element has a few quirks and some support gaps that, depending on your use case, you may be able to work around today. As you might imagine, the current state of support for live region features and properties limits your uses of live regions quite a bit. Furthermore, the inherent behavior of live regions also makes them unsuitable for certain types of updates. We’re going to elaborate more on this in the second part of this chapter. Fortunately, for many (if not most!) common UI patterns, there’s often a more robust way to make users aware of content updates when they happen. And for the few instances when you do need to use live regions, you can make them work by following a few implementation best practices. We will discuss all of that in more detail in the second part of this chapter. Many thanks to **James Edwards** (@siblingpastry) for reviewing this chapter.
www.sarasoueidan.com
September 17, 2025 at 7:41 PM
Setting up a screen reader testing environment on your computer
When you’re designing and developing for accessibility, performing manual testing using a screen reader is important to catch and fix accessibility and usability issues that cannot be caught by automated accessibility checkers. You can catch the majority of issues by performing testing **using the screen readers that your users rely on the most.** If you haven’t already, you want to set up a screen reader testing environment on your computer, and invest a little learning time to get acquainted with the most relevant screen reader commands and shortcuts that you will need to perfom basic manual testing with a screen reader on a day-to-day basis. In this chapter, we will walk through setting up a screen reader testing environment on your computer. We will discuss software options you have to do that (both free and premium), and **what screen reader and browser combinations to test with.** We will also go through enabling accessibility testing on a Mac (which requires a little manual work to do). And finally, we will learn about a few useful features and cheasheets that make testing a little friendlier when you’re just getting started. ## macOS vs Windows screen readers Both Windows and macOS come with screen readers built into them that are available for free. The built-in Windows screen reader is called **Narrator**. The macOS built-in screen reader is **VoiceOver**. According to WebAIM’s screen reader user survey, **more than 90% of screen reader users reported being on Windows**. And according to the same survey, **the two most popular screen readers areJAWS (Job Access With Speech) and NVDA (NonVisual Desktop Access)** (which are both Windows screen readers), followed by VoiceOver. If you’re already on Windows or if you own a Windows machine, you’re already halfway through setting up your screen reader-testing environment. If you’re on a macOS computer, **you shouldn’t test solely with VoiceOver.** It is more opinionated and does not always reflect what the majority of screen reader users experiences. If you’re on macOS and you have no access to a Windows machine (whether an actual machine or a virtual one), you can test your work with Windows screen readers using any modern browser instead. We’ll get back to this in another section. ## Setting up Windows screen readers JAWS is the most popular and feature-rich screen reader. a JAWS license isn’t free and is faily expensive. But you can still use it to perform testing for your work. **JAWS will run in full in demo mode for 40 minutes at a time, until it is activated on your computer.** While this is a limitation for longer testing sessions, the 40 minutes are usually more than enough to perform basic testing. **NVDA is a feature-rich,_free_ alternative to JAWS**. We will install and set up NVDA in the following sections. ### Download NVDA screen reader on Windows Go to the NVAccess Web site. Click the Download link. That will take you to the NVDA download page. NVDA is available for free, but a donation is strongly encouraged. Click the **Download** button. Wait for NVDA to download. And go through the installation wizard when it’s done. #### Visualize NVDA’s current focus target with Visual Highlight To make testing with NVDA more convenient (especially if you’re new to screen reader testing), I recommend enabling NVDA’s Visual Highlight feature. To enable it, go to **Preferences** > **Settings** > **Vision** > **Visual Highlight** , and check the **Enable Highlighting** option. What this does is it shows a focus highlight around the element that NVDA is currently focused on — whether it’s in a webpage or anywhere on your system. This feature is useful for partially-sighted screen reader users who want to track the location of the NVDA navigator object and the currently-focused element. Seeing where the screen reader’s current focus is at is also helpful for _you_ when you’re performing testing, especially if you’re recording your screen for an educational video, for example. ^^ #### Enable NVDA speech viewer Another helpful feature you can enable is the **NVDA Speech Viewer** log window. Click the NVDA icon in your taskbar (on the bottom right of your screen by default), and go to **NVDA** > **Tools** and enable **Speech Viewer**. You also have the option to open the speech viewer log window by default on NVDA startup. The speech viewer log window contains the text that NVDA speaks, which can be helpful when you’re just getting started with screen reader testing. Just keep in mind that its usefulness of sometimes limited because the log often does not fully represent what is announced. #### Setup keyboard layout for testing with NVDA on a Mac If you’re on a Mac, go to **NVDA** > **Preferences** > **Settings** > **Keyboard** and Choose “**Laptop** ” Keyboard layout instead of the default Desktop option. The desktop layout relies on many keys which do not exist on some Mac keyboards. You can also set this preference in NVDA’s start popup menu. If you're using JAWS, there is a similar option in the JAWS startup wizard to choose the Laptop keyboard layout instead of the default Desktop layout. ### Map the Insert key to another key on Mac The `insert` key is the default modifier key used by most screen readers on Windows. If you don’t own an external keyboard that has an `insert` key, you might need to use a software work-around to make up for the lack of the `insert` key on your keyboard. NVDA settings include an option to set the `caps lock` key as the NVDA modifier key. You can do that if you prefer. I personally prefer to not do that because it interferes with typing when the `caps lock` is On. Alternatively, you can use a software program to map one of your less-used keyboard keys to the missing `insert` key. I use Karabiner Elements. #### Setting up Karabiner Elements on macOS Karabiner is a free app. To use it: 1. Download the app from the Karabiner Elements Website. **You want to download it on your Mac, not in your virtual machine.** 2. Run through the setup, and make sure to enable access in your **System Preferences** settings if it is blocked by macOS (which it probably will be by default). 3. Once it is installed and your keyboard is recognized, go to **Simple Modifications**. 4. Choose the device(s) you want to create a mapping for, and then click **Add Item** to map an unused key to the insert key. In my Karabiner, I mapped the right `option` key to the Windows `insert` key. And I also mapped the right `cmd` key to the `print screen` key, which can be used in combination with other keys to quickly turn Windows High Contrast mode On and Off (which is a shortcut that will come in handy in another chapter). That’s it. Now if you open your VM and fire up a screen reader, you can use the right `option` key (or the key of your choice) as a modifier key in place of the `insert` key. ## Virtual accessibility testing in your browser If you’re on macOS and you have no access to a Windows machine, you can test your work with Windows screen readers using any modern browser instead. You can do that using a service called AssistivLabs. **AssistivLabs is to screen reader testing what BrowserStack is to cross-browser testing.** It **remotely connects you to real assistive technologies** (like NVDA, JAWS, and Windows High Contrast Mode) using any modern web browser. AssitivLabs _currently_ only offers testing with _Windows_ screen readers and assistive technologies (like Windows High Contrast Mode and Windows Magnifier) for most accounts; testing using macOS assistive technologies will be available in the future. AssitivLabs is a paid service — it’s not available for free by default. But it is very helpful for when and if getting access to a Windows machine is otherwise not possible. Note that Practical Accessibility course enrollees will get **a 6-months unlimited free trial** to Assistivlabs. 🎁 ## Enable keyboard accessibility on a Mac To complement your screen reader, you should enable keyboard accessibility on your Mac. Keyboard accessibility is not enabled by default on macOS. If you’ve ever tried to tab your way through interactive and focusable elements on webpages and couldn’t, that’s why. (Frustrating, I know.) **You need to manually enable keyboard accessibility on macOS** by going to **System Preferences** > **Keyboard** , and enabling the “**Use keyboard navigation to move focus between controls** ” option in the **Shortcuts** tab. On macOS 13+, you’ll go to **System Preferences** > **Keyboard** , and then enable the **Keyboard Navigation** (Use keyboard navigation to move focus between controls. Press the Tab key to move focus forwards and Shift Tab to move focus backwards) option. Once you’ve enabled system-wide keyboard accessibility, you want to also enable keyboard tabbing in Safari. In Safari, go to **Preferences** > **Advanced**. And enable the “**Press tab to highlight each item on a webpage** ” option. Now you can tab your way through webpages as you should. If you're on an older version of macOS or you want to enable these settings in Firefox and Chrome, I've added a couple of resources to help you do that in the recommended resources section at the end of the chapter. ## Which browser and screen reader pairings should you test on? **Screen readers work best when they are paired with the browsers they are the most compatible with.** When performing testing, you can catch most accessibility issues (sometimes even all of them) by pairing each screen reader with the browser it is most commonly used with. ### On macOS **VoiceOver works best with (and should, therefore, be paired with) Safari.** If you use VoiceOver with Chrome or Firefox, for example, you might get unexpected results because VoiceOver is **optimized** to work with Safari not with other browsers. ### On Windows **Narrator works best with Edge** , and has difficulty interfacing with other browsers. But Narrator isn’t most users’ first choice. **JAWS** — the most popular of all screen readers on Windows — works best with Chrome and Firefox. When perfoming testing, **pair it with Chrome.** **NVDA works best and is commonly paired with Firefox.** ### Mobile screen readers Throughout this course, we will focus mainly on desktop screen reader testing. But you should test your work using mobile screen readers as well. In WebAIM’s ninth screen reader user survey, **90% of respondents reported using a screen reader on a mobile device.** According to WebAIM, this number has increased over the last 12 years. WebAIM also notes that participants with disabilities (91.6%) are more likely to use a mobile screen reader compared to individuals surveyed without disabilities (71.4%). So it is very important that you test your work on mobile to ensure that it works for a large group of screen reader users. VoiceOver on iOS/iPadOS is the most popular mobile screen reader. VoiceOver comes bundled with iOS/iPadOS. Like its desktop version, you want to **use it in conjunction with mobile Safari.** On Android, **Talkback (the built-in screen reader) is best paired with Chrome.** ## Guides to browsing and navigating content with a screen reader Make some time to learn how to navigate and browse web content with each screen reader. It might take some time and feel like a steep learning curve at first, but by doing that you will gain an invaluable skill for your accessibility work. Here is a list of official user guides that are helpful for getting started: * NVDA User guide. Most read-only webpages are browsed in NVDA using Browse mode. * JAWS documentation (Shortcut to JAWS Hotkeys) * Complete guide to Narrator (Shortcut to Narrator keyboard commands and touch gestures) * VoiceOver Guide * Use VoiceOver to browse webpages on Mac * Apple VoiceOver Command charts * Talkback user guides * Turn on and practice VoiceOver on iPhone And to get a high-level (yet practical) overview of how someone using a screen reader browses the Web, I recommend watching the Browsing with assistive technologies video series by Tetralogical: * Browsing with a desktop screen reader * Browsing with a mobile screen reader ## Screen reader keyboard shortcut cheatsheets When you’re just getting started with screen reader testing, and you want to test with at least three screen readers across different platforms and devices, it can be difficult to remember all the keyboard shortcuts for each screen reader right away. Deque University provides useful screen reader keyboard shortcuts and gestures cheatsheets, that you can either reference on your computer, or print out and have them handy during your testing. * Desktop Screen Readers Survival Guide - Basic Keyboard Shortcuts * Desktop Screen Readers Forms Guide * NVDA keyboard shortcuts * JAWS keyboard shortcuts * Narrator keyboard shortcuts * VoiceOver keyboard shortcuts ## Resources and recommended reading * Efficiency in Accessibility Testing or, Why Usability Testing Should be Last * Checking Windows High Contrast mode on a Mac for free (inculdes instructions to download and set-up VirtualBox) * Using Windows Screen Readers on a Mac * The WebAIM screen reader user survey * No, tabbing is not broken. Yes, I was confused too. * Browser keyboard navigation in macOS * The Importance Of Manual Accessibility Testing * Your Accessibility Claims Are Wrong, Unless… * Relevant combinations of screen readers and browsers Hat tip and thanks to Adrian Roselli for pointing out that the Focus Highlight NVDA add-on is no longer necessary since focus highlighting has been built into NVDA 2019. And thank you to Corentin H. for providing the screenshot of keyboard accessibility preferences on macOS 13.
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
In Quest of Search
**Update:** There now exists a native HTML `<search>` element that maps to the ARIA `search` role. 🎊 As of March 24th, 2023, the HTML specification added a new grouping element: The `<search>` element. Read more about the element in Scott’s introductory blog post. There’s been a recent discussion on Twitter about the idea of adding a new element in HTML that represents a search interface. A search form, basically. The idea is to create a semantic HTML element for the ARIA `search` role, which represents a landmark region “that contains a collection of items and objects that, as a whole, combine to create a search facility.” Opinions have been shared in the Twitter thread about whether adding a new HTML element is necessary. Many have argued that it was unnecessary because we can use the ARIA `search` role and repurpose a `form` element to create the same semantics. I disagree. And this article is the longer version of **my personal opinion** on the subject. ## tl;dr I do strongly encourage the addition of a new HTML element that represents—and can consequently obviate the use of—the ARIA `search` landmark role. A search element would provide HTML parity with the ARIA role, and encourage less use of ARIA in favor of native HTML elements. The suggested element would be syntactic sugar for `<div role="search">` like `<main>` is syntactic sugar for `<div role="main">`. This means that it would an HTML sectioning element, not a replacement for another element. I would choose `<search>` as a name for that element. In my mind, `<search>` would be to `role="search"` what `<nav>` is to `role="navigation"`. But any other appropriate name would, of course, also work. The rest of this article is my reasoning for encouraging the idea of adding a semantic HTML element for search. ## HTML and ARIA landmark roles The ARIA specification includes a list of ARIA **roles** that are used to define regions of a page as landmarks: * `banner` * `complementary` * `contentinfo` * `form` * `main` * `navigation` * `region` * `search` HTML currently contains 112 elements. Eight of those elements are sectioning elements: `main`, `nav`, `aside`, `header`, `footer`, `article`, `section`, `form`. Seven of these HTML sectioning elements are mapped to ARIA landmarks, which are used by assistive technologies (ATs). * `header` is the HTML native equivalent for ARIA’s `role="banner"` (when it is scoped to the `body` element. See HTML Accessibility API Mappings for more information.) * `footer` is the HTML native equivalent for ARIA’s `role="complementary"` (also in the context of the `body` element) * `nav` is the HTML native equivalent for ARIA’s `role="navigation"` * `main` is the HTML equivalent for ARIA’s `role="main"` * `form` is the HTML equivalent for ARIA’s `role="form"` * `aside` is the HTML equivalent of ARIA’s `role="complementary"` * `section` is the HTML native equivalent for ARIA’s `role="region"` (when it has an accessible name) It is because these elements exist that we often don’t need to use ARIA’s equivalent roles (unless we absolutely _have_ to repurpose another element using those roles, or expose an element to ATs when it is outside of its expected context). If `<nav>` exists, why should a `<search>` (or whatever other name it gets) not? If `<search>` is to be deemed unnecessary because `role="search"` exists, wouldn’t this also mean that `<nav>` (and other landmark elements) would be considered _redundant_ because `role="nav"` (and other ARIA roles) exists? ## HTML and ARIA landmarks, beyond semantics ARIA landmark roles are roles assigned to regions of a page that are intended as **navigational landmarks**. Using ARIA landmarks (or their equivalent native HTML elements when they exist) is meant to also facilitate user navigation. From the W3C WAI-ARIA Editor’s Draft: > Assistive technologies SHOULD enable users to quickly navigate to elements with role search. User agents SHOULD treat elements with role search as navigational landmarks. User agents MAY enable users to quickly navigate to elements with role search. When HTML sectioning elements (and/or ARIA landmark roles) are appropriately used on a page, assistive technology users such as screen readers users could use those landmarks to navigate the page more efficiently, allowing them to jump to the area of the page that they want. For example, if the `<nav>` element (or, equivalently, the `role="navigation"` ARIA role on a qualifying element) is used to wrap a page’s navigation, the navigation shows up in the VoiceOver Rotor on macOS. Similarly, using the `main` element will make the main section of the page show up in the landmarks menu. The user can then quickly jump straight to the navigation section or to the main content area of the page if they want to, bypassing other regions of the page. This increases the user’s efficiency and improves their navigation experience. Similarly, when you use `role="search"` on a `form` element, that form will show up as a search region in the landmarks menu. The user can then jump to the search form if they need to quickly search for something. The search form on WebAIM's Web site shows up in the Landmarks menu by VoiceOver on macOS because `role="search"` ARIA role is present on the `form` element. The search form on Smashing Magazine's Web site is not recognized as a search landmark by VoiceOver on macOS because `role="search"` ARIA role is absent on the `form` element. _If HTML sectioning elements are used without understanding the associated landmark structure, assistive technology users will most likely be confused and less efficient in accessing content and interacting with web pages._ ### But is a native search landmark worth it? Yes, it is. Search is one of the most common and most used sections of many Web sites. Of course, a “It Depends” is warranted here, too. Depending on the Web site, search might be the first thing a user looks for and uses on a given site. E-commerce Web sites are a great example of where search forms are essential and heavily used. Educational and documentation sites are another example. Take MDN, for example. Search is so important and on MDN that the site even includes a Skip Link that enables keyboard users to skip straight to the search field. Now I don’t have any user research data or anything, but I would assume that the skip link was added because of how frequently users reach for the search field to look up documentation about specific topics they’re searching for. ## Just because an ARIA role exists, it doesn’t eliminate the usefulness of a native HTML equivalent I’ll just say it again: ust because an ARIA role exists, it doesn’t eliminate the usefulness of a native HTML equivalent. ## The purpose of ARIA …is to provide parity with HTML semantics. It is meant to be used to **fill in the gaps** and provide semantic meaning where HTML falls short. ARIA is **not meant to _replace_ HTML.** If anything, the need to use ARIA as ‘polyfill’ for HTML semantics could be considered as a sign and a constant reminder of the fact that HTML falls short on some semantics that benefit users of assistive technologies. This is due to the lack of native HTML elements that provide the meaning (and sometimes, by extension, the behavior) that these ATs need to convey to their users. If we can get an HTML element that fills a part of the gap, it’s only going to be a win—no matter how small of a win it might seem. > > ARIA is not meant to replace HTML > > this! In fact, I think we might want it to go the other way around, with HTML replacing ARIA bit by bit until its services are no longer required > > — Hidde (@hdv) September 15, 2021 ## The first rule of ARIA The first rule of ARIA use in HTML states that you should **avoid using ARIA if there is a native HTML element with the semantics of behavior that you require already built in.** If such an element exists, you should reach for that element instead. This means that ARIA should be **a second resort, not a first approach.** By providing HTML elements that are implicitly mapped to ARIA roles, we can encourage the use of proper HTML markup to convey semantic meaning, and spread more awareness to help avoid both overuse and misuse of ARIA in general. If we can get an HTML element that enables us to use ARIA less, then that element should, in my opinion, be a welcomed addition. ## Outro A native search element might feel like a _small_ technical win to many, but the consistency it provides, the HTML semantics gap it fills, and the awareness it could potentially help spread would all make it a useful and welcomed addition. 112 to 113 HTML elements? I hope so.
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
The CSS prefers-color-scheme user query and order of preference
I spent some time in Reeder app this morning, catching up with RSS and the latest articles published by my favorite blogs. I was reading Scott O’Hara’s article about using JavaScript to detect high contrast and dark modes, which includes a small, very useful script to do exactly what the title says. The output of that script at first looked like it was a “false positive”. But some further investigation led me to learn something new about the `prefers-color-scheme` CSS user query. Scott’s article includes a Codepen to demonstrate the output of the script. The script will check and detect if you currently have high contrast mode or dark mode enabled, and will output the result of the check. See the pen (@scottohara) on CodePen. Since JavaScript doesn’t run in Reeder app, I clicked to open the original article on Scott’s Web site. That’s when I saw that the script was reporting that I had dark mode ON, even though I don’t have dark mode enabled on my phone. Having just recently updated to iOS 15, my first thought that this might be a browser/OS bug or something. But then it hit me: I _do_ have dark mode enabled… _in Reeder app_. (Reeder has a nice dark mode which I enjoy reading in.) This instantly led me to question whether the media query was picking up _that_ dark mode, instead of the OS-level preference. When I opened the article on Scott’s Web site, I opened it in Reeder’s in-app browser. Which means that the script was running in that context when it reported that dark mode was ON. So to test my assumption further, I opened the article in iOS Safari, which is running in the Light scheme mode (set on the OS-level). The script does not report that dark mode is ON in that context. In order to confirm this behavior, I checked the results of the test in Reeder app on my Mac, which is running dark mode on OS-level. I toggled the theme in Reeder app between Light and Dark to verify the results. Sure enough, the script detected dark mode ON when the app theme was set to Dark, but not when the app theme was set to Light. The `prefers-color-scheme` media query picks up the dark mode set in the app. Note that dark mode is also enabled on the OS level, but the media query is picking up the color theme from the app context. App color theme taking precedence over OS-level theme. Even though dark mode is enabled on the OS level, the `prefers-color-scheme` media query picks up the light mode set in the app when the app’s theme is the classic light. In an attempt to verify whether this was a bug or a feature, I checked the specification. The spec includes these two paragraphs: > The method by which the user expresses their preference can vary. It might be a system-wide setting exposed by the Operating System, or a setting controlled by the user agent. […] User preferences can also vary by medium. […] UAs are expected to take such variances into consideration so that prefers-color-scheme reflects preferences appropriate to the medium rather than preferences taken out of context. That explains it. **UA preference > OS-level preference.** Something to keep in mind for when an “unexpected behavior” happens. A good reminder to always test and check the specifications. Had this not been in the spec, then further investigation might have led to an existing bug report or to the creation of one. Who knows. * * * And _that_ was my first #TIL moment of the day. **Stay curious.** (Oh and also: **RSS is awesome.** Thank you to everyone providing an RSS feed for their content. _You_ are awesome.)
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
Accessible notifications with ARIA Live Regions (Part 2)
In the first part of this chapter we discussed what we might need live regions for, and how to create them using HTML and ARIA. In this part, we’re going to discuss what live regions are _not_ suitable for and why, and we’re going to discuss more robust ways to implement some common UI patterns that you might otherwise consider using live regions for. After that, we’re going to go over some best practices for implementing live regions for when you do need to use them to represent things like status messages in your web applications. Live regions are easy to misuse and to _overuse_. Aside from inconsistent browser and screen reader support, live regions’ inherent capabilities are limited _by design_ , which makes them unsuitable for certain types of content updates. ## Live regions don’t handle rich text Live regions don’t handle rich text. This means that the semantics of elements like headings, lists, links, buttons, and other structural or interactive elements are not conveyed when the contents of a live region are announced. If a live region contains a button, for example, the screen reader will announce the text of the button when it is injected into the live region without any mention of the button’s role: <div aria-live="polite"> <!-- The semantics of this button are not conveyed in the live region announcement --> <button>You'll have to guess what I represent!</button> </div> The fact that the text represents the label of a button will not be communicated by the screen reader in the live region announcement. (Example borrowed from Scott O’hara’s article) Here is how VoiceOver with Safari announces a button when I add the button to a live region using JavaScript: Sorry, your browser doesn't support embedded videos. The screen reader will announce the entire contents of a live region as one long string of text, without any of the structure. This is why **you should not wrap entire sections of content in a live region.** Otherwise the entire section’s content will be announced as one long string of text, which would result in a bad user experience. When content updates happen in large sections of content, there is often a better way to communicate these updates to screen reader users. For example, say you’re building a filtering component for an e-commerce website or any website that offers the ability to filter content within a main section of the site. In most web applications, the content in the main section will filter dynamically as soon as the user selects one of the available filters. But **just because the content within the section gets updated does not mean that the section should be a live region.** So, how _do_ you let the user know that the content in the section is updating? **Providing simple instructional cues that set the user’s expectation of what will happen when they interact with an element is sometimes more than sufficient to let the user know of content updates even before they happen.** For the filtering component, what this means is that you can include an instructional cue at the top of the group of filters to let the user know that changing the filters will change the content in the main area. This way you’re setting the user’s expectation of what will happen when they select a filter, so you no longer need to make any announcements when the content updates. The user knows that they can just navigate to the main area and start exploring the filtered content. The WCAG Quick Reference website provides a live implementation of this approach. Preceding the content filters in the left sidebar of the Quick Reference, there is a note that lets the user know that “Changing filters will change the listed Success Criteria and Techniques”. No live region is necessary when this persistent cue is shown to all users, including screen reader users. Another very common UI component that can benefit from a similar implementation approach is a dynamic search component — particularly one where you have a search field that filters and displays results as you type into the field, like the search component you can find on the Smashing Magazine website. Implementing a dynamic search component like this one was one of the most popular requests that I got when I asked many of you what you'd like me to discuss in this course. So, here goes! What many developers will do when they build a similar component (and, admittedly, it’s a mistake I also made early in my career) is they will designate the search results container as a live region (like Smashing Magazine currently does) just because the content in the container is dynamically updated while the user is typing in the field. This results in a very noisy user experience. Here’s how VoiceOver on macOS starts announcing the contents of the results container after I type a search keyword in it: Sorry, your browser doesn't support embedded videos. A simpler, more robust, and much more user-friendly approach to implementing this pattern is to provide an instructional cue (i.e. an accessible description for the input field) that tells the user what will happen when they start typing in the field. The description might say “Results will filter / display / etc. ] as you type”. For example, the search component on [the a11ysupport.io website provides a live implementation of this approach. The accessible description of the search field lets you know that “Features will be filtered as you type”. The cue must be associated with the search field using `aria-describedby` so that screen readers announce it to their users. Providing instructional cues is helpful for all users. But if you don’t want the description to be visible, you can visually hide it using the `visually-hidden` utility class. Just make sure it is properly associated with the input field using `aria-describedby`. By letting the screen reader user know that results will be shown as they type, you no longer need to announce when results are shown, and the user knows that they can navigate to the search results once they’re done typing their keyword in the field. That being said, **you do still want to use an assertive live region to inform the user when _no_ results are found.** "While we don’t want to constantly interrupt people while typing, we need to interrupt when things go afoul", says accessibility engineer Scott O’Hara in his article Considering dynamic search results and content. "Specifically, let someone know immediately when they have entered a query that returns no results. Delaying such an announcement would result in wasted time, and potential uncertainty about “when” someone’s query stopped working. […] People who can see the UI are likely going to notice right away when the dynamic content dries up. They can then immediately correct for this by adjusting their query. This same affordance must be provided to people with disabilities." In his article, Scott elaborates more on all the usability considerations you should keep in mind when you’re implementing a dynamic search component. He then proposes a solution for how you might go about implementing it in a more robust and inclusive manner. I highly recommend checking the article out and following his implementation pattern if you can. In addition to using an accessible description for the search field, and a live region to announce when no results are found, Scott also suggests moving the user’s keyboard focus to the heading that introduces the results when the `Enter` key is pressed. Of course, if your search component doesn’t provide a heading, you wouldn’t need to do that. But it is a nice addition that improves the user experience for keyboard users, and screen readers will announce the number of results found when focus is moved to the heading. Here is how Scott’s demo works with NVDA on Firefox: Sorry, your browser doesn't support embedded videos. In this video, NVDA announces the search field followed by the accessible description when my focus moves to the field. The accessible description indicates that results will be shown as I type. I first type a keyword "one" into the search field. Then I press Enter. Pressing Enter moves keyboard focus to the heading which introduces the results and communicates the number of results found. Then I press the Escape key. Pressing the Escape key moves my focus back to the search field. When I type a keyword that's more than five characters long, the dummy example says that No results are found. Since Scott is using a live region to communicate when no results are found, NVDA announces "No results found!". Here’s an embed of Scott’s demo: See the Pen quick demo of showing / informing about dynamic results by Scott (@scottohara) on CodePen. ## Live regions are not suitable for notifications with interactive elements Live regions should not be used for messages or notifications that contain interactive elements, particularly if the user may need to act on those notifications. As we mentioned earlier, when a screen reader announces the contents of a live region, **it will announce the raw text content within the region without any of the structure or semantics. This means that the semantics of any interactive elements will not be conveyed.** Furthermore, when an update happens in a live region, screen readers will only announce the contents of a live region, but **the user’s focus does not move to the region.** And there is no mechanism available to allow the user to easily navigate to a live region to interact with any content that might be in it. So unless you provide a clear path for screen reader and keyboard users to get to the notification that contains interactive elements (like a well-documented keyboard shortcut, for example), then, depending on the position of the live region in the DOM, it can be difficult—if not impossible—for the user to get to the interactive content in the notification, especially if the notification dismisses itself after a short timeout. This is why toast messages that contain interactive elements are problematic. Unfortunately, toast messages that contain interactive elements are pretty common. You can see examples of them documented in Google’s Material Design system. But these messages come with usability and accessibility problems for screen reader users, as well as other users of assistive technologies like users browsing the web using a magnifier, and keyboard users as well. Adrian Roselli has documented and listed the most relevant WCAG failures that toast messages will typically be in violation of, particularly if they contain interactive elements. I highly recommend pausing here and taking a couple of minutes to read Adrian’s article, especially if you’re considering using toasts in your applications. If a notification contains an interactive element, you need to ensure that the user can easily navigate to it. And the best way to do that is to move the user’s focus to it. The `alert` and `status` live region roles are meant to represent short messages that do not require moving the user’s focus to (i.e. that do not contain interactive children). > Authors SHOULD ensure an element with role `status` does not receive focus as a result of change in status. > > […] > > Neither authors nor user agents are required to set or manage focus to an alert in order for it to be processed. Since alerts are not required to receive focus, authors SHOULD NOT require users to close an alert. If an author desires focus to move to a message when it is conveyed, the author SHOULD use `alertdialog` instead of alert. > > — The ARIA Specification If a notification contains an interactive element, it should not be a live region. And it should also not be a toast. You should move the user’s focus to it instead, and make it persistent. For interactive alert notifications, instead of using a toast message, consider using an alert dialog. The ARIA `alertdialog` role is used to represent a type of dialog that contains an alert message. As the name implies, `alertdialog` is a mashup of the `dialog` and `alert` roles. This means that it also expects similar keyboard interactions as modal dialogs do. Implementing an alert dialog is outside the scope of this chapter, but you can find the semantic and keyboard interaction requirements for implementing accessible alert dialogs documented on the APG website. Using `alertdialog` to alert the user of the presence of errors is an advisory technique to meet SC 4.1.3 Status Messages. For status type notifications that contain interactive elements, you may use a modal or non-modal dialog instead, and manage focus within these dialogs when they appear, as documented on the APG modal dialog example page. Keep in mind that **moving focus should be done as an immediate response to the user’s action**. If something happens async or after a delay like if a toast appears that the user hasn’t called up, then you shouldn’t move their focus to it, otherwise it would be disruptive to the user experience. **Decide if you should move focus or not based on what users are expecting.** ## Live regions are not a substitute for ARIA state properties Don’t use live regions to convey state changes when there’s an ARIA attribute to do that. For example, if a button toggles the visibility of some content on a page (like a ‘dropdown’), use the `aria-expanded` attribute to communicate the state of the content (whether it’s expanded or collapsed) to the user. You don’t need a live region to announce that the content has been expanded or collapsed. <!-- The aria-expanded attribute communicates the state of the disclosure widget to the user. No live region is needed to do that. --> <button aria-expanded="[ true | false ]">Terms of use</button> Similarly, if you’re building a dark theme switcher using a toggle button, for example, use the `aria-pressed` attribute to communicate to screen readers that the dark theme is currently ‘On’ or ‘Off’. <!-- The aria-pressed attribute communicates whether or not the [Dark Theme] is On or Off. No live region is needed to announce when the dark theme is applied. --> <button aria-pressed="[ true | false ]">Dark theme</button> When `aria-pressed` is declared on a `<button>`, the button’s ARIA role mapping will change in most accessibility APIs, and it will be exposed as a `toggle button` (not just a regular button), indicating that this `<button>` toggles a certain functionality On and Off. When the value of `aria-pressed` is true, it communicates to screen readers that the functionality is currently ‘On’. Combined with the button’s accessible name, the state attribute lets the user know what will happen when they activate the button. You don’t need a live region to announce that the dark theme has been applied to the page. Here’s a live demo of a simple theme switcher using two toggle buttons that you can try using a screen reader: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. When a toggle button is activated, the screen reader announces the state of the button (whether it’s pressed or not) when it announces the button’s role and accessible name. The state of the button is sufficient to communicate when a theme is ‘On’ or ‘Off’. You can try it for yourself in the debug version of this theme switcher. State property changes may not be announced to the user the same way live regions are, but not every change _needs_ to be announced. Using appropriate semantics, providing meaningful accessible names, and using the appropriate state attributes is sometimes sufficient for screen reader users to understand what will happen when they interact with an element. So before considering using a live region, ask yourself if there is a state attribute that does what you need. And if there is, use that attribute. ## Best practices for implementing (more robust) status messages with live regions Live regions are most suited for implementing short, non-interactive status messages that do not cause a change of context (like moving focus) and that cannot be communicated to screen reader users in another way. Live regions are currently the primary way to conform with Success Criterion **4.1.3 Status Messages (Level AA)**. If you’re implementing status messages in your web application(s), there are some best practices that most accessibility professionals agree on that can help you achieve maximum compatibility across browser and screen reader pairings: ### Make sure the live region container is in the DOM as early as possible The element that is designated as a live region **must exist on the page when the browser parses the contents and creates the accessibility tree of the page.** This ensures that the element will be monitored for changes when they happen and that these changes are communicated to the screen reader and the user. So when you create a live region, insert it into the DOM as soon as possible (ideally, when the page loads), **before you push any updates to it.** If you insert a live region into the DOM or convert a container into a live region _when you need it_ , there’s a high chance that it won’t work. ### Choose an appropriate hiding technique if the live region isn’t visible If the status message is not visible to all users, hide it visually using the `visually-hidden` utility class. Don’t hide the live region using using `display: none;` or `aria-hidden="true"`, or any other hiding technique that removes it from the accessibility tree. **Hidden live regions are not announced.** ### Limit the number of live regions on the page While there is no rule as to how many live regions you can have on a page, you should limit the number of live regions you create. As accessibility engineer Scott O’Hara says in his article “Are we live?”: > please do keep in mind that something just as bad as live regions being injected into a web page and then making no announcements, is a web page with a bunch of live regions that all start barking at assistive technology users at the same time. A good practice is to have only two live regions on the page: **one assertive region and one polite region** that get inserted to the page on page load. Then you insert updates into these two regions and manage the message queue in them via JavaScript. If you have multiple live regions on a page, they may interfere with each other, and some messages might not be announced at all. According to the specification: > Items which are assertive will be presented immediately, followed by polite items. User agents or assistive technologies MAY choose to clear queued changes when an assertive change occurs. (e.g., changes in an assertive region may remove all currently queued changes) What this means is that **the politeness level** indicated by the `aria-live` attribute **works as an ordering mechanism for updates**. In instances when you have multiple live regions on a page, `polite` updates take a lower priority; and `assertive` updates take a higher priority and could even potentially clear or cancel other updates that are queued for announcement. This causes some messages to get lost, or just partially announced. Carefully choreographing the sequence of events in a couple of live regions on the page will be your best approach to achieve maximum compatibility. This is one of the reasons why, in the previous chapter, we said that providing a summary of errors at the top of a form is a far more robust approach than using inline validation. ### Compose and insert your message into the live region in one go You should **pre-compose the notification message’s content and insert it into the region in one go.** Don’t make multiple DOM insertions to create one message, otherwise the screen reader may make multiple _separate_ message announcements, which is not what you want. ### Keep the content short and succinct and avoid rich content **Keep the message content concise.** And keep it as short as possible. There is no character limit on the notification message, but remember that live region announcements are transient and can’t be re-played. So make sure the message is easy to understand when it is announced the first time. Keeping it short also ensures it is less disruptive to the user flow. **Avoid rich content, interactive elements, and non-text elements** like images as these are also not conveyed to screen reader users. ### Empty the live region and wait a bit in between updates If the status message is not visible to everyone or it is removed after a short timeout, set a timeout (e.g. 350ms–500ms) to remove the notification text from the live region. You are not required to empty a live region after its contents have been announced because announcements are triggered on content additions by default, but emptying the live region between updates ensures that you don’t end up with weird or duplicate announcements. /* set a timeout to empty the live region */ setTimeout(() => { //empty live region }, 350); Emptying the live region when it’s no longer visible also ensures that screen reader users will not be able to navigate to it when they are not intended to. So make sure the live regions are cleared up in between updates, and wait a little bit before inserting new updates to them. And when you do insert a new update, insert the new message in one go. ## Debugging Live Regions If you use live regions, you’re going to want to debug them when they don’t work as expected, like when the screen reader announces something unexpectedly. Part of debugging live regions is seeing what goes inside in them and when. To debug live regions on a page, you can use the NerdeRegion browser extension. NerdeRegion is a developer tools extension for debugging live regions on a Web Page. When activated, it lists all active ARIA live regions, and keeps a record of all mutations that has happened on the region. You can use NerdeRegion to: * Check if a live region is being updated properly, * Check if accessible name computation (beta) is done correctly, * Check if live region is being re-used correctly. To use the extension, open your browser’s Developer Tools, and navigate to the NerdeRegion tab. There, you can keep track of timestamped announcements and the source element they originate from. Sorry, your browser doesn't support embedded videos. In this video, I have an assertive live region on the page. When I go to the NerdeRegion panel in the Edge DevTools, it lists the number of live regions on the page in the left sidebar, as well as the type of the live region (assertive or polite) in the main area of the panel. Then when I activate the button that populates the live region with a new message, NerdeRegion shows when the live region has changed, along with a time stamp of when it did. Since there can be bugs and inconsistencies with how ARIA live regions are announced with different screen readers, you should constantly be reviewing your live regions to ensure they are continuing to work as necessary. As we mentioned earlier, you should try to limit the number of live regions you use ideally to two or less. But if you absolutely have to use more than that, NerdeRegion can help you figure out if an issue is potentially caused by your code or by the device combination. ## Avoid live regions if you can I know this isn’t the advice you’d expect at the end of a whole chapter about live regions. But hear me out. Live regions are inconsistent and unpreditcable. It’s easy for their implementations to go wrong. There’s a lot of manual work involved to get them working properly. Furthermore, the design of live regions is intended to give maximum flexibility to screen readers to implement an experience that is best for their users. What this means is that ARIA live region properties are only **strong suggestions** as to how you want live region announcements to be made, but the value of these properties (and by extension: the behavior of live regions) **may be overridden by browsers, assistive technologies, or by the user.** This along with current bugs and implementation gaps means that you can’t guarantee that a live region will always work the way you designed it to. This is one of the reasons why you should try to rely on live regions as little as possible, and use alternative and more robust approaches whenever you can. Remember that live region announcements are transient. If the user misses an announcement, they miss it. Depending on the importance and urgency of the announcement, this can easily degrade the usability of your web application and result in a frustrating user experience. So if you can make your users aware of updates using other more persistent methods like moving focus or providing instructional cues, then you should consider doing so. **Not everything that updates in the background needs to be a live region.** For example, chat interfaces are typically a great candidate for live regions and would be implemented using the `log` ARIA role, but they don’t always _need_ to be implemented as live regions. Unless the chat is the main interface on the page, then it probably _shouldn’t_ even be a live region. For your day-to-day work, you’ll need live regions less often than you think, even if you’re building dynamic web applications like SPAs. For example, let’s say you’re building the navigation for a SPA. In most SPA navigations, activating a link will load a new page without causing a page refresh. Normally when the user activates a link and the link takes them to a new page, screen readers will announce the title of the new page first, which lets the user know where they have landed. But this doesn’t happen in SPAs. So what many developers will do is they will use live regions to announce when new content has been loaded. But this is not only unnecessary, but you can even let the user know that new content has loaded in a more efficient way. Instead of relying on a live region to announce the page change, you could send keyboard focus to the main `<h1>` of the page which, as we mentioned in the heading structure chapter, should describe the primary topic of the contents of the page and ideally be identical to the page’s `<title>`. By moving the user’s focus to the heading, the screen reader announces the heading’s content to the user, which gives them the same context that the page’s `<title>` would have given them if it had been announced after a page refresh. (But don’t forget to change the page’s `<title>` when a new page is loaded, too.) Moving focus to the primary heading of the page is also helpful for keyboard users. Usually when the page refreshes and the user starts tabbing through the page, a skip link should be the first element they focus on, and they can use that link to skip directly to the main content of the page. But if the page doesn’t refresh, they may have to tab their way through many elements before they reach the new content. So moving their focus to the main heading makes their navigating through the page more efficient. Live regions have their (limited) use cases — particularly for status messages as described in WCAG. But as accessibility engineer Scott O’Hara says: > if you can create an interface that can limit the number of live regions necessary - none being the ideal - then that’d be for the better. Your main purpose as a designer or developer is to make users aware of new content updates when they happen. But for most UI patterns, there are other ways you can achieve that, and those ways are often more robust and more reliable than live regions, and result in an overall better experience for your users. Another example that _could_ use live regions but that can also be implemented in a more robust manner is a shopping cart on an e-commerce website. A common pattern on many websites today is to show an overlay of the full cart when a new item is added to the cart. The modal cart overlay pattern used on the A Book Apart website. Instead of using live regions to announce that a new item has been added to cart, you can use this pattern and move the user’s keyboard focus to the cart when it is shown. This approach has a couple of benefits, one of them is that it makes it easier for keyboard users to get to the cart, see and/or edit what’s in it, and continue to checkout if this is what they want to do. Keep in mind that you must treat the cart as a modal dialog in this case and manage focus accordingly, particularly when the contents of the page are dimmed after the cart is shown. You can find the requirements for keyboard focus management for modal dialogs in the Modal Dialog page on the APG website. As a general rule of thumb: if you can achieve the same result without using live regions, then probably do so. The less ARIA you use, the better. Remember: No ARIA is better than bad ARIA. If you do use live regions in your web applications, make sure you **thoroughly test** across all browsers and screen reader pairings. And don’t forget to test with Braille displays, too. And perform usability tests with screen reader users. Not only will usability testing give you insights into what your users are expecting from the application and what they aren’t, but it will also help you understand the different ways screen reader users are using your application and how that will affect the announcements you’re trying to make with live regions. ## References, resources and recommended reading * The Many Lives of a Notification * Designing for Screen Reader Compatibility * output: HTML’s native live region element * Are we live? * We’re ARIA Live * Live Region Playground * (Test case demo) aria-atomic and aria-relevant on aria-live regions * Accessibility (ARIA) Notification API * More accessible skeletons * Defining ‘Toast’ Messages * A toast to an accessible toast… Many thanks to **James Edwards** (@siblingpastry) for reviewing this chapter.
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
CSS-only scrollspy effect using scroll-marker-group and :target-current
✨ This post is sponsored by everyone who has bought my Practical Accessibility course. ✨ The _Bootstrap Scrollspy_—now commonly known as just “Scrollspy”—is a feature that automatically updates navigation links based on the user’s scroll position to indicate which link is currently active in the viewport. It is popular because it aims to enhance the user experience by providing visual cues about which part of the content is currently being viewed. Sorry, your browser doesn't support embedded videos. The Scrollspy effect demonstrated in the Bootstrap documentation shows the navbar links are highlighted when their respective target sections are scrolled into view. By default, in-page navigation links (`<a href="">`) don’t get highlighted when their targets are scrolled into view. Historically, the Scrollspy effect has required us to use JavaScript to ‘spy’ on sections of content in a page (such as in an article) and then programmatically update the navigation links and indicate which one is “active”. That typically involved adding a CSS class name (e.g. `.active`) or an HTML attribute (e.g. `data-active`) to style the active link. Today, a new CSS property and pseudo-selector are available that are meant to enable us to create the Scrollspy effect with just two lines of CSS and no JavaScript. ## Scrollspy with CSS scroll markers If you’ve read my previous article discussing the accessibility of CSS-only carousels, then you’re already familiar with the CSS Overflow Module Level 5, and the concept of “scroll markers”. You will also be familiar with the fact that there are CSS-generated scroll markers (`::scroll-marker`), as well as HTML (and SVG) scroll markers (`<a href="">`). If you haven't read the CSS Carousels article yet, I highly recommend you pause here and go give it a read. There's information in there that provides context for some of the technical concepts discussed in this post. In the carousels article, I examined a CSS-only Scrollspy example from the ‘CSS Carousels’ gallery. The Scrollspy example in the Carousels gallery is created using CSS-generated scroll markers. By default, CSS-generated scroll markers are semantically exposed as tabs, not links, which introduced a bunch of usability issues with that pattern that I outlined in the previous post. As I noted in that post: > Instead of using `::scroll-marker`s to implement [the Scrollspy] example, I would instead expect to be able to create a semantic table of contents using an HTML list of `<a href="">`, and then use the `:target-current` pseudo-class to apply active styles to a link (the native scroll marker!) when its target is scrolled into view. […] However, that doesn’t seem to work at the moment. > > Unfortunately, even though the specification states that it "defines the ability to associate scroll markers with elements in a scroller", **the current implementation of the`:target-current` pseudo-class seems to work only for CSS-generated `::scroll-markers`, but not for native HTML ones.** > > Personally, I think `:target-current` is one of the most useful additions to the specification. It’s unfortunate that its current implementation is limited to the new pseudo-elements. Since I published that post, a new property has been proposed and added to the specification. This property is named `scroll-target-group` and it “ _enriches HTML anchor elements functionality to match the pseudo elements one_ ”, which makes it possible to use the `:target-current` selector to highlight links when their respective targets are in view. 🙌🏻 This post is about the `scroll-target-group` property and how to use it with the `:target-current` pseudo-selector to create the Scrollspy effect with CSS. ## Enriching HTML anchors to become scroll markers Using the `scroll-target-group` property (we’ll demonstrate how shortly), you can “promote” HTML anchors to become ‘scroll markers’. When **a group of HTML anchor elements** becomes scroll markers, the browser will run a specific algorithm to determine which anchor in the group is the active anchor, just like it determines the active scroll marker in a group of CSS `::scroll-marker`s. The active scroll marker is determined when its target element is scrolled to an ‘eventual scroll position’ chosen by the browser. According to the specification, the browser chooses an ‘eventual scroll position’ to which the target of a marker (an `<a href="">`) will reach. This ensures that the relevant marker is activated immediately. The active scroll marker then matches the `:target-current` pseudo-class, which you can use to visually highlight the active anchor. **All this requires no JavaScript on our part** , which is pretty impressive. Now, in order to use the `scroll-target-group` property, you will want to use it **not to the anchors themselves, but to an element containing the anchors.** Let’s demonstrate how. ### Using scroll-target-group and :target-current In the CSS Carousels article we talked about how the `scroll-marker-group` property is used to _generate_ a grouping container for a group of `::scroll-marker`s. When you use the `scroll-marker-group` property, both the scroll marker group container and the scroll markers themselves are generated by the browser as CSS pseudo-elements. On the other hand, the `scroll-target-group` property is meant to be a used on _an HTML element_ which _contains_ the HTML scroll markers (the links / anchors). For example, say you have a Table of Contents (TOC) on an article page and you want to style the active link within the TOC when its target section is scrolled into view. To use this property, you’ll want to start by setting up the semantic structure of the links: <nav aria-labelledby="toc-label"> <span id="toc-label" hidden>Table of Contents</span> <ol role="list"> <li><a href="#one">Section One</a></li> <li><a href="#two">Section Two</a></li> <li><a href="#three">Section Three</a></li> <li><a href="#four">Section Four</a></li> <li><a href="#five">Section Five</a></li> </ol> </nav> We have a named navigation landmark that contains an ordered list of anchors pointing to sections of content on the page. These links are, by default, keyboard-operable and they come with default link behavior and accessibility built in. To make these links behave like scroll markers, you will then use the `scroll-target-group` property on their container—this can be the `<ol>` or the `<nav>` element. nav[aria-labelledby=toc-label] { scroll-target-group: auto; } The `scroll-target-group` property specifies whether the element it is used on is **a scroll marker group container.** It accepts one of two values: `none` and `auto`. When the value of `scroll-target-group` is `auto`, "the element establishes a scroll marker group container forming a scroll marker group containing all of the scroll marker elements for which this is the nearest ancestor scroll marker group container.". Now as the user scrolls through the sections of content, the browser will determine which link is currently active. The active link will automatically match the `:target-current` selector, which you can use to highlight the link by giving it distinctive styles within the group: a:target-current { font-weight: bold; text-decoration-thickness: 2px; } ### Live demo Here’s a live example of the CSS Scrollspy effect in action: At the time of writing, you need to use Chrome 140+ to see the HTML scroll markers in action. And here is a Codepen for you to tweak at. Here’s a video recording of the example in action: Sorry, your browser doesn't support embedded videos. #### A note on accessible active anchor styling WCAG Success Criterion 1.4.11 Non-text Contrast Level AA requires that **visual information required to identify user interface components and states** (except for inactive components or where the appearance of the component is determined by the user agent and not modified by the author) **has a contrast ratio of at least 3:1 against adjacent color(s)**. If you’re using color alone to visually indicate the active link, make sure the color contrast is high enough to make the color change discernible by people with vision disabilities. I recommend that you don’t rely on changing the text or background color of the link alone to indicate that it is active, as these colors will be overridden in forced colors modes like Windows Contrast Themes. So the active link styles may no longer be conveyed to the user, _unless_ you use the `forced-colors` feature query and system color keywords to adapt the colors of the active link to the user’s chosen color scheme. This is outside the scope of this article. I personally prefer adding a border, an outline, or an underline, or increasing the thickness of the text (or a combination of any of these) to highlight the active link. So, using just a couple of lines of CSS, you can now create a scrollspy effect without needing a single line of JavaScript. One more thing! Usually when you load a page that has a fragment identifier in the URL, the browser scrolls the page to the ‘target element of the document’ and you can use the `:target` pseudo-class to apply custom highlight styles to that element. Until today, there hasn’t been a way to style/highlight the (in-page) _link_ that points to that target element. Today, if a link is a scroll marker, the `:target-current` styles will be automatically applied to the link when the browser scrolls to the document’s target element. This means that you can now combine `:target` and `:target-current` to style the target element identified in a URL fragment identifier _and_ the link to that element. ## The semantic accessibility of HTML scroll markers HTML anchor elements (`<a href="">`) come with default link accessibility and behavior built into them: they are exposed as `link`s to screen readers, and they come with keyboard interactions built into them by the browser. But an `<a href="">` element does not come with a built-in way to communicate that it is “active”, or that it is “the current” link within a group of links. A scroll marker, by definition, has a meaningful purpose: it lets the user know which part of content is currently being viewed. What this means is that when you visually highlight an active link, you’re communicating meaningful information to the user. To ensure that you are not excluding any of your users, you want to make sure that this information is communicated to _all_ your users, including screen reader users. **This is a baseline accessibility requirement.** WCAG Success Criterion 1.3.1 Info and Relationships (Level A) states that "information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text." So, how do you communicate that a link is active to screen reader users? How do you provide the same meaningful affordance that you’re creating with CSS to someone who can’t see? Since there is no native HTML way to indicate that a link within a group is currently “active”, you can use ARIA to communicate this information. Affordance is “the quality or property of an object that defines its possible uses or makes clear how it can or should be used”. As I mentioned in the CSS Carousels accessibility post, ARIA is similar to CSS in that it creates user interface affordances to the assistive technologies that rely on it. Using CSS, we provide visual affordances to our interfaces (using color, layout, spacing, and more). ARIA provides what “ _semantic_ affordances” to screen reader users. ARIA attributes “paint” a (non-visual) “picture” of the page to screen reader users. To indicate which anchor is currently active, ARIA provides a conveniently-named attribute: `aria-current`. > [The `aria-current` state attribute] indicates the element that represents the current item within a container or set of related elements. … The `aria-current` attribute is used when an element within a set of related elements is visually styled to indicate it is the current item in the set. Setting `aria-current` to `true` on the “active” anchor ensures that screen reader users get the same information that sighted users get about which part of the content is currently shown. <nav aria-labelledby="toc-label"> <span id="toc-label" hidden>Table of Contents</span> <ol role="list"> <li><a href="#one">Section One</a></li> <li><a href="#two" aria-current="true">Section Two</a></li> <li><a href="#three">Section Three</a></li> <li><a href="#four">Section Four</a></li> <li><a href="#five">Section Five</a></li> </ol> </nav> You may already be familiar with this attribute as you may already using it to indicate the active link within your website navigation. `aria-current` accepts one of seven values. For website navigation, the `page` value is appropriate as it indicates **the current _page_** on the website. For nested or in-page navigation links, the `true` value is sufficient to indicate the current link within the group. When the user scrolls through the sections of content and the active link is visually highlighted, this link must have `aria-current=true` set on it. Because the purpose of the `scroll-target-group` property and the `:target-current` selector is to allow us to create JavaScript-_free_ native HTML scroll markers, we should expect the browser to add and manage the necessary ARIA attribute(s) required for scroll markers to be inclusive. (After all, that’s the whole premise of this feature: to write a few lines of CSS and let the browser handle all the behavior for us.) However, at the time of writing of this post, Chrome (currently the only browser that has implemented this feature) doesn’t add `aria-current=true` to the active anchor yet. If you inspect the accessibility information of the links in the demo from the previous section you can see that the state of the active anchor is not communicated to assistive technologies when the anchor becomes active (see screenshot below). This is unfortunately akin to some of the accessibility issues with CSS Carousels that I discussed in the previous post. I filed a Chromium issue and I’m hoping this will be resolved soon enough to make this feature usable. I will update this post when the issue is resolved. **Update (2025-08-19)** : In response to the issue I filed a couple of days ago, there is now an active, work-in-progress patch to set `aria-current` for :target-current html anchor element. If you want to use this feature as a progessive CSS enhancement today, keep in mind that you _will_ , for the time being, need to use JavaScript to add `aria-current` to the active anchor when its corresponding target scrolls into view, **otherwise you risk an instant WCAG 1.3.1 violation.** I’ll personally wait till the issue is resolved and the feature becomes ready for production. When it does, I’ll be among the first to add it as an enhancement in my CSS. ✌🏻
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
Tag, You're It
## Why did you start blogging in the first place? I started blogging when I was still learning front-end development—specifically CSS—back in 2012. I was learning a lot and writing what I was learning as a way to organize my thoughts and solidify my learnings, and at some point **I realized that my notes can help others learn and understand the same.** So, I created my blog a few months later **in order to share my knowledge with the community**. I love writing and I love teaching, and **I found blogging to be one way I can bring together two things I love** , and to give a little back to the web community. ## What platform are you using to manage your blog and why did you choose it? Have you blogged on other platforms before? I use Eleventy. Before Eleventy I used Jekyll. I don’t choose my tools based on what’s most popular at any given moment. I choose the tool or platform that works for me and stick with it until I find myself _in need_ of something else. I moved from Jekyll to Eleventy when I needed more flexibility with how I wanted to structure my website’s source code and with how I style the different pages on the website. Eleventy still fits my needs perfectly until this day. ## How do you write your posts? For example, in a local editing tool, or in a panel/dashboard that’s part of your blog? I collect ideas, plan blog posts, and start drafting them in Obsidian. When a draft becomes close enough to publishing, I move it into my code editor where I will refine and polish it before I finally hit Deploy. I write in Markdown (`.md`) files in both Obsidian and my code editor, so moving from one tool to the other is quite effortless. ## When do you feel most inspired to write? **When I want to turn the chaos in my head into order.** This typically happens when I’m learning a new topic or idea, or when I’m teaching and helping others understand new topics or ideas. I’m most inspired by helping others understand what is otherwise a complex or confusing topic, and seeing other people’s eyes light up when they finally get that “ah-ha” moment. ✨ I mostly get to see this when I speak or run workshops in person, but the same also happens through writing as well. ## Do you publish immediately after writing, or do you let it simmer a bit as a draft? I publish immediately. Once a post is out there, it’s out there; and then I can move on and start working on the next thing. ## What are you generally interested in writing about? HTML, CSS, SVG, web accessibility, progressive enhancement, and how to use Web platform features to create **inclusive Web user interfaces**. ## What’s your favorite post on your blog? Understanding SVG Coordinate Systems and Transformations (Part 1) — The viewport, viewBox, and preserveAspectRatio, _hands down_! This post was the result of two weeks of deep-diving into the gnarly SVG specification, which resulted in me creating the interactive demo by which I got my first SVG epiphany! 💡 SVG became my niche for years after that. This post is also the reason hundreds (probably thousands!) of developers finally grapsed SVG coordinate systems and got their own SVG lightbulb moment. 💡 This one will always hold a special place in my heart. ## Who are you writing for? I started blogging for myself and for my future self. Now, I also write more for the community. I _start_ the writing process for myself; but I publish what I write for the community. ## Any future plans for your blog? Maybe a redesign, a move to another platform, or adding a new feature? I’m taking a comprehensive design course at the moment (ShiftNudge, in case you’re wondering) and am planning on redesigning my website as I go through the course. I will also be refactoring the code from the ground up this year. I’ll add new features during all of this, and will announce them in due time. ## Next? The format dictates I nominate other people to write a post like this. However, instead of nominating someone who may or may not be interested in joining the chain letter and who may instead feel pressured into doing so, I’m going to nominate _you_ , dear reader, if you have a blog are keen to join us and share some about it. If you _don’t_ have a blog, I hope this series of posts by people like you will inspire you to start writing, and sharing what you write with the world. Just write, and great things may follow.
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
CSS to speech: alternative text for CSS-generated content
✨ This post is sponsored by everyone who has bought my Practical Accessibility course. ✨ The CSS `::before` and `::after` pseudo-elements are used to insert presentational content before and after (respectively) existing content in an HTML element. The `content` property is used to define _what_ content is inserted in these elements. For example, the following CSS adds the text "Error: " before the content of an error message container: <div class="error-message">The value you entered is invalid.</div> .error-message::before { content: "Error: "; } This example adds an SVG chevron icon to a button that toggles the display of some content: <button class="disclosure-widget">License Agreement</button> button.disclosure-widget::before { content: url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m8.586 5.586c-.781.781-.781 2.047 0 2.828l3.585 3.586-3.585 3.586c-.781.781-.781 2.047 0 2.828.39.391.902.586 1.414.586s1.024-.195 1.414-.586l6.415-6.414-6.415-6.414c-.78-.781-2.048-.781-2.828 0z'/%3E%3C/svg%3E"); width: 1em; height: 1em; } This example adds the ↗ unicode character to indicate that the anchor links to an external resource or opens in a new tab or window: <a href="..." target="_blank" class="external">CSS Generated Content specification</a> a.external::after { content: " \2197" ; /* ↗ = North East Arrow */ } CSS-generated content is not exposed in the accessibility tree of the page like HTML content is. But **CSS content takes part in the accessible name computation of the element they are used on.** ## CSS pseudo-content in the accessible name computation algorithm When the browser needs to determine the accessible name of an element, it uses an algorithm called “Accessible Name and Description Computation” algorithm. According to the specification: when an element’s name is derived from its contents, the browser must include the textual contents of CSS pseudo-elements in the accessible name. This includes the contents of the `::before` and `::after` pseudo-elements, as well as the `::marker` when an element supports `::marker`. > Check for CSS generated textual content associated with the current node and include it in the accumulated text. The CSS `:before` and `:after` pseudo elements can provide textual content for elements that have a content model. > > * For `:before` pseudo elements, User agents MUST prepend CSS textual content, without a space, to the textual content of the current node. > * For `:after` pseudo elements, User agents MUST append CSS textual content, without a space, to the textual content of the current node. > For example, if we look at the link from the example in the previous section: <a href="..." target="_blank" class="external">CSS Generated Content specification</a> a.external::after { content: " \2197" ; /* ↗ = North East Arrow */ } …the accessible name for the link is going to be “CSS Generated Content specification ↗”. If you open the browser DevTools and check the accessibility information of the link, you will see the unicode character included in the accessible name. The name of the link will be read by a screenreader as “CSS Generated Content specification North East Arrow” or “CSS Generated Content specification Upright Arrow”. The screen reader announces the unicode character by its default alternative text, which does not commnunicate the _intended_ meaning of the character. **Where do screen readers get the alternative text for unicode characters from?** Steve Faulkner shares that "the text alternatives for Unicode symbols are usually contained within a text file in screen reading software’s program files directory." Emojis, like other unicode characters, also have default text alternatives that may differ slightly across platforms, as Steve demonstrates in his article. The text alternatives of emojis usually describe what the emoji is, not necessarily what you might be using it for. As Craig Abbott mentions in his recent writeup about integrating AI into screen readers, "the “red flag” emoji is actually announced as “triangular flag on post”, which does not usually provide enough context for the way that emoji is used in our culture". Not to mention that the same emoji may be interpreted differently by different sighted users! This is why using emojis to communicate meaningful information is generally not recommended. Images and characters inserted in CSS should be treated the same way you treat HTML images: **when an image (or character) is meaningful, it must be described to assistive technology (AT) users using alternative text that communicates its purpose.** This ensures that AT users are getting the same information as sighted users. Providing descriptive alternative text to meaningful images is a WCAG requirement. In HTML, you provide the alternative text of an image in the `<img>`'s `alt` attribute: <img src="/path/to/meaningful/image.jpg" alt="[ the text describing the purpose of the image to someone who can't see the image ]"> On the other hand, when the image is purely decorative, it should be hidden from screen readers and excluded from an element’s accessible name to avoid unnecessary or confusing announcements. In HTML, an image is marked as decorative by giving it an empty `alt` text. The empty `alt` value ensures that the image is not exposed and announced by screen readers: <!-- When the image is decorative, leave the alt text empty. --> <img src="/path/to/decorative/image.png" alt=""> But how do you provide alternative text to CSS-generated content to make sure it is announced (or not announced!) as expected? ## Alternative text for CSS generated content Historically, there hasn’t been a standard way to provide alternative text for CSS generated content and images. So, we resorted to creating placeholder `<span>`s in the markup for this purpose. For decorative CSS content, we inserted the content into a `<span>` and then used `aria-hidden="true"` to hide that `<span>` from screen reader users. <a href="..." target="_blank" class="external"> CSS Generated Content specification <span aria-hidden="true" class="icon"></span> </a> a.external .icon::before { content: " \2197"; } And to provide descriptive alternative text to otherwise meaningful graphics, we created an additional, visually-hidden `<span>` that included the alternative text of the graphic and that is exposed to screen reader users only: <a href="..." target="_blank" class="external"> CSS Generated Content specification <span aria-hidden="true" class="icon"></span> <span class="visually-hidden"> (Opens in a New Window)</span> </a> But there’s a better way to handle alternative text for CSS content today. We can now provide alternative text for CSS-generated content directly in CSS, after a slash following the content. According to the CSS Generated Content Module Level 3 specification: > Content intended for visual media sometimes needs alternative text for speech output or other non-visual mediums. The `content` property thus accepts alternative text to be specified after a slash (/) after the last `<content-list>`. If such alternative text is provided, it must be used for speech output instead. > > This allows, for example, purely decorative text to be elided in speech output (by providing the empty string as alternative text), and allows authors to provide more readable alternatives to images, icons, or text-encoded symbols. This means that for meaningful icons, it looks like this: a.external::after { content: " \2197" / "Opens in a New Window" ; } For decorative icons, it looks like this: .disclosure-widget::before { content: url("data:image/svg+xml,%3Csvg height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m8.586 5.586c-.781.781-.781 2.047 0 2.828l3.585 3.586-3.585 3.586c-.781.781-.781 2.047 0 2.828.39.391.902.586 1.414.586s1.024-.195 1.414-.586l6.415-6.414-6.415-6.414c-.78-.781-2.048-.781-2.828 0z'/%3E%3C/svg%3E") / ""; } In non-supporting browsers, the slash syntax will invalidate the entire declaration, which means that you would need to precede the new syntax with a fallback for non-supporting browsers. _That being said_ , support for alt text in the `content` property is very good today. So unless you need to support Internet Explorer or other older non-supporting browsers, you shouldn’t need to worry about the fallback. ## CSS alt text in browsers today I created a test page (embedded below) to test and demonstrate how browsers handle alt text for CSS generated content today, and how the content is exposed and announced by screen readers. The page contains two tables: The first table contains examples of different types of CSS generated content,some of that content is intended to be decorative and some is intended to be meaningful. For each type, I created three instances: one without alt text (alt text not included in CSS declaration), one with non-empty, descriptive alt text, and one with empty alt text (alt text is empty but not omitted). In the second table, I provided an HTML image for comparison. The HTML image table contains valid and invalid image references to demonstrate if and how the image’s alternative text is rendered in the browser. It also contains two instances of each of these images: one with non-empty alt text, and one with empty alt text. Here is a video demonstrating how VoiceOver on macOS announces each of the examples in the table. Sorry, your browser doesn't support embedded videos. Navigating the buttons and links with CSS generated content using VoiceOver on macOS paired with Safari v18.4. I got the same results with VoiceOver on iOS 26, and with VoiceOver on macOS when I paired it with both Chrome and Firefox on macOS. Some observations from the above test: * CSS images inserted using the `url()` function that don’t have an alt text provided are excluded from the browser’s accessibility tree by default and are, therefore, not announced by screen readers. These are images with _no_ alt text at all. * All images with an empty alt text value are also excluded from the accessibility tree. This is the expected behavior. * Images with non-empty alt text are announced by their alternative text. And the alt text of the image is announced as part of the accessible name of the element. * Images with broken references and non-empty alt text are also exposed and announced by their alternative text, and the alternative text of the image is announced as part of the accessible name of the element. This is the expected behavior. * Like images with valid references, images with broken references are not exposed when they don’t have any alt text provided. * Emojis and unicode characters are announced by their default alternative text, except when you override the alternative text in CSS. And finally, the alt text of a broken CSS image is not shown in place of the image when the image is not shown. The only exception on desktop is Safari on macOS which _does_ show the alt text of the image in a large square placeholder **but only when VoiceOver is turned On, or upon turning it Off**. In other words, Safari does not display the alt text of a broken CSS image by default. But if you enable VoiceOver—say by pressing `CMD + F5`, the broken image is replaced with a large, full-width square that contains the alternative text of the image. You can see that in action in the video recording above. Safari on iOS 26, on the other hand, did show the alt text in the large square placeholder in my tests whether VoiceOver was On or Off. I got very similar results with JAWS (2025) paired with Chrome (v140) and with NVDA (2025) with Firefox (v142) on Windows. The main difference from VoiceOver macOS that stood out is that NVDA announces “graphic” after the alt text of the CSS image inserted using the `url()` function. I haven't tested on Android because I don't have access to an Android device. If you do, please feel free to share your results in the comments! Support for alt text for CSS generated content has improved notably since I did the same tests last year ahead of my CSS Day talk. Last year, Chrome exposed CSS images (inserted using `url()`) even when they had empty alt text. I was pleasantly surprised to see that all browsers now behave the same way in terms of exposing alt text in CSS. ## Yes, but: Don’t use CSS to insert meaningful content and images While it’s good and certainly useful that we can now provide alt text for CSS generated content, **I do not recommend using CSS to insert _meaningful_ content into the page.** Here’s why… ### The CSS alt text is not shown when a CSS image is not rendered The alt text of a CSS image is not shown in place of the image when the image itself doesn’t load. If the image is meaningful and it is not shown for some readon, then its meaning will be lost on most users. And the missing alt text can make interactive elements unusable by speech control users. As I mentioned in the previous section, the only browser that currently shows the alt text of a broken CSS image is Safari on iOS and Safari on macOS with VoiceOver enabled. The non-empty alt text of a CSS image still contributes to the accessible name of the element it is used on even when it is not rendered visually. This creates a mismatch between the visible label of the element and its accessible name. When there’s a mismatch between the visible label and the accessible name of an element, particularly when the element is interactive, it becomes more tedious for speech control users who rely on visible labels to activate controls. Furthermore, depending on the position of the CSS image’s alt text in the accessible name, this mismatch may create an immediate violation of WCAG Success Criterion 2.5.3 Label in Name. The alt text of an HTML `<img>`, on the other hand, is _more likely_ to be shown in most browsers. ### CSS Generated content does not currently translate via automated tools CSS generated content does not currently translate via automated tools. As Adrian Roselli notes in his article on alternative text for CSS generated content, "in the future, `attr("data-alt")` can potentially get around that unless you rely on automated translation tools. If you need your content to auto-translate, then the CSS approach is not for you." ### CSS content is only accessible when your CSS is used CSS content is only accessible when your CSS is used. But **not all assistive technologies will render your stylesheets when displaying the page’s content.** An example of an assistive technology that replaces your style sheets with custom ones is Reader Mode. If you’re using CSS to insert meaningful content, this content will not be presented to users viewing the page in the browser’s Reader Mode or in a reading app. ### CSS content is (currently) not searchable and selectable At the time of writing of this post, CSS content is not searchable using the browser’s Find in Page functionality, nor is the generated text selectable. If you try searching for CSS-generated text on a page, you’ll notice that the browser won’t “find” or highlight the text, even if it is present on the page. And if you try to select CSS-generated content to copy it, for example, you’ll find that you can’t. This makes CSS generated content less usable. Note that the CSS Generated Content specification states in the Accessibility of Generated Content section that "generated content **should** be searchable, selectable, and available to assistive technologies" (emphasis mine). So if browsers follow the spec recommendation in the future we might get different results. ## So, what should you do instead? If the content is integral to the understanding of the page, it should be in your HTML. For text content, use HTML text. For images, use an HTML `<img>`, and provide a descriptive alternative text in the image’s `alt` attribute. If you _do_ insert meaningful content in CSS, make sure you give it a descriptive alt text that describes its purpose. ## Does this mean that alt text in CSS isn’t useful? No, it doesn’t. Not at all! I’ve seen arguments that because meaningful content belongs in HTML, and since you should use CSS-generated content for decorative content only, then you’ll probably never need the alt text feature. I find the contrary to be true! _Because_ you should only provide _decorative_ content using CSS, and _because_ CSS-generated content contributes to the accName of an element, and because you don’t want that content to cause unwanted screen reader announcements, you will want to hide that content from assistive technologies in order to improve the user experience. This is where CSS alt text provides the most utility: hiding decorative CSS images and text from screen readers. You can get really creative with CSS-generated content. CSS pseudo-elements have long been used to create visual text effects to pages, and Mandy Michael discusses an example of such effects in a blog post she wrote. As Mandy shows, hiding the duplicate text in CSS is essential, otherwise the text will be announced multiple times, resulting in a bad screen reader user experience. Just keep in mind not to leave the alt text out when the content is decorative, otherwise it _will_ be announced (except in the case of an image added using `url()`), resulting in unnecessary and unhelpful screen reader announcements. ## Summary and outro 1. Ideally, avoid using CSS pseudo-elements for meaningful content. Prefer using HTML. 2. If you do insert meaningful content in CSS, ensure it has descriptive alt text that communicates its purpose. 3. ‘Hide’ decorative or redundant CSS-generated content by giving it an empty alt text. And remember that support may change, bugs may be fixed, others may appear, heuristics may be introduced, and things may stop working the way they have always worked. So make sure to **always perform your own testing** to ensure that your content and component works as expected. ## Recommended reading I wanted to keep this post short and only discuss the current state of alternative text for CSS generated content, as well as general recommendations to get the most out of this feature. If you’re interested in learning more about any of the topics I mentioned throughout the post, consider reading these articles: * Alternative text for CSS generated content * alt text for CSS generated content * The problem with data- attributes for text effects * Voice Control Usability Considerations For Partially Visually Hidden Link Names ## Continue learning Did you know that the `alt` text is only one of _five_ different ways the browser computes the accessible name of an image? For example, did you know that if the image has no `alt` attribute (i.e. it is omitted) and no `title` attribute, and the `<img>` is contained in a `<figure>` that only contains the `<img>` and a `<figcaption>`, then the browser will use the text equivalent of the `<figcaption>` to give the image an accessible name? Understanding how browsers compute the accessible name of an element, and understanding what _you_ need to do to give your images accessible names is like having accessibility superpowers. Not only will you know what to do to make your images accessible, but you will also have a toolkit of techniques at hand that you can reach for when you need to _fix_ existing images in your codebases. There are least three different ways to provide an accessible name to elements **that are allowed to have one**. What happens when multiple naming methods collide? HTML label? ARIA attribute? CSS content? And what happens when multiple naming methods collide? Which one “wins” in the accessibility tree? ⚖️ If you’re ready to stop guessing and to _really_ understand web accessibility, then I highly recommend enrolling in the Practical Accessibility course. Practical Accessibility is a structured curriculum that will equip you with the foundational knowledge you need to start creating more accessible websites and web applications today. Learn more about the course and enroll at practical-accessibility.today. Thank you for reading!
www.sarasoueidan.com
September 17, 2025 at 7:40 PM
Accessible notifications with ARIA Live Regions (Part 1)
In this chapter, we’re going to learn about ARIA live regions — the accessible notifications system that enables us to make **dynamic** web content more accessible to screen reader users. Without live regions, some rich web applications would be more challenging to use for screen reader users. So if you’re building web applications such as Single Page Applications (SPAs), you need to understand live regions so that you can utilize them **where appropriate**. This chapter is split into two parts. In **this first part** , we’re going to learn about why ARIA live regions are important, and the different ARIA attributes and roles that you can use to create them. We’re going to get an overview of these attributes, as well as learn about their current support landscape and limitations. In the second part, we’re going to get more practical and discuss why you should _not_ use live regions as much as you might think that you do, and we’ll talk about alternative approaches you should use instead when you create some common UI patterns. And then we’ll discuss best practices for implementing more robust live regions when you need them today. But first, in order to understand _why_ live regions are important, **we must first understand how a screen reader parses web content and presents it to the user.** We won’t get into much detail (not at all, really!), just enough to get a good understanding of what problem live regions solve. ## How screen readers parse web content The way screen readers parse and present web content to their users is very different to how sighted users see that content. Screen readers work by **linearizing web content.** Linearizing a page’s content means converting the page’s two-dimensional content into **a one-dimensional string** (that is then either spoken to the user using text-to-speech, or delivered to them via a refreshable braille display). When content is linearized, it is presented to the user **one item at a time.** "You can think of it like listening to a cassette tape", says Web accessibility specialist Ugi Kutluoglu, "which you can rewind, fast forward, pause and play." This means that a screen reader user can skip to items or sections they want, and they can tab through interactive elements and Shift-tab their way back. But at the end of the day, they can only move forwards or backwards, **one item at a time** , because what they are presented with is a one-dimensional version of the page. ## Why we need an accessible notification system for screen reader users Reading content linearly works well for static webpages, but it doesn’t work so well for pages where content is altered and updated dynamically or asynchronously using JavaScript. If the user can only move in one dimension, and focus on one item at a time, how would they know when content is added, removed, or modified **somewhere else** on the page? For example, when a user sends an email in most email web apps, a status message is shown at the top of the screen, or a “toast” message pops up (typically at the bottom of the screen) to notify them of the status of their interaction — for example that the email is sending, has been sent, or maybe that the email could _not_ be sent. Some of these messages are urgent (like an error message), and some of them are not (like a success message, or a Draft Saved notification). When these status messages appear, they are intended to be communicated to all users. But while these messages may be perceivable by a sighted user, they’re not preceivable by a blind screen reader user. When the status message is shown, it is not communicated to screen reader users by default because the screen reader focus is on another element at that moment (on the ‘Send Email’ button in this case). Here is what happens when I use NVDA and activate the Send Email button and show a status message in a dummy email app demo I created. NVDA does not announce the status message when it is shown. Sorry, your browser doesn't support embedded videos. Here is a dummy email app demo you can try for yourself. See the Pen Live region status message in email web app by Sara Soueidan (@SaraSoueidan) on CodePen. If you activate the Send Email button in the debug version of the dummy email app, the screen reader will _not_ announce the status message that is shown. **A screen reader can only focus on one element or part of the page at a time.** This means that if the user presses a button and that button triggers an update somewhere else on the page, **there’s a good chance they will be oblivious to it**. So they need a way to be notified of these updates when they happen. There are two primary ways you make a screen reader announce an update when it happens: 1. By **_moving_ focus** to where the update has happened, (like we did with the summary of error messages in the accessible form validation chapter); 2. By **notifying** the screen reader of these updates when they happen. When you move the user’s focus to an element, screen readers typically announce the element to the user. But when an update happens and you don’t move the user’s focus to it, you must notify screen readers in some other way. ## Status messages in WCAG WCAG Success Criterion **4.1.3 Status Messages (Level AA)** states that: > In content implemented using markup languages, status messages can be programmatically determined through role or properties such that they can be presented to the user by assistive technologies without receiving focus. A status message is defined in the specification as "change in content that is not a change of context, and that provides information to the user on the success or results of an action, on the waiting state of an application, on the progress of a process, or on the existence of errors." Examples of a **change of context** are opening a new window, **moving focus to a different component** , going to a new page (including anything that would look to a user as if they had moved to a new page) or significantly re-arranging the content of a page. From the Understanding Status Messages page: > This Success Criterion specifically addresses scenarios where new content is added to the page without changing the user’s context. Changes of context, by their nature, interrupt the user by taking focus. They are already surfaced by assistive technologies, and so have already met the goal to alert the user to new content. As such, messages that involve changes of context do not need to be considered and are not within the scope of this Success Criterion. In other words, this success criterion aims to ensure that, unless you move the user’s focus or cause another change of context like a page refresh, you must ensure that status messages are communicated to screen reader users using the appropriate roles and properties. To do that, you currently need ARIA live regions. ## What are ARIA live regions? ARIA live regions are **a specific type of notification system primarily surfaced for screen reader users.** Using live regions, you can communicate content updates down to the accessibility layer so that screen readers are made aware of these updates when they happen. On an implementation level, a **live region** (**not to be confused with the`region` landmark**) is an element on the page that has been designated as being “live”. When an element is designated as a live region, **a screen reader is notified when any updates take place within the element (and its children), wherever its focus is at the time.** "Think of live regions as something like a livestream" says Web accessibility specialist Ugi Kutluoglu, "everything happening inside will be announced live like a news channel you’re listening to in the background." Using live regions, you can mark up status messages and other similar updates so that they are communicated to screen reader users. Here is our email notification example again with ARIA live regions working. Notice how when the ‘Send Email’ button is activated, NVDA announces the contents of the status message that is shown: Sorry, your browser doesn't support embedded videos. The screen reader announces the contents of the message because I’ve designated the message container as a live region (we’re going to learn how to do that shortly). So now the element is monitored for updates and the screen reader is notified of these updates when they happen. Then, when the button is activated, I inserted the contents of the message into the message container. And when I did, the screen reader was notified and it announced the update to the user. When an update happens in a live region, the screen reader announces that update, and it only announces it once. Toast messages and other similar status messages are the closest **visual equivalent** of a live region announcement. (Though not all toast messages are a good candidate for live regions. We’ll talk more about this later.) A toast message is used to present **timely information** — including confirmation of actions, statuses, and alerts. By nature, **toast messages are auto-expiring** — they disappear on their own after a few seconds. And once they disappear, they’re gone. The user cannot review the message again. Like toasts, **live region notifications are transient.** **Once an announcement is made, it disappears forever.** They cannot be reviewed, replayed, or revealed later. If the user misses an announcement, they miss it. It’s gone. That is, unless you provide them with a way to review it (like collecting all notifications in a notifications center, for example). Because of their transient nature, live regions have specific and limited use cases and should not be used as an alternative to other more persistent approaches. In fact, if you _can_ use another more persistent approach, you almost definitely should. We’ll talk more about how to use live regions and when _not_ to use live regions later in the chapter. ## Creating a live region Using ARIA, **almost any element can be designated as a live region**. It doesn’t need to be a structural element; and it doesn’t need to have any implicit semantics by default. You can designate an element as a live region using: 1. The `aria-live` attribute. 2. ARIA live region roles. HTML also provides one native element that has implicit live region semantics: the `<output>` element. We’re going to talk more about it in another section. ### 1. Using the `aria-live` attribute `aria-live` is the primary attribute used **to designate an element as a live region.** When used on an element, it indicates that this element may be updated, and those updates should be communicated to screen readers. The value of `aria-live` **describes the types of updates** that can be expected from the region. It accepts three values: `assertive`, `polite`, and `off` (which is equivalent to removing the property altogether). <!-- this div is now a live region! It's as simple as that. --> <div aria-live="[ polite | assertive ]"> ... </div> The value of `aria-live` you choose will depend on **the type, urgency and priority of the update.** * If the update is important enough that it requires the user’s **immediate attention** , `assertive` will tell the screen reader to _immediately_ notify the user, **interrupting whatever the user’s currently doing.** Assertive notifications are good for when users need to immediately know something and act on it, like when there’s **an error** in submitting information in a form, or something more serious like a **session timeout** or a **security alert**. Assertive notifications are very disruptive and should be limited to a few use cases where the messages are critical to the user and require their immediate attention. Otherwise, they may disorient users or cause them not to complete their current task. * `polite` on the other hand, is more… polite. It indicates that the screen reader **should wait until the user is idle** (such as when the screen reader has finished reading the current sentence, or when the user pauses typing) before presenting updates to them. Polite regions do not interrupt the user’s current task. They are more suitable for things like success messages, feeds, chat logs, and loading indicators, for example. `aria-live="off"` is the assumed default value for all elements. It indicates that updates to the element should not be presented to the user **unless the user is currently focused on that region**. So creating a live region is literally as simple as declaring the `aria-live` on an element. Here is an example where I have a `<div>` with no `aria-live` set on it. When you activate the button, the `<div>` will get populated with a message via JavaScript; but the screen reader will not announce the update. So the user will not be aware that any content has been added to the `<div>` at this point. Try adding `aria-live="polite"` or `aria-live="assertive"` to it and then activate the button again. The screen reader will announce the contents of the message even though focus is not moved to the message: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. This is pretty powerful! The live region works even if it is visually-hidden, as long as it is not hidden in a way that removes it from the accessibility tree. (We’ve learned all about choosing an appropriate hiding technique for your content in the hiding techniques chapter.) There are some valid use cases for visually-hidden live regions, but the general rule of thumb is that **if the update or message is visible to all users and the conveyed text is equivalent to the visible text (as is the case for most status messages), then you might as well use the same element for screen reader users that you are using for everyone else** , and designate it as a live region so that all users get the same update. For example, consider the dummy email example from the previous section again. To convey the same status message to screen reader users, all you would need to do is designate the message container as a live region using the `aria-live` property. Error notifications are urgent and require the user’s immediate attention, and you want the user to know that an error has occured as soon as possible. As such, the value of `aria-live` should be `assertive`. <div aria-live="assertive"></div> At this point it is important to note that you should place the live region container in the DOM as early as possible and _then_ populate it with the contents of the message using JavaScript when the notification needs to be announced. **This ensures that the live region is monitored for updates before they happen.** Otherwise, the update may not be communicated to screen readers. We will learn more about best practices for implementing live regions later in the chapter. By default, any padding, margin, and border on an element will take up space in the page’s layout even if the element is empty. Since the message container is placed in the DOM before the notification is shown, you will probably want to prevent it from taking up any visual space on the page when it is empty. To do that, you can use the `:not(:empty)` CSS selector to only apply the visual styles to it when it is _not_ empty (i.e. when the notification is shown). [aria-live="assertive"]:not(:empty) { padding: .25em 1em; background: maroon; ... } Because it is a best practice to include live region containers in the DOM as early as possible, I always use this CSS selector to visually “hide” my live regions when they are empty. And yes, yes I know that you can just apply these styles to the notification using a class name that you could add to the container via JavaScript, but why require JS for something so simple that can so easily be accomplished using CSS? Here is a live demonstration of this implementation: See the Pen Live region status message in email web app by Sara Soueidan (@SaraSoueidan) on CodePen. Fire up a screen reader on the debug version of this demo and then activate the ‘Send’ button. Try removing the `:not(:empty)` selector from the live region’s ruleset in the to see how it affects the visibility of notification container when it is empty. A live region does not need to be initially empty. Here’s another example where I have a list and I’m adding items to the list. I’ve designated the list as a assertive live region using `aria-live`. So now every time an item is added, the screen reader makes an announcement. See the Pen #PracticalA11y: Basic live region by Sara Soueidan (@SaraSoueidan) on CodePen. This means that you can use live regions to communicate different types of updates to an element, such as when content is added to the element or existing content is modified. ### Live region configuration ARIA provides three attributes that enable you to ‘configure’ when the screen reader should make an announcement, and what that announcement should contain: * `aria-relevant`, * `aria-atomic`, and * `aria-busy`. These attributes are _very_ useful and would enable you to use live regions to communicate different kinds of content updates when they are needed, but unfortunately current browser and screen reader support is inconsistent, so you can’t rely on them in your projects just yet. But we’re still going to get a quick overview of what they do because it will help you better understand the current limitations with ARIA live regions. #### aria-relevant: when should an announcement be made? The `aria-relevant` attribute is used to specify **what type of changes in the live region should trigger an announcement.** For example, should the screen reader make an announcement when a node is _added_ to the region? or when a node is _removed_? or when the text within an element changes? or maybe when any of these updates happen? `aria-relevant` accepts a space-separated list of the following values: `additions`, `removals`, `text`, and a single catch-all value: `all`. * `additions` will trigger a notification **when a DOM node is added to the region.** * `removals` will trigger a notification **when a DOM node is removed from the region.** * `text` will trigger when **text changes happen inside the region** , such as changing a text node inside the region or changing a text alternative for an image inside the region. * `all` is a shorthand for all three options. The default value is `additions text`, which means that a live region will trigger an announcement when content is added or text is changed within the region. The `removals` and `all` values should be used sparingly. Screen reader users only need to be informed of content removal when its removal represents an important change, such as when a user is removed from the list of active users in a chat room, for example. #### aria-atomic: what is contained in an announcement? The `aria-atomic` attribute determines what is contained in the announcement. It indicates whether the screen reader should present all or only parts of the changed element based on the change notifications defined by the `aria-relevant` attribute. For example, if a piece of text changes inside an element, should the screen reader announce only the changed text? or the entire contents of the live region? If text is _added_ to a live region, should only the newly added text be announced? or should the entire region’s content be announced every time? `aria-atomic` accepts two values: `true`, and `false`. * When `aria-atomic` is `false`, a screen reader should only announce the parts of the element that have changed. **This is the default value.** * When `aria-atomic` is `true`, the screen reader should announce the entire contents of the live region when a change happens inside of it. It doesn’t matter what has changed. It’s going to read everything — the entire content of the live region, plus the region’s accessible name, if it has one. `aria-atomic="true"` is useful for when a part of the region changes but you want the entire content to be read because otherwise the updated content may not make much sense on its own. A practical example is a “Now Playing” indicator. <p> <span>Now Playing:</span> <span>[ movie/soundtrack title ]</span> </p> If a playlist of movies or soundtracks is playing while the user performs other tasks on the page, and the name of the soundtrack that is currently playing changes, you can utilize live regions to announce that a new soundtrack is playing. When the soundtrack changes, the only part of the indicator that gets updated is the soundtrack’s name. But you want the entire sentence to be announced so that the user gets the context they need. You can do that by designating the indicator as an atomic live region (using `aria-atomic="true"`): <p aria-live="polite" aria-atomic="true"><span>Now Playing:</span><span>[ movie/soundtrack title ]</span></p> Now every time the title of the soundtrack or movie changes, the screen reader should announce “Now playing” followed by the name of the soundtrack. Here is a live demo: See the Pen Untitled by Sara Soueidan (@SaraSoueidan) on CodePen. Start a screen reader and try out the debug version of the playing indicator #### aria-busy: please wait until the changes are complete The `aria-busy` attribute is used to indicate that an element (typically an entire section on the page) is undergoing changes (such as a section loading new content), and that screen readers should therefore **wait until the changes are complete before exposing the content to the user**. By default, all elements have an `aria-busy` value of `false`. Meaning that they are _not_ undergoing changes and screen readers can, therefore, expose their content when the user navigates to them. To use `aria-busy`, you would set it to `true` on an element while the element is undergoing changes, and then flip its value to `false` when the changes are complete and ready to be exposed or announced to the user. `aria-busy` can be used on any element that is undergoing changes, even if that element is not a live region. If you use `aria-busy` on a live region, the contents of the live region will be announced after `aria-busy` is set to `false`. If multiple changes have been made to the element while it was busy, they are announced as a single unit of speech when `aria-busy` is turned off. ‘Skeleton screens’ in Single Page Applications (SPA) are a practical use case for the `aria-busy` attribute. A skeleton screen is a specific type of loading indicator that is shown in lieu of the content of a section being loaded until the content of that section loads. They often provide a wireframe-like visual that mimics the layout of the page and helps users build a mental model of what will be on the page when the content loads. `aria-busy` can be used to tell screen readers to ignore the section that is currently loading content until the content finishes loading. In that sense, it has a similar effect to `aria-hidden` — it ‘hides’ the contents of a busy region while it is undergoing changes. <!-- This section is updating... --> <section aria-busy="true"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> Since the busy section is effectively hidden from screen reader users, you will want to communicate the state of the loading content to screen reader users. You can do that by using a separate, visually-hidden live region. Using this region, you can communicate to the user that a screen has started loading, and then let them know when the loading is complete. So, when the content is loading, your markup would at a certain moment look like this: <div aria-live="polite" class="visually-hidden">Loading content...</div> <section aria-busy="true"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> and then when the content is loaded, flip the value of `aria-busy` to `false`, and update the content of the live region: <div aria-live="polite" class="visually-hidden">Content loaded.</div> <section aria-busy="false"> <h2>..</h2> <p>..</p> <article>..</article> .. <!-- more content is loading --> </section> This is an example of when a live region can be used exclusively for notifying screen reader users, but doesn’t need to be rendered visually because there is an alternative visual indicator for sighted users (the skeleton screen, in our case). So you can think of the live region like a text alternative for the skeleton screen in this case. Unfortunately, because `aria-busy` is currently not well-supported across screen reader and browser pairings, most screen readers (except JAWS) will read the contents of the busy region even before it’s done loading, which would result in a sub-optimal experience. You currently need to work your way around that by hiding the busy region using `aria-hidden`, and un-hiding it when its contents are done loading. For a detailed writeup about implementing accessible skeleton screens, check out Adrian Roselli’s article “More Accessible Skeletons”. Adrian provides a solution that doesn’t even require you to use live regions at all, and his article includes a live demo that you can tinker with and try for yourself. #### Summary and support landscape Properties for configuring live region announcements Value | Description ---|--- `aria-atomic` | _What is announced? When you update a live region, should it read all the content again or just the added content?_ If `true`: Announce the entire content of the live region, including its label, if present. If `false`: announce only the changed content. `aria-relevant` | _When is an announcement made? What types of changes to a live region should trigger the announcement? additions? removals? or all?_ If `additions`: Trigger an announcement when new elements are added to the accessibility tree of the live region. If `text`: Trigger an announcement when text content or a text alternative is added to any descendant in the accessibility tree of the live region. If `removals`: Trigger an announcement when an element, text, or text alternative is removed from the accessibility tree of the live region. If `additions text (default)`: Equivalent to the combination of `additions` and `text`. If `all`: Equivalent to the combination of `additions removals text`. `aria-busy` | Indicates that an entire section on the page is undergoing changes (such as a section loading new content), and you're telling screen readers to **wait until the changes are complete before notifying the user** If `true`: The element is being updated. If `false`: There are no expected updates for the element. As we mentioned earlier, **support for the`aria-relevant`, `aria-atomic`, and `aria-busy` attributes is currently inconsistent across browsers and screen reader pairings.** Paul J. Adam has created a test page that includes test cases for `aria-atomic` and `aria-relevant` when used on live regions, and has documented support gaps across platforms and screen readers. So, unfortunately, you can’t rely on these properties in your projects just yet. If you do, many of your content updates may be announced in ways that you did not intend them to be announced, which could result in a sub-optimal user experience. ### 2. Using live region roles When you use `aria-live` to create a live region, the element’s implicit semantics (if it has any) are retained. This means that you can use the appropriate element to represent the component you’re creating, and if the component is getting updated you can then designate it as a live region with the `aria-live` attribute. <!-- this list will be treated like any list on the page would be; since it is also designated as being live, any changes that happen to it should be communicated to screen readers and announced to the user --> <ul aria-live="polite"> <li>My list semantics are important.</li> <li>But I want you to know when new list items are added.</li> </ul> But what if you’re creating a notification or status message that has no semantic HTML element to represent it? For example, there are no semantic elements to represent (and distinguish between) different types of notifications (such as an alert notification or a status message, for example). While it is fine to use a `<div aria-live="">` for these notifications, it would be ideal if we exposed the nature or type of a notification to the user using appropriate semantics. ARIA provides five live regions roles that semantically represent five different types of updates: * **The`alert` role:** represents a live region with important, and usually time-sensitive information, such as error notifications. * **The`status` role:** represents a live region whose content is advisory information for the user but is not important enough to justify an alert, often but not necessarily presented as a status bar (such as a status or success message). * **The`log` role:** represents a live region where new information is added **in meaningful order** , and old information may disappear. Examples of logs are chat logs, messaging history, a game log, or an error log. In contrast to other live regions, **in this role there is a relationship between the arrival of new items in the log and the reading order.** The log contains a meaningful sequence and new information is added only to the end of the log, not at arbitrary points. * **The`marquee` role:** represents a live region where non-essential information changes frequently, such as stock tickers. The primary difference between a marquee and a log is that logs usually have a meaningful order or sequence of important content changes. * **The`timer` role:** represents a live region containing a numerical counter which indicates an amount of elapsed time from a start point, or the time remaining until an end point. Live region roles are **pre-configured**. They come with _implicit_ `aria-live` and `aria-atomic` values. ARIA live region roles and their implicit `aria-live` and `aria-atomic` mappings Role | `aria-live` value | `aria-atomic` value ---|---|--- `alert` | `assertive` | `true` `status` | `polite` | `true` `log` | `polite` `marquee` | `off` `timer` | `off` `alert` and `status` are the most commonly used live regions roles and have generally good support. The others have specialized uses and have **poor or no support** , and `marquee` and `timer` are even in danger of being deprecated and removed from the ARIA specification. #### Difference between using `aria-live` and live region roles The primary difference between using live region roles and using `aria-live` on its own is that **live region roles have semantic meaning.** They add explicit _semantics_ to an element ("This is an alert", "This is a status message", etc.), so some screen readers may announce “alert” before announcing the content of the message. For example, here is the dummy email app example again. Instead of using `aria-live="assertive"` on the notification container, I’m using `role="alert`. Here’s a video comparing how NVDA announces the notification, first when it is designated as a live region using `aria-live="assertive"`, and the when it is designated as a live region using `role="alert"`. Sorry, your browser doesn't support embedded videos. NVDA announces the word “Alert” before announcing the content of the notification when `role="alert"` is used. You can try it for yourself in the debug version of the demo using the role attribute. Here is another example that implements a form success message using `role="status"`: See the Pen #PracticalA11y: role status success message by Sara Soueidan (@SaraSoueidan) on CodePen. Another advantage to using a live region role over `aria-live` is that **live region roles accept an accessible name.** If you use `aria-live` to create a live region, the implicit semantics of the element you’re using it on will determine whether or not it can have an accessible name. As we learned in the accessible names and descriptions chapter, some elements are name-prohibited. For instance, a `<div>` will not consistently expose an accessible name unless you give it a meaningful role. ARIA live region roles provide meaningful roles to the elements they are used on and can therefore accept an accessible name. When a live region has an accessible name, screen readers include the name of the region in the announcement. See the Pen #PracticalA11y: shopping cart by Sara Soueidan (@SaraSoueidan) on CodePen. In this example I have a polite live region that contains the number of items in the user’s shopping cart. When the ‘Add to cart’ button is activated, the number of items is updated and the screen reader announces that number. But anouncing the number of items alone doesn’t provide the user with the same context that sighted users get when the shopping cart is visually updated. Ideally, you’d want the screen reader to announce “Shopping cart, 5 items”. Using `aria-labelledby`, you can provide an accessible name to the live region (namely: “Shopping Cart”). So now when the number of items is announced, the screen reader announces ‘Shopping cart’ before announcing the number of items it contains. You can try the live example out for yourself in the debug version of the shopping cart example. Providing an accessible name to a live region is useful for when you may have multiple updating regions on the page and you want to communicate which region the updates are coming from. A region’s name thus provides the necessary context for each announcement. ### 3. Using the HTML `output` element HTML provides one native live region element: `<output>`. By definition, `<output>` represents an element into which you can inject the results of a calculation **or the outcome of a user action.** The second part of the definition can be interpreted to mean that the `<output>` element can be used to display a feedback message as a result of user interaction (like a toast or status message!). The `<output>` element has implicit live region semantics. It maps to the ARIA `status` role, which means that it represents a polite live region. `<output>` is also a labelable element, which means that you can give it an accessible name using the `<label>` element. <label for="[ outputID ]">..</label> <output id="[ outputID ]"> .. </output> <!-- or --> <label for="[ outputID ]"> .. <output id="[ outputID ]"> </output> </label> A practical use case for the `<output>` element is using it to represent the total price of products in a cart on an e-commerce website. <label for="result">Your total is:</label> <output id="result"> </output> <!-- or --> <label for="result"> Your total is: <output id="result"> </output> </label> When the user updates the number of items in their cart, the total price is updated to reflect the new total price. Wrapping the price in an `<output>` element allows it to be announced by the screen reader when it is updated. Here is a dummy example where the total price is updated based on how many items are chosen in the select dropdown: See the Pen #PracticalA11y: The <output> live region element by Sara Soueidan (@SaraSoueidan) on CodePen. You have probably also noticed that VO with Safari announces the initial total value before it announces the updated total value every time it makes an announcement. The `<output>` element is currently not consistently announced across browser and screen reader pairings. And not all screen readers announce the accessible name of the `<output>` when its content is updated. For example, VoiceOver with Safari announces the content of the `<output>` element in the example above but it does not announce its accessible name. NVDA with Firefox does not announce the accessible name either. Whereas paired with Chrome, VoiceOver announces the contents of `<output>` with its accessible name as it is intended. There are also other quirks and some inconsistencies in the way `<output>` is currently announced across browser and screen reader pairings. Accessibility engineer Scott O’Hara has written a great article about the `<output>` element that I recommend reading if you want to learn more details about `<output>` and its quirks. Scott shares the current state of support, as well as suggestions for working around some of the support gaps. ## Summary and outro So, to quickly sum up: * ARIA live regions are a specific type of notification system primarily surfaced for screen reader users. * You can create a live region using the `aria-live` attribute. The value of the attribute depends on the type and urgency of the updates you’re communicating. * The `aria-relevant`, `aria-atomic`, and `aria-busy` attributes allow you to configure when an announcement should be made and what the announcement should contain. But support for these attributes is currently poor. * ARIA provides five roles that represent five different types of updates. Of these five roles, `alert` and `status` have the best support and can be used to represent status messages in web applications. * The `<output>` element is currently the only native HTML live region. The `<output>` element has a few quirks and some support gaps that, depending on your use case, you may be able to work around today. As you might imagine, the current state of support for live region features and properties limits your uses of live regions quite a bit. Furthermore, the inherent behavior of live regions also makes them unsuitable for certain types of updates. We’re going to elaborate more on this in the second part of this chapter. Fortunately, for many (if not most!) common UI patterns, there’s often a more robust way to make users aware of content updates when they happen. And for the few instances when you do need to use live regions, you can make them work by following a few implementation best practices. We will discuss all of that in more detail in the second part of this chapter. Many thanks to **James Edwards** (@siblingpastry) for reviewing this chapter.
www.sarasoueidan.com
September 13, 2025 at 5:24 PM
A guide to designing accessible, WCAG-conformant focus indicators
Imagine you visit a website and you want to browse it for some content. You want to buy something; or maybe book a flight somewhere. And as you move your cursor onto the page, it suddenly disappears. Your hand may be still on the mouse, and you’re moving the mouse across the screen and across the page; but you can’t see where it is. You may or may not be hovering over a link or a button or any other form control at any moment. But if you _are_ hovering over one, you don’t know which one it is. You could try clicking and then finding out, but you can probably already imagine what a nightmare of an experience you’re about to get into. Unfortunately, keyboard users experience the Web in a similarly frustrating manner too often. Their equivalent of a mouse cursor is usually hidden on too many websites, making it almost impossible for them to navigate those sites. A keyboard user’s cursor equivalent is the **focus indicator**. By designing and implementing accessible focus indicators, we can make our products accessible to keyboard users, as well as users of assistive technology that works _through_ a keyboard or emulates keyboard functionality, such as Speech control, switch controls, mouth sticks, and head wands, to mention a few. ## What exactly _is_ a focus indicator? Keyboard users typically navigate their way through websites by pressing the `tab` key. This allows them to jump from one focusable element on the page to another. Just like mouse users, they need to be able to see where they are on a page as they Tab their way through it, otherwise they won’t be able to identify the elements they are interacting with. That’s what **focus indicators** are for. A focus indicator is a visual indicator that “highlights” the currently focused element. This visual indicator is commonly presented as an outline around the element. An outline takes the shape of its element, and since every element in CSS is a rectangle, a focus indicator is, therefore, typically a rectangle drawn around an element. Sorry, your browser doesn't support embedded videos. Navigating the Mozilla Developer Network (MDN) website using a keyboard. As you tab through the homepage, you can see a rectangular outline highlighting the currently focused element. A focus indicator can also take other forms, but outlines are very common for good reasons. Outlines have an advantage over other visual indicators (like borders or background colors, for example) in that they can be applied to the element without causing any significant changes to that element. And since an outline is not part of an element’s box model, it does not affect the layout of that element, and will therefore not cause any layout shifts when it is applied. (That’s also why outlines are preferred over borders for visualizing and debugging layouts 💡) Furthermore, outlines are retained in forced colors modes where background colors, border colors, and box shadows are usually overridden by user and system styles. So a focus indicator allows a keyboard user to see exactly where they are at any given moment. Without it, they wouldn’t know where they are on a page and they wouldn’t be able to navigate the page and operate its controls. **The focus indicator is to keyboard users what the mouse cursor is to mouse users.** And just like you would never want to hide the mouse cursor, you never want to hide the focus indicator. Laura Carvajal on stage at Fronteers conference 2018. In fact, **a visible focus indicator** is a requirement for a site to be considered accessible under the Web Content Accessibility Guidelines (WCAG). Removing or hiding focus indicators is a violation of (and will therefore fail) Success Criterion (SC) 2.4.7: Focus Visible (Level A), which states that: > any keyboard operable user interface has a mode of operation **where the keyboard focus indicator is visible.** ## Browser default focus indicators Browsers provide focus indicators to native interactive elements out of the box, for free. And most of us—if not all—have at some point in time included this CSS snippet in our stylesheets: :focus { outline: none; } to remove those focus indicators applied by the browser. To meet **Focus Visible** , you should avoid removing the focus indicator provided by the browser _unless_ you are replacing it with your own accessible focus indicator. And I do recommend that you do that. (We’ll elaborate _why_ in this article.) By not removing the default browser focus indicators, you may meet the requirement of showing a visible focus indicator. But that may not always be enough because a focus indicator needs to be _clearly_ visible to be considered accessible. And browser focus indicators may not always be. (What’s the benefit of _showing_ a focus indicator that many users can’t _see_?) In order for a focus indicator to be clearly visible it needs to have a color contrast that is high enough for users with moderately low vision to be able to discern it. The default focus indicator’s color contrast is not consistent across browsers. How Chrome, Firefox, MS Edge, and Safari style their respective focus indicators (at the time of writing) when applied to a blue button on a white background. Depending on your website’s color palette, the colors of the default browser focus indicators may clash with the colors used on your website, making them difficult (if not impossible) to see. When that happens, you’ll need to _override_ the default focus styles with better, more accessible ones. In this article, we’re going to learn about the accessibility requirements that your focus indicators need to meet to be considered accessible. Using these requirements, you’ll be able to determine when and why default browser focus indicators don’t meet these requirements, and how you can ensure that your custom indicators will. ## WCAG 2.1 and WCAG 2.2 focus indicator accessibility requirements SC 2.4.7: Focus Visible (Level A) requires that a visible focus indicator exists for components that have keyboard focus. So, the first step in creating accessible focus indicators is to **not hide focus indicators.** 😊 /* Do. Not. Do. This. */ * { outline: none; /* This is bad. */ } SC 1.4.11 Non-Text Contrast (Level AA) states that (emphasis mine): > The visual presentation of the following have a contrast ratio of at least 3:1 against adjacent color(s): > > User Interface Components: Visual information required to identify user interface components and states… Focus indicators are used to identify a component state (focus). So, according to this criterion, focus indicators **must have a color contrast ratio of at least 3:1 against adjacent colors.** For user interface components, ‘adjacent colors’ means the colors adjacent to the component. For a component’s focus indicator, ‘adjacent colors’ **depends on the position of the focus indicator** within the component. We will learn about this in more detail in another section. In WCAG 2.2, three success criteria are added that define the accessibility of focus indicators depending on their **color, surface area, and visibility** : * **SC 2.4.11 Focus Not Obscured (Minimum) (Level AA)** , * **SC 2.4.12 Focus Not Obscured (Enhanced) (level AAA)** * **SC 2.4.13 Focus Appearance (Level AAA)** These new criteria aim to ensure that a keyboard focus indicator is **clearly visible and discernible** , and they define the conditions to ensure that. **SC 2.4.13 Focus Appearance** states that: > When the keyboard focus indicator is visible, an area of the focus indicator meets all the following: > > * is at least as large as the area of a 2 CSS pixel thick perimeter of the unfocused component or sub-component, and > * has a contrast ratio of at least 3:1 between the same pixels in the focused and unfocused states. > **Focus Appearance** is closely related to **Focus Visible** and **Non-Text Contrast**. **Focus Visible** requires that a visible focus indicator exists while a component has keyboard focus; **Focus Appearance** defines a minimum level of visibility (which we’ll learn about in this article). Where **Non-Text Contrast** mandates that focus indicators have a color contrast ratio of at least 3:1 against _adjacent_ colors, **Focus Appearance** requires a sufficient change of contrast for the focus indicator area. (We’ll elaborate more on what this means shortly.) Even though these criteria are at varying conformance levels, they complement each other, and together they ensure that focus indicators are clearly visible and accessible to more people. Since we’re always aiming for usability beyond conformance, we’re going to treat them all equal and learn how to design focus indicators that pass all of them. The purpose of **Focus Appearance** is to specify **a minimum area** for the focus indicator, as well as a minimum contrast ratio for that area. The “2px thick perimeter” part of the SC is the **minimum _area_ of the focus indicator that has a 3:1 contrast ratio between the same pixels in the focused and unfocused states.** This does not mean that the focus indicator needs to be a 2px-thick solid outline around the element, only that the indicator needs to be **at least** that large. > A keyboard focus indicator can take different forms. This Success Criterion encourages the use of a solid outline around the focused user interface component, but allows other types of indicators that are at least as large. . — Understanding Focus Appearance Obviously, providing a 2px thick outline around the element would be the simplest way to meet the size requirement. But the focus indicator can take other forms. In this article, we’re going to understand what it means to be “at least as large as a 2px thick perimeter”, and we’re going to see examples of how you can meet the minimum area requirement without necessarily providing a 2px thick outline as a focus indicator. We’re going to start by defining two terms that will help you design custom focus indicators while still ensuring they pass the requirements specified in **Focus Appearance** : the **focus indication area** , and the **contrasting area**. These terms were introduced in previous versions of the success criterion and have been edited out in the final wording. But I think they are helpful in understanding and meeting the requirements. For most of the examples, we’ll be demonstrating and examining the focus indicator requirements when applied to **a blue button set on a white background.** In what follows, we’re going to get a little nerdy! ### 1. The focus indication area and the contrasting area When a component changes on focus to include a focus indicator, that change can always be measured as a change of color contrast. If you add a black outline around the blue button, the change of color between the unfocused and focused states is from white to black. That’s because **the area — the pixels on the screen — that has changed color** in the focused state is the area _around_ the button. That area was initially white, and it changed to black when the button received focus. This area is called **the focus indication area.** The focus indication area is the area in square CSS pixels where the change in color between the focused and unfocused states of the component happens. **Focus Appearance** states that “an area of the focus indicator” must have a 3:1 contrast ratio between the unfocused and focused states. That is, an area _of the focus indication area_ (a _subset_ of the focus indication area) must have a 3:1 contrast ratio between the unfocused and focused states. The area that meets this contrast requirement is called the **the contrasting area**. And **the contrasting area may or may not be equal to the entire focus indication area.** In the previous example, the color change happens from a solid white to a solid black, and the color contrast ratio between the unfocused and focused state (white and black) is **21** :1. So the entire focus indication area meets the minimum contrast requirement. This means that the contrasting area is equal to the entire focus indication area. Similarly, if you add a black outline that is separated from the button, once again, the area that exhibits the change in color is the contrasting area. I like this pattern because it adds some breathing room and helps the focus indicator stand out, making it easier to see. You can offset the outline from the component using the CSS `outline-offset` property. If you add an outline inside the button itself, the contrasting area then lies inside the button. The change of color is from blue (the button’s background color) to black. The color contrast ratio between the focused and unfocused state is **4.86** :1. If the button changes its background color from blue to black on focus, then the entire button’s background area is the contrasting area, and the color contrast ratio between the focused and unfocused state is once again **4.86** :1. If the button’s border color changes when it receives focus, then the contrasting area lies along the button’s border: When the button’s border color or background color change on focus, the contrasting area must have a minimum contrast ratio of 3:1 between the focused and unfocused state to meet **Focus Appearance** contrast requirements. If the contrast change is less than 3:1, the focus indicator not only fails **Focus Appearance** , but it will also fail **SC 1.4.1 Use of Color (Level A)**. When the focus indicator is a solid color, measuring the color contrast ratio in the contrasting area is straightforward. But color changes may not always be solid. You may want to indicate focus on the button by applying a gradient drop shadow to it. In this case, **only the portion of the gradient with sufficient contrast (larger than 3:1) will be our contrasting area** ; the remaining portion that fails will not be a part of it. This is an example of when the contrasting area is smaller than the focus indication area. You may need to take some spot-checks on the gradient area and establish what area meets the contrast requirement. **The greater the change of contrast between the unfocused and focused states, the easier it is for users to see it.** ### 2. Minimum contrasting area **The bigger the visible change when the component receives focus, the easier it is to see.** And to ensure that focus indicators have good visibility, **Focus Appearance** requires **a minimum surface area** for the contrasting area: the contrasting area needs at least as large as a 2px thick perimeter around the element. The simplest way to meet this requirement is to provide a 2px thick outline that _encloses_ the element or component. To enclose a component means to **solidly** _bound_ or _surround_ the component. The Understanding Focus Appearance page provides two images that demonstrate the difference between an outline that bounds a component, and an outline that surrounds it: In the first image, the focus indicator solidly _bounds_ the star. In the second image, the focus indicator (also a solid outline) _surrounds_ the star. A solid outline around an element is an example of a focus indicator that solidly bounds an element. .element { outline: 1px solid #000; /* this outline *bounds* the element */ } And a solid **2px** outline has an area of at least 2px thick perimeter of the component. (Of course, any outline thicker than 2px will also meet the area requirement.) .element { outline: 2px solid #000; /* this outline meets the minimum area requirement */ } If the focus indicator is dashed or dotted (not solid), it no longer “solidly” bounds or surrounds the component (because it is no longer “solid”). And it will also no longer be equal to a 2px thick perimeter of the component. .element { outline: 2px dashed #000; /* this outline does not the minimum area requirement */ } The perimeter is a "continuous line forming the boundary of a shape not including shared pixels, or the minimum bounding box, whichever is shortest." The perimeter calculation for a 2px thick perimeter around an element is: 2×(2×h+2×w) = (4×w + 4×h), where **h** is the height of the element, and **w** is the width. (The calculation is simplified in that it does not include shared pixels.) The perimeter of a circle is **2𝜋r** , where **r** is the radius of that circle. A 2px thick perimeter around a circle is equal to **4𝜋r**. When the focus indicator is a 2px dashed outline, its area is equal to 2 times the length of the perimeter of the button (or component) _minus_ all the gap spaces introduced between the dashes of the outline. The resulting length is approximately half of the required minimum area. To meet the minimum area requirement, you can double the thickness of the outline to compensate for the area that is lost in the gaps. .element { outline: 4px dashed #000; /* this outline meets the minimum area requirement */ } Here’s what a dashed outline looks like with 4px thickness. **The thicker the outline, the larger its surface area, and the easier it is to see.** Now let’s assume, for demonstration purposes, that you’re designing focus styles for a **150px** by **75px** button. A 2px thick perimeter around this button is equal to 4×150px+4×75px = **900px**. So the focus indicator of this button needs to have a contrasting area of at least 900px. If you apply an _inner_ outline to the button, this outline is going to be smaller than the perimeter of the button (because the outline’s width and height are shorter than the button’s width and height). And an inner 2px solid outline has an area less than the 2px thick perimeter of the button. Once again, increasing the thickness of the outline will make up for the area lost by placing the outline inside the button. A 130px by 55px 1px-thick solid outline inside the button will have a surface area of 370px. A 2px-thick solid outline's area will be 2×370px = 740px, which is smaller than the required minimum area (900px). A 3px-thick solid outline passes the minimum area requirement with an area of 1110px. Inner outlines are useful for many elements where providing an outer outline may not be always suitable, like if you have a list of items in a drop-down menu or a drop-down navigation, for example. Other elements with hidden overflow will also benefit from inner outlines. As we mentioned before, the focus indicator can take other forms, too. For example, you may provide a focus indicator only on either side of the component. You only need to ensure that the line is thick enough so that its area is at least as large as the perimeter of the item. In this example, each item in the list is 187px wide and 42px high. A 2px thick perimeter around an item is equal to 4×187+4×42 = 916px. The focus indicator provided is a 12px thick line along each of the shortest side of an item. The area of each line is 12×42 = 504px. The total area of the focus indicator is 2×504 = 1008px, which is larger than the minimum 916px area. So, this indicator passes minimum area requirements. The thicker the lines, the larger the contrasting area, the more visible the focus indicator is. The main goal of the minimum area requirement is to ensure that the focus indicator is easier to see. Whetever the style you choose to indicate focus, the important thing is to ensure that the contrasting area meets the minimum area requirement(s), so that it can be easily seen. If you provide a gradient focus indicator to an element, you’ll want to calculate the component’s perimeter and ensure the contrasting area within the gradient is at least twice as large as the perimeter. ### 3. Minimum contrast against adjacent colors According to SC 1.4.11 Non-Text Contrast (Level AA), focus indicators **must have a color contrast ratio of at least 3:1 against adjacent colors.** The Understanding Non-Text Contrast page defines ‘adjacent color(s)’ as the “colors adjacent to the component”. For our button, the adjacent color is the color of the white background around the button. The adjacent colors for the _focus indicator_ depend on the position of the focus indicator within the component. The focus indicator can be: * **outside the component** , in which case it needs to contrast with the background around the component (that’s the indicator’s adjacent color). In this example, the button's focus indicator is a 2px thick black outline that lies outside the button. The adjacent color is the color of the white background around the button. The indicator's black color must have a minimum 3:1 contrast ratio with the white background (which it does). * **inside the component** , in which case it needs to contrast with the adjacent color(s) **within the component**. In this example, the button's focus indicator is a 4px thick black outline that lies inside the button. The adjacent color is the background color of the button. The indicator's black color must have a minimum 3:1 contrast ratio with the button's blue background (which in this example it does). * **along the component’s border** , in which case it needs to contrast **with both the component’s background as well as the background around the component**. In this example, the button's focus indicator is a 4px thick black _border_. The adjacent colors are both the background color of the button, as well as the background color around the button. The indicator's black color must have a minimum 3:1 contrast ratio with the button's blue background as well as with the white background around the button (which in this example it does). In this example, the button's background color is black. So the black focus indicator does not meet Non-Text Contrast requirements because it has no contrast with the button's black background color, even if it has sufficient contrast against the white background outside the button. If the focus indicator is **an inner border** , it needs to contrast with the component’s background color, as well as with the component’s border color. Those two are its adjacent colors. For example, if the button has a blue background, a black border, and the focus indicator is a white inner border, then the white indicator must have a 3:1 contrast ratio against both the blue background and the black border. * **partly inside and partly outside the component** , where either part of the focus indicator can contrast with the adjacent colors. I haven’t seen a focus indicator in the wild that’s partially inside and partially outside an element. But if the indicator _is_ partially inside and partially outside, then it needs to have a 3:1 contrast ratio **either** with the component’s background color, **or** the color outside the component. The focus indicator in this example is an outline that lies partially inside and partially outside the button. So it needs to contrast either with the white background or with the button's blue background. ### 4. The focused element cannot be obscured The purpose of a focus indicator is to allow the user to see where they are on a page, by making the currently-focused element more prominent. But what good is a focus indicator if the focused element itself is not visible because it’s hidden off-screen or obscured by other elements on the page? SC **2.4.11 Focus Not Obscured (Minimum)** states that: > When a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content. In other words, you want to make sure the user can actually see the element or component that they’re focusing on, by making sure it’s not hidden behind other content on the page. That being said, this criterion requires that the component be "not entirely hidden". This does imply that it could be partially hidden, as long as it’s still partially visible. **SC 2.4.12 Focus Not Obscured (Enhanced)** (which is the level-AAA version of this requirement) states that: > When a user interface component receives keyboard focus, no part of the component is hidden by author-created content. When aiming for level-AA conformance, you may get away with partially hiding the focused component, though I can’t imagine where or how that would not be problematic. Try to **always make sure focused component is _entirely_ visible** and not obscured by other content. It’s just better for usability. **Focus Not Obscured (Enhanced)** is one of the level-AAA criteria that are fairly easy to meet, even if you’re not aiming for level-AAA conformance. When you’re testing your web pages for keyboard accessibility, check that the elements are visible when they receive focus. Make sure they’re not obscured by other elements, like modal dialogs, fly-outs, or fixed components like fixed headers. Conversely, you want to ensure that elements that _are_ intentionally hidden cannot receive focus when they shouldn’t. ## Examining (current) browser focus indicators against WCAG requirements Now that we know the accessibility requirements for focus indicators, we can examine the focus indicators provided by the most popular browsers and measure how well they meet these requirements. **Non-Text Contrast** states that "Visual information required to identify user interface components and states [must have a contrast ratio of at least 3:1 against adjacent color(s)], except for inactive components **or where the appearance of the component is determined by the user agent and not modified by the author**" (emphasis mine). What this means is that browser focus indicators are exempt from these requirements, **even if they don’t meet them**. That is, the default focus indicators are considered conforming to this criterion, even if they don’t have a 3:1 contrast ratio against adjacent colors — i.e. if they’re not clearly visible. The default focus indicators are also exempt from **Focus Appearance** requirements as long as "the focus indicator and the indicator’s background color are not modified by the author." So, you could get away with showing the default indicator(s) and pass conformance tests, but this does not mean that the focus indicators will be usable by the people who need them. (And they’re often not going to be, as you’re going to see in this section.) Furthermore, the default focus indicators can be modified and customized by users, and there is no way for you to know what color the indicator is, and whether or not it has sufficient contrast with the colors on your website. This is yet another instance of “WCAG does not guarantee usability” that we’ve seen quite a few examples of over the course of the previous chapters. In this section, we’re going to measure just how well the default focus indicators meet or don’t meet the requirements for focus indicators to be clearly visible (and usable). The purpose of this section is to demonstrate how you might check whether a focus indicator meets the requirements specified in the WCAG criteria we discussed earlier, and to show how and when the default focus indicators would not meet these criteria and, therefore, why you should consider providing more accessible indicators instead. To determine if the default indicator passes the requirements, you need to check that: * it has sufficient contrast with adjacent colors * it has a contrasting area that is at least as large as a 2px thick perimeter of the button * it has sufficient contrast in the contrasting area between the focused and unfocused state If it does not meet either of these, then there’s a good chance the indicator is not easily discernible by people with low vision, and you may want to consider overriding the default focus indicators with more visible ones. We’re going to test the default focus indicators as they are applied to an unstyled, native button, as well as to three styled buttons: a black one with a grey border, a blue one, and a pink one. We’re going to determine the position of the indicator relative to the button (inside, outside, or along the border) first, which we will use to determine the indicator’s adjacent color(s), and to calculate whether or not it has sufficient contrast with those colors. When the focus indicator is provided outside the button, its adjacent color is the color of the background around the button. The background color is also going to be the color of the focus indicator’s contrasting area in the unfocused state. This means that if the focus indicator has sufficient contrast with the background color, then it will pass both color contrast requirements specified in **Non-Text Contrast** and **Focus Appearance**. If the focus indicator is provided along the border of the button, then to meet **Non-Text Contrast** requirements, it needs to have sufficient contrast against the button’s background color, as well as the background color of the page. To meet **Focus Visible** _and_ **Use of Color** requirements, it needs to have sufficient contrast between the colors in the unfocused and focused states (i.e. the initial color of the border and the color of the border in the focused state). I use the Color Contrast Analyzer (CCA) desktop app created by the folks at **TPGi** to check the contrast of colors within my components. It is very handy for doing quick spot checks for pretty much any element on the screen — Web or native. **Safari** : Safari’s default focus indicator color is the accent color defined on the OS-level in **Settings > Appearance > Accent Color**. The default accent color is blue, and so is the default focus indicator color. This color is customizable, so the focus indicator may take any color chosen by the user. On my OS, that color is pink. So the focus indicator is a light pink outline that surrounds the button. The indicator’s adjacent color is the background color surrounding the button. Depending on the color theme of your website, it may not meet the minimum contrast defined in **Non-Text Contrast** and **Focus Appearance**. The pink color has a **low contrast ratio** against most background colors. It has a 2.1:1 contrast ratio against the white background. And a 2.1:1 contrast ratio against a black background. Safari's default focus indicator as it appears on our four buttons and a white page background. Safari's default focus indicator as it appears on our four buttons and a black page background. Note that the indicator also changes when the button is styled: it is offset from the edges of the button, and it becomes thinner, which makes it even more difficult to discern. **Chrome** : Chrome’s focus indicator is a pink outline applied along the button’s border. This means that it needs to have sufficient contrast against both the background color outside the button, as well as with the button’s background color. Depending on the colors used in your components, it may not meet the minimum contrast defined in **Non-Text Contrast**. For example, the pink outline has a low contrast against the background color of the black button (2.8:1), the blue button (1.7:1), and the pink button (1.4:1), which means that it does not meet **Non-Text Contrast** requirements. Chrome's default focus indicator as it appears on our four buttons and a white page background. **Edge** : Edge’s focus indicator is a black outline applied along the button’s border, which means that it needs to have sufficient contrast against both the background color outside the button, as well as with the button’s background color. Depending on the colors used in your components, it may not meet the minimum contrast defined in **Non-Text Contrast**. Being black, the indicator provides good contrast against light background colors. But it doesn’t meet the minimum contrast ratio against dark colors. For example, in our test here, the focus indicator has no contrast against the adjacent black background inside the black button, so it does neet meet the minimum contrast defined in **Non-Text Contrast**. Edge's default focus indicator as it appears on our four buttons and a white page background. **Firefox** : Firefox’s current focus indicator is a pink outline that surrounds the button. The indicator’s adjacent color is the background color of the page. Depending on the background color you use, the pink outline may not meet the minimum contrast defined in **Non-Text Contrast** and **Focus Appearance**. For example, on a light grey background, the contrast ratio between the pink and the grey is 2:1 (which is less than 3:1). This means that the contrasting area’s contrast ratio does not meet the minimum contrast requirement, and neither does the contrast ratio against adjacent colors. Firefox's default focus indicator as it appears on our four buttons and a light grey page background. * * * Chrome, Edge, and Firefox apply what looks like **a second outline** — a white outline — around the indicator. You can see it more clearly on darker background colors. For example, here is what it looks like on a dark purple background in Chrome and Edge, respectively: Chrome's default focus indicator as it appears on our four buttons and a dark purple page background. Edge's default focus indicator as it appears on our four buttons and a dark purple page background. The fact that the focus indicator in these browsers consists of two outlines, and that **both outlines meet the minimum area requirement** (they’re both at least as large as a 2px thick perimeter), then either of these outlines may be sufficient to pass the minimum color contrast requirements. This white outline lies outside the button, which means that it only needs to contrast with the background color outside the button. It does not need to contrast against the color(s) of the button. The white color provides sufficient contrast against dark backgrounds, making the indicator clearly visible. This means that if the pink outline in Chrome’s focus indicator does not meet the minimum contrast ratio, for example, the white outline might, thus passing the criteria. White and pink are still likely going to fail minimum contrast requirements against light background colors, though. If the white outline were black instead, it would provide sufficient contrast against light background colors. Now imagine if there were two outlines surrounding a component: a black one _and_ a white one. The black would ensure sufficient contrast against light background colors, and the white would ensure sufficient contrast against dark background colors. This means that the outline would always meet minimum contrast requirements regardless of the colors used on the page. Edge’s current focus indicator is the closest out of all default indicators that gets close to providing this level of contrast. But the fact that Edge’s indicator is provided as a border means that it should contrast against two adjacent colors, and if these colors are opposite colors (like if one of them is light and the other is dark), it is probably not going to meet the minimum contrast ratio defined in **Non-Text Contrast** against one of them. But if the black and white outlines both _surrounded_ the button, then they would only need to have a 3:1 contrast ratio against the background color around the button. Measuring the contrast ratio against only one color means that either the black or the white outline will pass adjacent color contrast requirements. If white doesn’t pass, black will; and vice versa. This makes the combination of black and white the ideal recipe for a more ‘universal’ focus indicator. ## A ‘universal’ focus indicator Inspired by Edge’s indicator, you can provide an improved, universal, black-and-white focus indicator for your website that works for most (if not all) focusable elements and provides sufficient contrast against all background colors. This focus indicator consists of two (or more!) outlines: a white outline and a black outline. Since we can’t use the `outline` property to provide two outlines of different colors, we currently need to use a combination of `outline` and `box-shadow` to create our desired effect: :focus-visible { outline: 3px solid black; box-shadow: 0 0 0 6px white; } Using the `:focus-visible` selector instead of the `:focus` selector means that the focus indicator will only be shown when an element reveives _keyboard_ focus. We’ll talk more about this in the next section. The `box-shadow` declaration creates a ‘solid’ box shadow (with a zero blur radius) around the element. Outlines overlap box-shadows, which means that the 3px solid (black) outline created with the `outline` property will overlap the box shadow created ‘behind’ the element. So we extend the box-shadow by 3px (so the total width is 6px). This ensures that the visible portion of the (white) shadow is also 3px wide and that it looks like a solid outline. If most of the buttons or components on your website have dark colors, you may want to flip the order of the black and white outlines so that the white outline creates an even higher contrast with the components: :focus-visible { outline: 3px solid white; box-shadow: 0 0 0 6px black; } In addition to providing sufficient contrast against adjacent colors, this outline has other advantages: 1. Because it is provided outside the component, it increases the visual size of the component when it is shown, making it easier to spot. 2. The outline provided using the `outline` property is retained in forced colors modes (like Windows High Contrast Mode) where other visual styles like box-shadows and background colors are overridden by system colors. Here is a live demonstration of this focus indicator. Press the `tab` key on your keyboard to navigate to the focusable elements and show the indicator. See the Pen #PracticalA11y: Universal focus indicator by Sara Soueidan (@SaraSoueidan) on CodePen. If you want to take your focus indicators to the next level and make them _even more_ visible, you can use what designer Erik Kroes calls the “Oreo-focus" indicator. The concept is the same: you use black and white to create a focus indicator, but instead of a white and a black outline, you create two black outlines with a white outline in between (like an Oreo!) This makes the focus indicator even more visible, regardless of what colors you use in your component(s). You may want to use relative units to size your outlines like Erik does. For demonstration purposes, I’m going to use pixels and increase the outline width to 9px to ensure each black line is 3px thick: :focus-visible { outline: 9px double black; box-shadow: 0 0 0 6px white; } Depending on the color theme of your website, you may also flip the order of the black and white outlines to make them even more visible against your components. Here is a live demonstration of this focus indicator. Press the `tab` key on your keyboard to navigate to the focusable elements and show the indicator. See the Pen #PracticalA11y: Universal focus indicator by Sara Soueidan (@SaraSoueidan) on CodePen. You can use either of the outlines we just demonstrated in almost all of your projects. I’ve personally started including them in all my starter CSS files that I reuse across my projects. ## Showing the focus indicator only for keyboard users The main argument I usually hear _against_ focus indicators is that they appear even when you don’t want them to—such as when you click on the component with a mouse or tap on it. Designers and stakeholders are usually not very fond of that. Today, **all modern browsers only show the default focus indicators when they are needed: when navigating the page with a keyboard.** The focus outline doesn’t show up when you click or tap an element; it only shows up when you tab to it with a keyboard. When you provide your own custom focus indicators, you probably want to do the same and only show them for users who need them. You can do that using the `:focus-visible` selector. `:focus-visible` does exactly the same thing `:focus` does, except that it only applies the focus indicator styles to an element when that element receives _keyboard focus_. /* shows the universal focus indicator only when elements receive keyboard focus */ :focus-visible { outline: 9px double black; box-shadow: 0 0 0 6px white; } Now the focus indicator will only be visible to users navigating with a keyboard (or keyboard-like assistive technology), and those who aren’t using a keyboard won’t even know it’s there! Browser support for `:focus-visible` is pretty good — pretty much all modern browsers support it today. If you (still) need to support Internet Explorer, you can use either use the focus-visible JavaScript polyfill, or, if you’re like me and you prefer a progressive enhancement approach that doesn’t rely on JavaScript, you can use `:focus-visible` as an enhancement _in combination with_ `:focus`. In his article about `:focus-visible` and backwards compatibility, Patrick Lauke demonstrates how you can use the `:not()` negation pseudo-class, and (paradoxically) define styles not for `:focus-visible`, but to undo `:focus` styles when it is absent, and then to use `:focus-visible` if you wanted to provide additional stronger styles for browsers that support it. button:focus { /* some exciting button focus styles */ } button:focus:not(:focus-visible) { /* undo all the above focused button styles if the button has focus but the browser wouldn't normally show default focus styles */ } button:focus-visible { /* some even *more* exciting button focus styles */ } The `button:focus:not(:focus-visible)` part is CSS for “when the button receives focus that is not focus-visible”. That is, “when the button receives focus that is not keyboard focus”… then undo all the `:focus` styles. Then apply keyboard-only focus styles using `button:focus-visible`. As Patrick notes, "this works even in browsers that don’t support `:focus-visible` because although `:not()` supports pseudo-classes as part of its selector list, browsers will ignore the whole thing when using a pseudo-class they don’t understand/support, meaning the entire `button:focus:not(:focus-visible) { ... }` block is never applied." I’ll end this section with this paragraph from Patrick’s article (emphasis mine): > If you care about backwards compatibility (and you should, **_until you can absolutely guarantee without any doubt that all your users will have a browser that supports_** **_:focus-visible_**), you will always have to either polyfill or use the combination of `:focus` and `:not(:focus-visible)` (plus optional even stronger `:focus-visible`). ## Outro Focus indicators are one small yet critical addition to your websites that has the ability to improve the usability of your website or application for millions of people who will use it. If you’re a designer, make a habit to design and include focus indicator styles in your design specs if you don’t already do so. And when you’re designing them, aim for maximum visibility and prioritize usability over aesthetics. If you don’t want to design custom focus indicators that match your design theme, you may provide the universal focus indicator we showed earlier. If you’re a developer, include focus styles in your CSS defaults. If you’re working with designers, strike up a discussion about focus styles with them if they don’t already prioritize them in design specs. And use `:focus-visible` to ensure the focus indicators are only shown for the people that need them. ## Resources, references and further reading * Understanding SC 2.4.13 Focus Appearance * Indicating focus to improve accessibility * `:focus-visible` and backwards compatibility * Focusing on Focus Styles * Prevent focused elements from being obscured by sticky headers * The universal focus state * * * _Many thanks toJames Edwards for his review and feedback on this version of the article, and to Alastair Campbell for his review and feedback on the previous version._
www.sarasoueidan.com
September 13, 2025 at 5:24 PM
CSS-only scrollspy effect using scroll-marker-group and :target-current
✨ This post is sponsored by everyone who has bought my Practical Accessibility course. ✨ The _Bootstrap Scrollspy_—now commonly known as just “Scrollspy”—is a feature that automatically updates navigation links based on the user’s scroll position to indicate which link is currently active in the viewport. It is popular because it aims to enhance the user experience by providing visual cues about which part of the content is currently being viewed. Sorry, your browser doesn't support embedded videos. The Scrollspy effect demonstrated in the Bootstrap documentation shows the navbar links are highlighted when their respective target sections are scrolled into view. By default, in-page navigation links (`<a href="">`) don’t get highlighted when their targets are scrolled into view. Historically, the Scrollspy effect has required us to use JavaScript to ‘spy’ on sections of content in a page (such as in an article) and then programmatically update the navigation links and indicate which one is “active”. That typically involved adding a CSS class name (e.g. `.active`) or an HTML attribute (e.g. `data-active`) to style the active link. Today, a new CSS property and pseudo-selector are available that are meant to enable us to create the Scrollspy effect with just two lines of CSS and no JavaScript. ## Scrollspy with CSS scroll markers If you’ve read my previous article discussing the accessibility of CSS-only carousels, then you’re already familiar with the CSS Overflow Module Level 5, and the concept of “scroll markers”. You will also be familiar with the fact that there are CSS-generated scroll markers (`::scroll-marker`), as well as HTML (and SVG) scroll markers (`<a href="">`). If you haven't read the CSS Carousels article yet, I highly recommend you pause here and go give it a read. There's information in there that provides context for some of the technical concepts discussed in this post. In the carousels article, I examined a CSS-only Scrollspy example from the ‘CSS Carousels’ gallery. The Scrollspy example in the Carousels gallery is created using CSS-generated scroll markers. By default, CSS-generated scroll markers are semantically exposed as tabs, not links, which introduced a bunch of usability issues with that pattern that I outlined in the previous post. As I noted in that post: > Instead of using `::scroll-marker`s to implement [the Scrollspy] example, I would instead expect to be able to create a semantic table of contents using an HTML list of `<a href="">`, and then use the `:target-current` pseudo-class to apply active styles to a link (the native scroll marker!) when its target is scrolled into view. […] However, that doesn’t seem to work at the moment. > > Unfortunately, even though the specification states that it "defines the ability to associate scroll markers with elements in a scroller", **the current implementation of the`:target-current` pseudo-class seems to work only for CSS-generated `::scroll-markers`, but not for native HTML ones.** > > Personally, I think `:target-current` is one of the most useful additions to the specification. It’s unfortunate that its current implementation is limited to the new pseudo-elements. Since I published that post, a new property has been proposed and added to the specification. This property is named `scroll-target-group` and it “ _enriches HTML anchor elements functionality to match the pseudo elements one_ ”, which makes it possible to use the `:target-current` selector to highlight links when their respective targets are in view. 🙌🏻 This post is about the `scroll-target-group` property and how to use it with the `:target-current` pseudo-selector to create the Scrollspy effect with CSS. ## Enriching HTML anchors to become scroll markers Using the `scroll-target-group` property (we’ll demonstrate how shortly), you can “promote” HTML anchors to become ‘scroll markers’. When **a group of HTML anchor elements** becomes scroll markers, the browser will run a specific algorithm to determine which anchor in the group is the active anchor, just like it determines the active scroll marker in a group of CSS `::scroll-marker`s. The active scroll marker is determined when its target element is scrolled to an ‘eventual scroll position’ chosen by the browser. According to the specification, the browser chooses an ‘eventual scroll position’ to which the target of a marker (an `<a href="">`) will reach. This ensures that the relevant marker is activated immediately. The active scroll marker then matches the `:target-current` pseudo-class, which you can use to visually highlight the active anchor. **All this requires no JavaScript on our part** , which is pretty impressive. Now, in order to use the `scroll-target-group` property, you will want to use it **not to the anchors themselves, but to an element containing the anchors.** Let’s demonstrate how. ### Using scroll-target-group and :target-current In the CSS Carousels article we talked about how the `scroll-marker-group` property is used to _generate_ a grouping container for a group of `::scroll-marker`s. When you use the `scroll-marker-group` property, both the scroll marker group container and the scroll markers themselves are generated by the browser as CSS pseudo-elements. On the other hand, the `scroll-target-group` property is meant to be a used on _an HTML element_ which _contains_ the HTML scroll markers (the links / anchors). For example, say you have a Table of Contents (TOC) on an article page and you want to style the active link within the TOC when its target section is scrolled into view. To use this property, you’ll want to start by setting up the semantic structure of the links: <nav aria-labelledby="toc-label"> <span id="toc-label" hidden>Table of Contents</span> <ol role="list"> <li><a href="#one">Section One</a></li> <li><a href="#two">Section Two</a></li> <li><a href="#three">Section Three</a></li> <li><a href="#four">Section Four</a></li> <li><a href="#five">Section Five</a></li> </ol> </nav> We have a named navigation landmark that contains an ordered list of anchors pointing to sections of content on the page. These links are, by default, keyboard-operable and they come with default link behavior and accessibility built in. To make these links behave like scroll markers, you will then use the `scroll-target-group` property on their container—this can be the `<ol>` or the `<nav>` element. nav[aria-labelledby=toc-label] { scroll-target-group: auto; } The `scroll-target-group` property specifies whether the element it is used on is **a scroll marker group container.** It accepts one of two values: `none` and `auto`. When the value of `scroll-target-group` is `auto`, "the element establishes a scroll marker group container forming a scroll marker group containing all of the scroll marker elements for which this is the nearest ancestor scroll marker group container.". Now as the user scrolls through the sections of content, the browser will determine which link is currently active. The active link will automatically match the `:target-current` selector, which you can use to highlight the link by giving it distinctive styles within the group: a:target-current { font-weight: bold; text-decoration-thickness: 2px; } ### Live demo Here’s a live example of the CSS Scrollspy effect in action: At the time of writing, you need to use Chrome 140+ to see the HTML scroll markers in action. And here is a Codepen for you to tweak at. Here’s a video recording of the example in action: Sorry, your browser doesn't support embedded videos. #### A note on accessible active anchor styling WCAG Success Criterion 1.4.11 Non-text Contrast Level AA requires that **visual information required to identify user interface components and states** (except for inactive components or where the appearance of the component is determined by the user agent and not modified by the author) **has a contrast ratio of at least 3:1 against adjacent color(s)**. If you’re using color alone to visually indicate the active link, make sure the color contrast is high enough to make the color change discernible by people with vision disabilities. I recommend that you don’t rely on changing the text or background color of the link alone to indicate that it is active, as these colors will be overridden in forced colors modes like Windows Contrast Themes. So the active link styles may no longer be conveyed to the user, _unless_ you use the `forced-colors` feature query and system color keywords to adapt the colors of the active link to the user’s chosen color scheme. This is outside the scope of this article. I personally prefer adding a border, an outline, or an underline, or increasing the thickness of the text (or a combination of any of these) to highlight the active link. So, using just a couple of lines of CSS, you can now create a scrollspy effect without needing a single line of JavaScript. One more thing! Usually when you load a page that has a fragment identifier in the URL, the browser scrolls the page to the ‘target element of the document’ and you can use the `:target` pseudo-class to apply custom highlight styles to that element. Until today, there hasn’t been a way to style/highlight the (in-page) _link_ that points to that target element. Today, if a link is a scroll marker, the `:target-current` styles will be automatically applied to the link when the browser scrolls to the document’s target element. This means that you can now combine `:target` and `:target-current` to style the target element identified in a URL fragment identifier _and_ the link to that element. ## The semantic accessibility of HTML scroll markers HTML anchor elements (`<a href="">`) come with default link accessibility and behavior built into them: they are exposed as `link`s to screen readers, and they come with keyboard interactions built into them by the browser. But an `<a href="">` element does not come with a built-in way to communicate that it is “active”, or that it is “the current” link within a group of links. A scroll marker, by definition, has a meaningful purpose: it lets the user know which part of content is currently being viewed. What this means is that when you visually highlight an active link, you’re communicating meaningful information to the user. To ensure that you are not excluding any of your users, you want to make sure that this information is communicated to _all_ your users, including screen reader users. **This is a baseline accessibility requirement.** WCAG Success Criterion 1.3.1 Info and Relationships (Level A) states that "information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text." So, how do you communicate that a link is active to screen reader users? How do you provide the same meaningful affordance that you’re creating with CSS to someone who can’t see? Since there is no native HTML way to indicate that a link within a group is currently “active”, you can use ARIA to communicate this information. Affordance is “the quality or property of an object that defines its possible uses or makes clear how it can or should be used”. As I mentioned in the CSS Carousels accessibility post, ARIA is similar to CSS in that it creates user interface affordances to the assistive technologies that rely on it. Using CSS, we provide visual affordances to our interfaces (using color, layout, spacing, and more). ARIA provides what “ _semantic_ affordances” to screen reader users. ARIA attributes “paint” a (non-visual) “picture” of the page to screen reader users. To indicate which anchor is currently active, ARIA provides a conveniently-named attribute: `aria-current`. > [The `aria-current` state attribute] indicates the element that represents the current item within a container or set of related elements. … The `aria-current` attribute is used when an element within a set of related elements is visually styled to indicate it is the current item in the set. Setting `aria-current` to `true` on the “active” anchor ensures that screen reader users get the same information that sighted users get about which part of the content is currently shown. <nav aria-labelledby="toc-label"> <span id="toc-label" hidden>Table of Contents</span> <ol role="list"> <li><a href="#one">Section One</a></li> <li><a href="#two" aria-current="true">Section Two</a></li> <li><a href="#three">Section Three</a></li> <li><a href="#four">Section Four</a></li> <li><a href="#five">Section Five</a></li> </ol> </nav> You may already be familiar with this attribute as you may already using it to indicate the active link within your website navigation. `aria-current` accepts one of seven values. For website navigation, the `page` value is appropriate as it indicates **the current _page_** on the website. For nested or in-page navigation links, the `true` value is sufficient to indicate the current link within the group. When the user scrolls through the sections of content and the active link is visually highlighted, this link must have `aria-current=true` set on it. Because the purpose of the `scroll-target-group` property and the `:target-current` selector is to allow us to create JavaScript-_free_ native HTML scroll markers, we should expect the browser to add and manage the necessary ARIA attribute(s) required for scroll markers to be inclusive. (After all, that’s the whole premise of this feature: to write a few lines of CSS and let the browser handle all the behavior for us.) However, at the time of writing of this post, Chrome (currently the only browser that has implemented this feature) doesn’t add `aria-current=true` to the active anchor yet. If you inspect the accessibility information of the links in the demo from the previous section you can see that the state of the active anchor is not communicated to assistive technologies when the anchor becomes active (see screenshot below). This is unfortunately akin to some of the accessibility issues with CSS Carousels that I discussed in the previous post. I filed a Chromium issue and I’m hoping this will be resolved soon enough to make this feature usable. I will update this post when the issue is resolved. **Update (2025-08-19)** : In response to the issue I filed a couple of days ago, there is now an active, work-in-progress patch to set `aria-current` for :target-current html anchor element. If you want to use this feature as a progessive CSS enhancement today, keep in mind that you _will_ , for the time being, need to use JavaScript to add `aria-current` to the active anchor when its corresponding target scrolls into view, **otherwise you risk an instant WCAG 1.3.1 violation.** I’ll personally wait till the issue is resolved and the feature becomes ready for production. When it does, I’ll be among the first to add it as an enhancement in my CSS. ✌🏻
www.sarasoueidan.com
September 13, 2025 at 5:24 PM
The CSS prefers-color-scheme user query and order of preference
I spent some time in Reeder app this morning, catching up with RSS and the latest articles published by my favorite blogs. I was reading Scott O’Hara’s article about using JavaScript to detect high contrast and dark modes, which includes a small, very useful script to do exactly what the title says. The output of that script at first looked like it was a “false positive”. But some further investigation led me to learn something new about the `prefers-color-scheme` CSS user query. Scott’s article includes a Codepen to demonstrate the output of the script. The script will check and detect if you currently have high contrast mode or dark mode enabled, and will output the result of the check. See the pen (@scottohara) on CodePen. Since JavaScript doesn’t run in Reeder app, I clicked to open the original article on Scott’s Web site. That’s when I saw that the script was reporting that I had dark mode ON, even though I don’t have dark mode enabled on my phone. Having just recently updated to iOS 15, my first thought that this might be a browser/OS bug or something. But then it hit me: I _do_ have dark mode enabled… _in Reeder app_. (Reeder has a nice dark mode which I enjoy reading in.) This instantly led me to question whether the media query was picking up _that_ dark mode, instead of the OS-level preference. When I opened the article on Scott’s Web site, I opened it in Reeder’s in-app browser. Which means that the script was running in that context when it reported that dark mode was ON. So to test my assumption further, I opened the article in iOS Safari, which is running in the Light scheme mode (set on the OS-level). The script does not report that dark mode is ON in that context. In order to confirm this behavior, I checked the results of the test in Reeder app on my Mac, which is running dark mode on OS-level. I toggled the theme in Reeder app between Light and Dark to verify the results. Sure enough, the script detected dark mode ON when the app theme was set to Dark, but not when the app theme was set to Light. The `prefers-color-scheme` media query picks up the dark mode set in the app. Note that dark mode is also enabled on the OS level, but the media query is picking up the color theme from the app context. App color theme taking precedence over OS-level theme. Even though dark mode is enabled on the OS level, the `prefers-color-scheme` media query picks up the light mode set in the app when the app’s theme is the classic light. In an attempt to verify whether this was a bug or a feature, I checked the specification. The spec includes these two paragraphs: > The method by which the user expresses their preference can vary. It might be a system-wide setting exposed by the Operating System, or a setting controlled by the user agent. […] User preferences can also vary by medium. […] UAs are expected to take such variances into consideration so that prefers-color-scheme reflects preferences appropriate to the medium rather than preferences taken out of context. That explains it. **UA preference > OS-level preference.** Something to keep in mind for when an “unexpected behavior” happens. A good reminder to always test and check the specifications. Had this not been in the spec, then further investigation might have led to an existing bug report or to the creation of one. Who knows. * * * And _that_ was my first #TIL moment of the day. **Stay curious.** (Oh and also: **RSS is awesome.** Thank you to everyone providing an RSS feed for their content. _You_ are awesome.)
www.sarasoueidan.com
September 13, 2025 at 5:23 PM
In Quest of Search
**Update:** There now exists a native HTML `<search>` element that maps to the ARIA `search` role. 🎊 As of March 24th, 2023, the HTML specification added a new grouping element: The `<search>` element. Read more about the element in Scott’s introductory blog post. There’s been a recent discussion on Twitter about the idea of adding a new element in HTML that represents a search interface. A search form, basically. The idea is to create a semantic HTML element for the ARIA `search` role, which represents a landmark region “that contains a collection of items and objects that, as a whole, combine to create a search facility.” Opinions have been shared in the Twitter thread about whether adding a new HTML element is necessary. Many have argued that it was unnecessary because we can use the ARIA `search` role and repurpose a `form` element to create the same semantics. I disagree. And this article is the longer version of **my personal opinion** on the subject. ## tl;dr I do strongly encourage the addition of a new HTML element that represents—and can consequently obviate the use of—the ARIA `search` landmark role. A search element would provide HTML parity with the ARIA role, and encourage less use of ARIA in favor of native HTML elements. The suggested element would be syntactic sugar for `<div role="search">` like `<main>` is syntactic sugar for `<div role="main">`. This means that it would an HTML sectioning element, not a replacement for another element. I would choose `<search>` as a name for that element. In my mind, `<search>` would be to `role="search"` what `<nav>` is to `role="navigation"`. But any other appropriate name would, of course, also work. The rest of this article is my reasoning for encouraging the idea of adding a semantic HTML element for search. ## HTML and ARIA landmark roles The ARIA specification includes a list of ARIA **roles** that are used to define regions of a page as landmarks: * `banner` * `complementary` * `contentinfo` * `form` * `main` * `navigation` * `region` * `search` HTML currently contains 112 elements. Eight of those elements are sectioning elements: `main`, `nav`, `aside`, `header`, `footer`, `article`, `section`, `form`. Seven of these HTML sectioning elements are mapped to ARIA landmarks, which are used by assistive technologies (ATs). * `header` is the HTML native equivalent for ARIA’s `role="banner"` (when it is scoped to the `body` element. See HTML Accessibility API Mappings for more information.) * `footer` is the HTML native equivalent for ARIA’s `role="complementary"` (also in the context of the `body` element) * `nav` is the HTML native equivalent for ARIA’s `role="navigation"` * `main` is the HTML equivalent for ARIA’s `role="main"` * `form` is the HTML equivalent for ARIA’s `role="form"` * `aside` is the HTML equivalent of ARIA’s `role="complementary"` * `section` is the HTML native equivalent for ARIA’s `role="region"` (when it has an accessible name) It is because these elements exist that we often don’t need to use ARIA’s equivalent roles (unless we absolutely _have_ to repurpose another element using those roles, or expose an element to ATs when it is outside of its expected context). If `<nav>` exists, why should a `<search>` (or whatever other name it gets) not? If `<search>` is to be deemed unnecessary because `role="search"` exists, wouldn’t this also mean that `<nav>` (and other landmark elements) would be considered _redundant_ because `role="nav"` (and other ARIA roles) exists? ## HTML and ARIA landmarks, beyond semantics ARIA landmark roles are roles assigned to regions of a page that are intended as **navigational landmarks**. Using ARIA landmarks (or their equivalent native HTML elements when they exist) is meant to also facilitate user navigation. From the W3C WAI-ARIA Editor’s Draft: > Assistive technologies SHOULD enable users to quickly navigate to elements with role search. User agents SHOULD treat elements with role search as navigational landmarks. User agents MAY enable users to quickly navigate to elements with role search. When HTML sectioning elements (and/or ARIA landmark roles) are appropriately used on a page, assistive technology users such as screen readers users could use those landmarks to navigate the page more efficiently, allowing them to jump to the area of the page that they want. For example, if the `<nav>` element (or, equivalently, the `role="navigation"` ARIA role on a qualifying element) is used to wrap a page’s navigation, the navigation shows up in the VoiceOver Rotor on macOS. Similarly, using the `main` element will make the main section of the page show up in the landmarks menu. The user can then quickly jump straight to the navigation section or to the main content area of the page if they want to, bypassing other regions of the page. This increases the user’s efficiency and improves their navigation experience. Similarly, when you use `role="search"` on a `form` element, that form will show up as a search region in the landmarks menu. The user can then jump to the search form if they need to quickly search for something. The search form on WebAIM's Web site shows up in the Landmarks menu by VoiceOver on macOS because `role="search"` ARIA role is present on the `form` element. The search form on Smashing Magazine's Web site is not recognized as a search landmark by VoiceOver on macOS because `role="search"` ARIA role is absent on the `form` element. _If HTML sectioning elements are used without understanding the associated landmark structure, assistive technology users will most likely be confused and less efficient in accessing content and interacting with web pages._ ### But is a native search landmark worth it? Yes, it is. Search is one of the most common and most used sections of many Web sites. Of course, a “It Depends” is warranted here, too. Depending on the Web site, search might be the first thing a user looks for and uses on a given site. E-commerce Web sites are a great example of where search forms are essential and heavily used. Educational and documentation sites are another example. Take MDN, for example. Search is so important and on MDN that the site even includes a Skip Link that enables keyboard users to skip straight to the search field. Now I don’t have any user research data or anything, but I would assume that the skip link was added because of how frequently users reach for the search field to look up documentation about specific topics they’re searching for. ## Just because an ARIA role exists, it doesn’t eliminate the usefulness of a native HTML equivalent I’ll just say it again: ust because an ARIA role exists, it doesn’t eliminate the usefulness of a native HTML equivalent. ## The purpose of ARIA …is to provide parity with HTML semantics. It is meant to be used to **fill in the gaps** and provide semantic meaning where HTML falls short. ARIA is **not meant to _replace_ HTML.** If anything, the need to use ARIA as ‘polyfill’ for HTML semantics could be considered as a sign and a constant reminder of the fact that HTML falls short on some semantics that benefit users of assistive technologies. This is due to the lack of native HTML elements that provide the meaning (and sometimes, by extension, the behavior) that these ATs need to convey to their users. If we can get an HTML element that fills a part of the gap, it’s only going to be a win—no matter how small of a win it might seem. > > ARIA is not meant to replace HTML > > this! In fact, I think we might want it to go the other way around, with HTML replacing ARIA bit by bit until its services are no longer required > > — Hidde (@hdv) September 15, 2021 ## The first rule of ARIA The first rule of ARIA use in HTML states that you should **avoid using ARIA if there is a native HTML element with the semantics of behavior that you require already built in.** If such an element exists, you should reach for that element instead. This means that ARIA should be **a second resort, not a first approach.** By providing HTML elements that are implicitly mapped to ARIA roles, we can encourage the use of proper HTML markup to convey semantic meaning, and spread more awareness to help avoid both overuse and misuse of ARIA in general. If we can get an HTML element that enables us to use ARIA less, then that element should, in my opinion, be a welcomed addition. ## Outro A native search element might feel like a _small_ technical win to many, but the consistency it provides, the HTML semantics gap it fills, and the awareness it could potentially help spread would all make it a useful and welcomed addition. 112 to 113 HTML elements? I hope so.
www.sarasoueidan.com
September 13, 2025 at 5:23 PM