<playground-ide editable-file-system="" line-numbers="" resizable=""> <script type="sample/html" filename="index.html"> <!doctype html> <body> Hello <script type="module" src="./index.js"></script> </body> </script> <script type="sample/js" filename="index.js"> document.body.appendChild(document.createTextNode("World!")) </script> </playground-ide> <aside role="note" class="sponsorship"> <p>✨ This post is sponsored by everyone who has bought my <a href="https://practical-accessibility.today/">Practical Accessibility course</a>. ✨</p> </aside> <p>“CSS Carousels” were formally introduced a few weeks ago in <a href="https://developer.chrome.com/blog/carousels-with-css">an article on the Chrome for developers blog</a>, and quite a few people have shared the excitement since then.</p> <p>When I first heard of them I was very reluctant to jump on the bandwagon of excitement. I will also admit that there was even a small part inside of me that was <em>terrified</em> by the idea. Not only does creating interactive widgets using CSS violate the principle of Separation of Concerns (of which I am an advocate), but also because <strong>pretty much every implementation of a CSS-only widget I have seen before has had at least moderate to major accessibility issues.</strong></p> <p>But because the introductory article mentioned that <q>carousel best practices are handled by the browser</q>, and that <q>it’d be very difficult to make a more accessible carousel than this</q>, I was curious to learn more about them so that I could form a more objective and informed opinion on them. (After all, I’m also a developer and convenience sounds appealing to me too.)</p> <p>So I did. I read the CSS specification and inspected the examples.</p> <p>In this post, I want to share my findings from examining the accessibility and usability of “CSS Carousels”.</p> <aside role="note"> <p><strong>Important note:</strong> This post is intended to be an objective look at (the current state of) “CSS Carousels”. It is not intended to shame or call anybody out.</p> <p>It is also not intended as formal feedback to the team implementing these features. (Though, it definitely can be.)</p> <p>As a developer not directly involved in implementing CSS Carousels, what I want you to take away from the post isn’t how to fix CSS carousels. It’s not your job to fix them (nor should it be).</p> <p>Rather, the purpose of this post is to <strong>spread accessibility awareness so that the new CSS features are not used prematurely</strong>, resulting in confusing experiences for assistive technology users. So it is primarily aimed at developers who want to <em>understand why</em> CSS Carousels are not accessible.</p> <p>So, let’s get started.</p> </aside> <p>All the examples of CSS Carousels I’ve seen on the wild are based on the same reference—namely the <a href="https://chrome.dev/carousel/horizontal/list/">CSS Carousels gallery</a>. So we will be examining a few examples from the gallery to better understand how the new features work and how they affect the accessibility of HTML.</p> <p>As I mentioned earlier, this stuff is still highly experimental. At the time of making of this post, it is only supported in Chrome Canary behind a flag. I will include video recordings of the examples that I am going to examine, so you don’t need to have Chrome Canary installed unless you want to try the examples out for yourself.</p> <p>Let’s start by first defining what CSS Carousels are.</p> <h2 id="what-are-css-carousels%3F" tabindex="-1">What are CSS Carousels?</h2> <p>“CSS Carousels” is an umbrella name for a collection of JavaScript-free, CSS-only implementations of common <strong>scrolling UI patterns</strong>—mainly patterns like sliders and carousels—that are implemented using new features defined in <a href="https://www.w3.org/TR/css-overflow-5/">the CSS Overflow Module 5 specification</a>.</p> <p>You can find examples of these implementations in the <a href="https://chrome.dev/carousel/horizontal/list/">“CSS Carousel gallery”</a> that we will be examining in this post.</p> <p><a href="https://www.w3.org/TR/css-overflow-4/">The CSS Overflow specification Module (Level 4)</a> specifies CSS features for handling scrollable overflow. When an element has “too much” content for its size, the content “overflows”, and CSS provides features that allow you to handle this overflow—by making the element scroll in either or both directions, for example, or by clipping the overflow, truncating it, and so on and so forth.</p> <p><a href="https://www.w3.org/TR/css-overflow-5/">The Level 5 of this specification</a> (which is currently still a Working Draft) defines a set of <strong>pseudo-elements that are designed to provide specific visual and interactive affordances for scroll containers</strong>.</p> <p>More specifically, according to the specification, this module:</p> <blockquote> <p>“defines the ability to associate scroll markers with elements in a scroller (or generate them automatically as <code>::scroll-marker</code> pseudo-elements, with automatic user behavior and accessible labels), which can be activated to scroll to the associated elements and reflect the scroller’s relative scroll progress via the :target-current` pseudo-class.”</p> </blockquote> <p>Let’s break this down a little bit.</p> <p>The specification “defines the ability to associate <a href="https://www.w3.org/TR/css-overflow-5/#scroll-marker">scroll markers</a> with elements in a scroller”…</p> <blockquote> <p>A scroll marker is any element or pseudo-element with a <a href="https://www.w3.org/TR/css-overflow-5/#scroll-target">scroll target</a>.</p> <p>The <a href="https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element">HTML <code><a></code> element</a> and <a href="https://www.w3.org/TR/SVG2/linking.html#Links">SVG <code><a></code> element</a> are scroll markers… […] While these navigational links can be created today, there is little feedback to the user regarding the current content being viewed…</p> </blockquote> <p>For example, think of a sticky Table of Content (TOC), where a link is highlighted when the link’s target section scrolls to the top of the viewport. You can see an example of such a TOC on <a href="https://web.dev/blog/interop-2025?hl=en">the web.dev blog</a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSSoverflow/CSScarousels">on MDN guides</a> as well. The active link changes based on which section is scrolled into the viewport.</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/web.dev-toc.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption>The TOC on the web.dev blog highlights the currently-active link as the link's target section scrolls to the top of the viewport.</figcaption> </figure> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/mdn-toc.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption>The TOC on the MDN blog also highlights the currently-active link as the link's target section scrolls to the top of the viewport.</figcaption> </figure> <p>The links in these tables of content are scroll markers. They <em>mark</em> the scroll position of their target sections. When a link’s target section scrolls to the top of the page, the link is styled to reflect the current scroll position of the section, and to indicate that the section is currently “active”.</p> <p>We’ve always resorted to using JavaScript to style these links when their respective sections are scrolled into view.</p> <p>If you inspect the active links on the web.dev blog in the Chrome DevTools, you can see that a specific class name is added to a link when it becomes “active”. This class name is used to apply active styles to the link in CSS.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/web.dev-active-lnks-class.png" alt="A screenshot of the web.dev article shown in Chrome with the Chrome DevTools open. The Styles panel shows the active styles applied to the currently-active link that has a class name 'devsite-nav-active'." /> <figcaption>Active links on the web.dev blog are styled by adding the <code>devsite-nav-active</code> class name to the links when their corresponding target sections scroll to the top of the viewport. This class name is added via JavaScript.</figcaption> </figure> <p>So, the premise of not requiring JavaScript to style these links and instead take advantage of CSS’s new pseudo-selectors sounds really great! (The <code>:target-current</code> selector in particular is supposed to enable this. More on this later.)</p> <p>Next, the specification specifies that it <q>adds a mechanism for creating groups of scroll markers, and for automatically creating <code>::scroll-marker</code> pseudo-elements</q>, and that <q>within each group, the active marker reflects the current scroll position, and can be styled to give the user an indication of which section they are in.</q></p> <p>In other words, this specification defines a mechanism that allows you to (1) create a group of scroll markers for a scroll container, where each of the individual scroll markers in the group corresponds to an item in the scroll container, and (2) these markers can be styled to indicate the scroll position within the container.</p> <p>But scroll markers, by nature, are interactive elements. So the purpose of this specification is to enable CSS to create interactive pseudo-elements (not real elements because CSS can’t do that, nor is it intended to).</p> <p>This is where things start to become concerning.</p> <p>Before we discuss why, let’s first get a quick overview of how the scroll markers are created.</p> <h3 id="a-(very-quick)-overview-of-how-to-create-scroll-markers-in-css" tabindex="-1">A (very quick) overview of how to create scroll markers in CSS</h3> <p>This post is not a tutorial. Since the announcement of CSS Carousels, a few tutorials have been written about the topic, including an <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSSoverflow/CSScarousels">MDN guide for Creating CSS Carousels</a>.</p> <p>But for the purposes of completeness of this post, here’s a high-level, bird’s eye view of how it works:</p> <p>Say you have a scroller element containing a series of items. For example, say you have a list of images in a horizontally-scrolling container. And say you want to create a list of “dots” for this container that provide a visual indicator of how many images there are in the list and that indicate which item in the list is currently “active”. These dots are also interactive and can be used to scroll their target images into view.</p> <p>And say that, for <em>some</em> reason, you don’t want to create these indicators in HTML but rather want to create them using CSS instead. (I won’t judge. Yet.) This is the kind of thing that this level of the Overflow specification aims to enable.</p> <p>You can use the new <code>scroll-marker-group</code> property defined in the specification to instruct the browser to create a grouping container for these dots:</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">ul.scroller</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">scroll-marker-group</span><span class="token punctuation">:</span> after<span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>The property accepts three values: <code>none</code>, <code>before</code>, and <code>after</code>.</p> <p>The <code>before</code> and <code>after</code> values indicate whether you want to show the scroll markers before or after the items in the scroll container. If you want the dots to appear before the list of items in the scroller, you use the <code>before</code> value.</p> <p><strong>The scroll marker group is created <em>inside</em> the list in the form of a pseudo-element: <code>::scroll-marker-group</code>.</strong></p> <p>The <code>::scroll-marker-group</code> pseudo-element is a fully-styleable element (i.e. you can use any CSS property to style it), and it implicitly behaves as a single focusable component, establishing a <code>focusgroup</code>. This means that <strong>the group takes up only one tab stop on the page.</strong> To navigate through the scroll markers inside the group, you can use the Arrow keys.</p> <aside role="note"> <p>This is similar to how a group of radio buttons that share the same <code>name</code> establish only one tab stop on the page, and where you can navigate through the radio buttons using Arrow keys to select the one that you want.</p></aside><p></p> <p>The <code>::scroll-marker-group</code> pseudo-element is a <em>container</em> for its contained <code>::scroll-marker</code> pseudo-elements (these would be the “dots” corresponding to the images in the list).</p> <p>To create a scroll marker for the items in the list, you can use the <code>::scroll-marker</code> pseudo-element on the list items. Like other pseudo-elements, this element will not be rendered if the <code>content</code> property is not declared:</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">ul.scroller</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">scroll-marker-group</span><span class="token punctuation">:</span> after<span class="token punctuation">;</span></span><br /><span class="highlight-line"> </span><br /><span class="highlight-line"> <span class="token selector">> li::scroll-marker</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> ... / ...<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>Even though the scroll markers are prepended to the list items in the markup, they are (according to the specification) “collected into the <code>::scroll-marker-group</code>” so that they can be exposed as a group to assistive technologies.</p> <aside role="note"> <p>This means that a part of what the browser (Chrome at the moment) is doing behind the scenes is “re-slotting” the scroll markers into the scroll marker group, even though they are not direct children of the group in the markup. They are, after all, <em>pseudo</em>-elements.</p> </aside> <!-- <aside role="note"> It is worth highlighting here that you will will want to use other new CSS features like anchor positioning and scroll snap features to position the group of markers relative to the scroll container and to create the expected scrolling behavior on the group and within the scroll container. </aside> --> <p>Now, <strong>the <code>::scroll-marker</code>s that the browser creates are interactive elements</strong>. Activating a scroll marker will scroll its corresponding item into view.</p> <p>And the <code>:target-current</code> selector can be used to style the currently-active <code>:scroll-marker</code> when its corresponding target is shown. For example:</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">ul.scroller</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">scroll-marker-group</span><span class="token punctuation">:</span> after<span class="token punctuation">;</span></span><br /><span class="highlight-line"> </span><br /><span class="highlight-line"> <span class="token selector">> li::scroll-marker</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> ... / ...<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token selector">> li::scroll-marker:target-current</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">/* style active marker /</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>Unlike native scroll markers, though, these are not links. These are <em>interactive pseudo-elements</em>. More on this later.</p> <p>The specification also defines the <code>::scroll-button()</code> pseudo-element, which you can use on the scroll container to add (you guessed it!) <em>scroll buttons</em>.</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">ul.scroller</span> <span class="token punctuation">{</span></span><br /> <span class="token selector">...<br /><span class="highlight-line"></span><br /> &::scroll-button(left)</span> <span class="token punctuation">{</span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> ...<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> </span><br /><span class="highlight-line"> <span class="token selector">&::scroll-button(right)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> ...<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>These pseudo-buttons are also fully styleable elements: there is no restriction on what properties you can apply to them. And you can even use the <code>:disabled</code> pseudo-class to apply disabled state styles to the <code>::scroll-button()</code>s when they are disabled.</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">ul.scroller</span> <span class="token punctuation">{</span></span><br /> <span class="token selector">...<br /><span class="highlight-line"></span><br /> &::scroll-button(left)</span> <span class="token punctuation">{</span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> ...<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"> </span><br /><span class="highlight-line"> <span class="token selector">&::scroll-button(right)</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> ...<span class="token punctuation">;</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">/ focus styles /</span></span><br /><span class="highlight-line"> <span class="token selector">&::scroll-button():focus-visible</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">/* focus styles here /</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"></span><br /><span class="highlight-line"> <span class="token comment">/ disabled state styles /</span></span><br /><span class="highlight-line"> <span class="token selector">&::scroll-button():disabled</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token comment">/* disabled state styles here /</span></span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>As we mentioned earlier, unlike the <code>::before</code> and <code>::after</code> pseudo-elements which are <em>static</em> text elements, the <code>::scroll-marker</code>s and <code>::scroll-button()</code>s are interactive elements.</p> <p><strong>Interactive elements have specific accessibility requirements.</strong> They should have <strong>meaningful roles and descriptive names that identify their purpose</strong>, so that the user knows what to expect when they interact with them.</p> <p>So, at this point there are quite a few questions we should be asking:</p> <ul> <li>Do these scroll markers meet the accessibility requirements for interactive elements?</li> <li>How are the scroll markers exposed to assistive technology users like screen reader users?</li> <li>What roles is the browser exposing for these interactive pseudo-elements?</li> <li>Do they provide meaningful semantics to the user to help them understand what they are interacting with?</li> <li>Does the browser give them accessible names? The specification states that it “defines the ability to associate scroll markers… or generate them automatically as <code>::scroll-marker</code> pseudo-elements, <strong>with automatic user behavior and accessible labels</strong>” (emphasis mine). So how are these markers labelled?</li> </ul> <p>We can only find the answer to these questions in the HTML markup, which the browser uses to create the accessibility tree.</p> <h2 id="scroll-markers%3A-from-css-to-html-to-the-accessibility-tree" tabindex="-1">Scroll markers: From CSS to HTML to the accessibility tree</h2> <p>Semantic HTML is the foundation of accessibility on the Web.</p> <p>Semantic HTML carries <strong>meaning</strong>. Assistive technologies (AT) like screen readers (SR) rely on the meaningful semantics in HTML to present Web content to their users and to create an interface for navigating that content.</p> <p>But <strong>HTML is only as accessible as you write it</strong>. Even semantic HTML can be “inaccessible” if you don’t write it as it is intended.</p> <p>For example, many elements are only meaningful when they are children of other elements, or when they are associated with other elements. If you don’t use these elements as intended, then they will lose their meaning and they won’t be as useful to screen reader users anymore.</p> <p>So there are certain “rules” that you should follow to ensure that you get the most out of HTML’s inherent accessibility.</p> <p>HTML provides many meaningful, semantic elements that represent various types of content & interactive controls. And <strong>using those elements is critical for describing the purpose of your content to assistive technology users</strong>.</p> <p>But there are still some more complex components that don’t yet have meaningful elements in HTML to represent them. Until these elements exist, we can use ARIA to create them.</p> <p>Think of ARIA as a polyfill for HTML semantics. It provides <strong>additional attributes</strong>—roles, states, and properties—that allow us to create complex, interactive components that do not yet have native equivalents in HTML. Using ARIA attributes we can describe these UI components to assistive technologies like screen readers.</p> <aside role="note"> <p>I like to think of ARIA as being similar to CSS in that it creates user interface affordances to the assistive technologies that rely on it. (If you’ve taken my Practical Accessibility course then you’ll remember me saying this before.)</p> <p>Affordance is “the quality or property of an object that defines its possible uses or makes clear how it can or should be used”.</p> <p>Using CSS, we provide <em>visual</em> affordances to our interfaces (using color, layout, spacing, and more).</p> <p>ARIA provides what I like to call “<em>semantic</em> affordances” to screen reader users. ARIA attributes “paint” a (non-visual) “picture” of the page to screen reader users.</p> </aside> <p>So, together, HTML and ARIA provide important accessibility information to screen readers, without which the content of the page would not be perceivable, operable, or understandable by their users.</p> <p>This is why <strong>it is <em>always</em> critical to understand how a CSS feature may affect the accessibility information created in HTML</strong>.</p> <p>The CSS Overflow Module aims to define a set of features that provide visual affordances to scrollable containers <em>by creating and appending new interactive elements (the scroll markers) into the HTML markup of the page</em>. As such, we <em>must</em> expect these elements to affect the accessibility information exposed to assistive technologies like screen readers.</p> <p>The <code>::before</code> and <code>::after</code> pseudo-elements <em>already</em> do affect the accessibility information of an element because the contents of these elements are exposed to screen readers, and they contribute to <a href="https://www.w3.org/TR/accname-1.1/#mappingadditionalndte">the accessible name computation</a> of the element they are created on.</p> <p>Scroll markers will affect the accessibility information differently because they are also interactive, which means that <strong>they are expected to expose roles, states, and other properties, depending on the type of element that they are exposed as.</strong></p> <aside role="note"> <p>If you want to learn more about how the <code>::before</code> and <code>::after</code> pseudo-elements affect the accessibility information of a page, check out my CSS Day talk from last year: <a href="https://www.youtube.com/watch?v=obMqXeDVE">“The Other C in CSS” on Youtube</a>.</p> </aside> <p>Now, to check the accessibility information of the page, we can inspect the page’s accessibility tree.</p> <p><strong>The accessibility tree (“accTree”) is a tree of objects (similar to the DOM tree), each object containing accessibility information about an element on the page.</strong> Not <em>all</em> elements are represented in the accTree because not all elements are relevant for accessibility. Only a <em>meaningful</em> element that is not hidden (using HTML or CSS) is exposed in the accessibility tree.</p> <p>The information the browser exposes about an element depends on the nature of the element (like whether it’s interactive or not).</p> <p>Typically, there are four main pieces of information that the browser exposes about an element in the accTree:</p> <ol> <li><strong>The element’s role</strong> (What kind of thing is it?). The role of an element identifies its purpose. It lets a screen reader user know <strong>what something is, which is also an indication of how that thing is to be used.</strong></li> <li><strong>The element’s name</strong> (a.k.a the accessible name, “accName”). The names identifies an element within an interface and in some cases helps indicate what an element does.</li> <li><strong>The element’s description</strong> if it has one. For example, a text input field may have a short description of what the expected input looks like.</li> <li><strong>The element’s state</strong> when it has one. For example, is the button pressed? is the checkbox checked, unchecked, or undetermined?</li> </ol> <p>The accessibility tree also exposes any <strong>properties</strong> that the element may have (such as if a button is focusable or disabled), as well as any <strong>relationships</strong> with other elements (like if the element is part of a group, or if is it labelled by or described by another element).</p> <p>The information exposed in the accessibility tree is very useful to us as developers because it gives us insights into how our content will be exposed to and presented by screen readers.</p> <p><strong>Knowing how the browser exposes scroll markers to the user allows you to check whether the information being exposed is hepful to the user’s understanding of the page or not</strong>, and it allows you to test whether your component meets the expectations the user has based on that information.</p> <p>Importantly, how scroll markers are exposed <strong>in the context they are used in</strong> will be <em>critical</em> to determining how they affect the accessibility and the usability of your components.</p> <h3 id="quick-refresher%3A-inspecting-css-scroll-markers%E2%80%99-accessibility-information-in-the-browser-devtools" tabindex="-1">Quick refresher: Inspecting CSS scroll markers’ accessibility information in the browser DevTools</h3> <p>We can inspect how the browser is exposing an element in the accTree using the browser DevTools.</p> <p>When you open the Chrome DevTools, you can find the accessibility information of an element under <strong>Accessibility</strong> panel. You will find the Accessibility panel on the right side of the <strong>CSS Styles</strong> panel.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/accTree-devtools.png" alt="Screenshot of the Chrome DevTools showing the accessibility panel on the right side of the CSS Panel." /> </figure> <p>In addition to inspecting the accessibility object for each element in the Accessibility Panel, Chrome also provide a <strong>full-page accessibility tree</strong> view. To use it, in the Accessibility tab, check the “Enable full-page accessibility tree” option.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/accTree-devtools-checkbox.png" alt="Screenshot of the Chrome DevTools highlighting the Enable Full-page accessibility tree checkbox." /> </figure> <p>(First-time only) click the “Reload devtools to enable feature” button at the top of the DevTools.</p> <p>Then, in the <strong>Elements</strong> tab, click “Switch to accessibility tree view” button in the top right corner.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/accTree-devtools-btn.png" alt="Screenshot of the Chrome DevTools highlighting the Switch to accessibility tree view” button in the top right corner of the Elements panel." /> </figure> <p>Now, the full-page accessibility tree replaces the DOM tree in the panel, and element names, roles, values and states are shown in an easy-to-read, and very practical hierarchal tree view.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/accTree-devtools-full-view.png" alt="Screenshot of the Chrome DevTools showing the full-view accessibility tree in the Elements panel." /> </figure> <p>This view gives you an overview of how the contents of the entire page are exposed. We’re going to use this tree view to understand the CSS Carousel examples better.</p> <p><svg role="separator" style="display: block; max-width: 65%; margin: 4rem auto;" width="500px" height="32px" viewBox="0 0 794 51" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <polygon id="path-1" points="0.907103825 0 798.907104 0 798.907104 364 0.907103825 364"></polygon> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd"> <g id="36257-O0KTX6" transform="translate(0.000000, -212.000000)"> <g id="Group" transform="translate(-4.907104, 171.000000)"> <g id="Group-70"> <mask id="mask-2" fill="white"> <use xlink:href="#path-1"></use> </mask> <g id="Clip-67"></g> <path d="M610.198586,77 C699.286076,77 783.215114,73.4231956 849.907104,64.0083772 L849.765531,63.0342269 C783.152303,72.4392054 699.257163,76.0169938 610.198586,76.0160098 C468.900459,76.0169938 314.600519,67.0124993 195.629099,58.0080049 C136.142392,53.5062497 85.4880646,49.0035105 49.7059121,45.62744 C31.8148358,43.9389128 17.6415541,42.5318068 7.94178925,41.5478166 C3.09140831,41.0548375 -0.640344182,40.6681294 -3.15975068,40.40442 C-5.67816019,40.1407106 -6.98023573,40 -6.98422371,40 L-7.09289617,40.9780863 C-7.08292622,40.9790703 327.520578,77 610.198586,77 Z" id="Fill-68" fill="var(--hr-color)" mask="url(#mask-2)"></path> </g> <path d="M204.690104,47.32 C206.259104,42.729 211.015104,44.247 211.564104,45.397 C212.744104,47.867 213.564104,47.459 213.564104,47.459 C213.564104,47.459 211.475104,47.412 210.596104,49.717 C210.211104,50.727 208.816104,56.337 204.832104,58.949 C202.997104,60.153 200.700104,60.071 198.966104,61.194 C197.487104,62.163 196.569104,65.733 194.477104,65.784 C194.528104,65.733 194.579104,65.58 194.630104,65.479 C194.069104,65.835 193.355104,65.632 192.743104,65.632 C192.845104,65.376 192.590104,65.58 192.743104,65.325 C192.641104,65.428 192.641104,65.172 192.539104,65.223 C192.488104,64.764 192.284104,64.458 192.590104,64.101 C192.437104,64.254 192.590104,64.101 192.437104,64.203 C192.131104,63.285 196.633104,60.39 197.092104,57.891 C198.220104,51.746 202.522104,48.379 204.690104,47.32" id="Fill-73" fill="var(--hr-color)"></path> <path d="M237.461104,48.48 C240.188104,44.985 244.713104,48.584 244.314104,51.397 C248.502104,52.923 251.137104,63.152 251.621104,65.056 C251.621104,65.056 254.867104,70.497 254.733104,74.267 C254.250104,72.998 254.242104,74.368 253.553104,73.169 C253.483104,73.311 252.586104,72.323 252.517104,72.464 C251.439104,71.459 250.904104,67.897 250.103104,67.455 C249.301104,67.012 244.307104,65.92 241.012104,62.94 C237.824104,60.056 237.482104,56.195 237.693104,54.727 C238.377104,49.959 234.689104,50.147 234.689104,50.147 C234.689104,50.147 236.502104,49.709 237.461104,48.48" id="Fill-79" fill="var(--hr-color)"></path> <path d="M490.329073,53.2234708 C489.681073,52.5704708 487.329073,50.5564708 485.450073,51.0884708 C483.536073,51.6304708 481.516073,54.0154708 481.624073,56.5644708 L481.662073,56.5564708 C481.662073,56.5564708 480.546073,57.4964708 480.276073,57.7454708 C479.575073,58.3664708 475.473073,63.1004708 474.745073,69.2234708 C474.510073,71.1834708 474.352073,73.7664708 474.662073,75.0564708 C474.972073,76.3454708 473.722073,78.5464708 473.020073,79.9774708 C472.696073,80.5984708 469.438073,84.9064708 471.702073,83.9734708 C470.907104,85.7234708 472.329073,85.1394708 472.329073,85.1394708 C472.329073,85.1394708 471.827073,86.6204708 472.662073,86.6204708 C473.001073,86.6204708 473.995073,85.1394708 473.995073,85.1394708 C473.995073,85.1394708 473.995073,87.9734708 475.238073,84.4414708 C476.071073,87.7754708 477.917073,79.0904708 478.528073,77.9324708 C479.140073,76.7754708 481.459073,75.7964708 483.265073,74.8634708 C485.072073,73.9324708 486.663073,71.7724708 487.632073,69.7204708 C489.035073,66.8614708 489.662073,64.4804708 489.662073,61.3064708 C489.662073,54.2234708 492.971073,53.9734708 492.971073,53.9734708 C492.971073,53.9734708 490.976073,53.8764708 490.329073,53.2234708" id="Fill-87" fill="var(--hr-color)"></path> <path d="M177.695104,46.218 C179.252104,47.12 180.579104,51.973 180.214104,56.075 C179.708104,61.772 180.593104,64.226 180.581104,64.709 C180.567104,64.962 180.224104,63.94 180.126104,64.119 C180.028104,64.3 179.377104,66.022 179.377104,66.022 C178.314104,65.897 177.788104,64.477 177.788104,64.477 C177.788104,64.477 177.614104,65.198 177.439104,65.022 C176.716104,64.271 177.416104,61.669 177.165104,60.689 C176.433104,57.829 174.939104,58.128 173.502104,55.834 C171.625104,52.839 171.708104,49.822 172.127104,48.584 C172.433104,47.644 172.922104,46.91 172.226104,45.906 C171.746104,45.206 171.072104,45.652 170.403104,45.709 C170.714104,45.484 171.124104,45.445 171.434104,45.219 C171.788104,44.971 175.734104,39.39 177.695104,46.218" id="Fill-90" fill="var(--hr-color)"></path> <path d="M422.710104,55.938 C422.537104,55.574 421.710104,50.668 417.874104,51.317 C415.392104,51.738 413.710104,53.668 414.627104,57.084 C414.271104,59.936 412.264104,61.525 413.761104,67.05 C415.427104,73.204 419.112104,76.736 419.257104,78.691 C419.356104,80.243 419.127104,86.627 419.127104,86.627 C419.236104,86.528 420.370104,85.727 420.478104,85.628 C420.696104,85.909 420.845104,86.237 421.127104,86.501 C421.153104,86.355 421.604104,86.239 421.632104,86.093 C422.013104,86.465 422.516104,87.242 423.070104,87.497 C423.089104,87.079 423.173104,86.461 423.354104,86.134 C423.363104,86.406 424.202104,87.052 424.257104,87.243 C424.202104,87.052 424.678104,86.266 424.824104,85.33 C425.142104,85.72 426.640104,85.572 426.877104,85.918 C424.794104,82.584 424.141104,77.345 424.120104,75.908 C424.019104,72.43 426.519104,69.102 426.492104,65.673 C426.454104,60.903 422.710104,55.938 422.710104,55.938" id="Fill-98" fill="var(--hr-color)"></path> <path d="M302.845104,52.885 C302.750104,52.573 302.523104,47.483 298.347104,48.334 C296.386104,48.734 295.575104,51.737 296.049104,53.353 C295.437104,55.595 294.345104,56.846 294.877104,61.459 C295.470104,66.599 298.846104,68.692 298.732104,70.277 C298.630104,71.533 298.642104,73.546 298.598104,74.85 L298.535104,75.183 C298.632104,75.116 298.744104,75.156 298.841104,75.089 C298.980104,75.342 299.019104,76.129 299.210104,76.376 C299.248104,76.262 299.903104,75.988 299.529104,75.487 C299.785104,75.832 300.989104,77.493 301.170104,77.254 C301.146104,77.474 301.582104,76.552 301.603104,76.712 C301.582104,76.552 301.681104,76.558 301.906104,75.826 C302.110104,76.178 302.440104,76.195 302.585104,76.501 C303.798104,75.019 302.642104,71.989 302.793104,70.834 C303.120104,68.033 304.624104,64.088 305.002104,61.334 C305.529104,57.506 302.845104,52.885 302.845104,52.885" id="Fill-99" fill="var(--hr-color)"></path> <path d="M542.099104,60.139 C541.980104,59.748 542.402104,54.385 537.377104,54.3367902 C534.210104,54.307 533.793104,57.501 533.210104,60.084 C532.422104,62.891 530.212104,63.731 530.877104,69.501 C531.616104,75.928 535.338104,80.463 535.187104,82.446 C535.052104,84.019 534.894104,88.962 535.018104,88.878 C535.195104,89.194 535.379104,89.576 535.622104,89.884 C535.671104,89.741 535.845104,89.516 535.894104,89.374 C536.220104,89.805 537.677104,91.884 537.908104,91.584 C537.877104,91.859 538.835104,90.146 539.124104,89.228 C539.384104,89.669 539.485104,89.668 539.670104,90.051 C541.222104,88.192 540.271104,82.86 540.467104,81.415 C540.893104,77.911 543.928104,73.925 544.419104,70.48 C545.104104,65.688 542.099104,60.139 542.099104,60.139" id="Fill-100" fill="var(--hr-color)"></path> </g> </g> </g> </svg></p> <p>So what we’re going to do next is we’re going to go over a few of the examples in the CSS Carousel gallery and we’re going to inspect the accessibility information for these examples, use a screen reader to navigate them, operate them using a keyboard, and generally examine the usability of these examples. After all, the gallery’s homepage encourages us to <q><strong>inspect</strong> the CSS, review the DOM, and check the accessibility tree</q>. So, let’s do just that.</p> <h2 id="examining-the-accessibility-of-css-carousels" tabindex="-1">Examining the accessibility of CSS Carousels</h2> <p>Before going through each example separately, I want to mention a few things that all examples have in common:</p> <ol> <li>The <code>scroll-marker-group</code> property is used on the scroll container to create a <code>::scroll-marker-group</code> pseudo-element inside the container. If you disable the property, the group of scroll markers (<code>::scroll-marker-group</code>) is removed, and so is every scroll marker (<code>::scroll-marker</code>) corresponding to each of the items in the container.</li> <li>The <code>::scroll-marker-group</code> element is exposed as a <code>tablist</code> in the accessibility tree. Note that this is not mentioned anywhere in the CSS specification (at the time of making of this post). These are the semantics that Chrome is currently exposing under the hood.</li> <li>Each <code>::scroll-marker</code> element is exposed as a <code>tab</code> within the tablist.</li> <li>The accessible name for the scroll marker is provided in CSS via the <code>content</code> property. <em>You must</em> provide the name for each tab. The browser will not do this for you.</li> <li>When <code>::scroll-button()</code>s are present, they are exposed as <code>button</code>s. You are also expected to provide an accessible name to these buttons via the CSS <code>content</code> property.</li> </ol> <p>Now, here is the most important takeaway from all of this:</p> <p>Because all the scroll markers are exposed as <code>tab</code>s, this means that <strong>all the carousel examples in the gallery are <em>supposed to be</em> Tabs widgets.</strong></p> <p>Yes, you read that right. Even though most of these components don’t look or behave like Tabs, the browser is using Tabs widget semantics to describe them to assistive technologies. This is already concerning because the gallery contains different UI patterns that are clearly <em>not</em> Tabs.</p> <p>If you’re a student of my Practical Accessibility course, then you’ll remember the statement: <strong>ARIA is a promise.</strong></p> <p>When you use ARIA to describe an element to your users, you must ask yourself: Am I delivering on the promise I have made to the user? Is this element <em>really</em> what I’m exposing it to be?</p> <p>The browser is using ARIA Tab widget roles to expose the examples in the CSS Carousels gallery as Tabs widgets. The question is: Are they really? Do these examples meet the expectations and requirements for Tabs widgets?</p> <p>We’ll start to get more technical from here on.</p> <h3 id="tabs-widget-accessibility-requirements" tabindex="-1">Tabs widget accessibility requirements</h3> <p>Tabs have specific accessibility requirements. We’ll start by reviewing these requirements for so we have a benchmark to test the examples against.</p> <p>If these requirements are not met, then the examples are going to be confusing and unusable by screen reader users.</p> <p>If you’ve taken my course, then you’ll remember from the very first ARIA chapter—ARIA 101—that <strong>ARIA is extremely powerful but also very dangerous if you don’t use it correctly.</strong> And that if you’re not aware of how roles, states and properties work together, you can end up creating a more confusing and inaccessible user experience.</p> <p>We also learned that the ARIA specification documents the requirements for ARIA roles in the definition of each role, and that <strong>there are strict parent-child relationships between some ARIA roles.</strong> This means that the use of some attributes is restricted to specific contexts or parents. Some roles can only be used as a child to a specific—usually composite—ARIA role.</p> <p>It is important that you learn and understand how ARIA attributes are used and nested, especially if you’re creating components that are re-used in various contexts across a website or application.</p> <p>To create a Tabs widget today, you need to use the ARIA <code>tab</code> role, the <code>tabpanel</code> role, and the composite <code>tablist</code> role.</p> <p>According to the specification (emphasis mine):</p> <blockquote> <p>Authors MUST ensure elements with role <code>tab</code> are <strong>contained in, or owned by, an element with the role <code>tablist</code>.</strong></p> <p>[…]</p> <p>Authors <em>MUST</em> ensure that <strong>if a <code>tab</code> is active, a corresponding <code>tabpanel</code></strong> that represents the active <code>tab</code> is rendered.</p> <p>[…]</p> <p>For a single-selectable <code>tablist</code>, authors <em>SHOULD</em> <strong>hide other <code>tabpanel</code> elements from the user</strong> until the user selects the tab associated with that tabpanel.</p> <p>[…]</p> <p>In either case, authors <em>SHOULD</em> ensure that <strong>a selected tab has its <code>aria-selected</code> attribute set to <code>true</code></strong>, that <strong>inactive tab elements have their <code>aria-selected</code> attribute set to <code>false</code></strong></p> <p>— <a href="https://www.w3.org/TR/wai-aria-1.3/#tab">The ARIA Specification, tab role definition</a></p> </blockquote> <p>So the specification specifies how the <code>tab</code> and <code>tablist</code> roles should be used, and states the requirements needed to ensure the Tabs widget you’re creating is accessible.</p> <p>The ARIA specification also refers to the <a href="https://www.w3.org/WAI/ARIA/apg/">ARIA Authoring Practices Guide (APG)</a> for technical guidance about implementing Tabs.</p> <p>The APG’s primary purpose is <strong>to demonstrate how to use ARIA to implement widgets in accordance with the ARIA specification.</strong></p> <p>According to the guidance in the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/">APG’s Tabs pattern</a> page (emphasis mine):</p> <blockquote> <p>Tabs are a set of layered sections of content, known as tab panels, <strong>that display one panel of content at a time.</strong> Each tab panel has an associated tab element, that when activated, displays the panel.</p> <p>[…]</p> <p>When a tabbed interface is initialized, one tab panel is displayed and its associated tab is styled to indicate that it is active. <strong>When the user activates one of the other tab elements, the previously displayed tab panel is hidden, the tab panel associated with the activated tab becomes visible, and the tab is considered “active”.</strong></p> </blockquote> <p>There are also two types of Tabs:</p> <ol> <li><a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/">Tabs With Automatic Activation</a>: A tabs widget where tabs are automatically activated and their panel is displayed when they receive focus.</li> <li><a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/">Tabs With Manual Activation</a>: A tabs widget where users activate a tab and display its panel by pressing Space or Enter.</li> </ol> <p>Regardless of the type of activation, a Tabs component has these keyboard interaction requirements:</p> <ul> <li>When you press the <code>tab</code> key and focus moves into the tab list, focus moves to <strong>the active <code>tab</code> element</strong>.</li> <li>When the tab list contains the focus, pressing the <code>tab</code> key again moves focus to the next element in the page tab sequence outside the tablist, which is <strong>the tabpanel</strong> unless the first element containing meaningful content inside the tabpanel is focusable.</li> <li>When focus is inside the tab list: <ul> <li>Pressing the <code>Left Arrow</code> key moves focus to the previous tab. If focus is on the first tab, it moves focus to the last tab.</li> <li>Pressing the <code>Right Arrow</code> key moves focus to the next tab. If focus is on the last tab element, moves focus to the first tab.</li> </ul> </li> </ul> <p>We’re going to focus on the automatic activation tabs widget because the CSS Carousels examples are implemented so that a scroll marker’s corresponding item is shown when the marker receives focus, which means that the widget is supposed to be an automatic activation widget.</p> <p>Here is a video demonstration of how the automatic activation tabs widget is expected to be operated using a keyboard, and then using a screen reader.</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/automatic-tabs-with-keyboard-and-vo.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption>Some highlights: <ul> <li>Pressing the <kbd>tab</kbd> key moves keyboard focus into the tablist. The selected tab receives focus.</li> <li>Pressing <kbd>tab</kbd> again moves keyboard focus to the active tabpanel.</li> <li>Using Left and Right Arrow keys navigates between the tabs in the tablist and automatically activates the focused tab. The corresponding tabpanel is shown. The other tabpanels are hidden.</li> <li>Using a screen reader (VoiceOver on macOS in this video): <ul> <li>If you navigate using the <kbd>tab</kbd> key, the experience is the same, and the screen reader announces the tab and the tabpanel when they receive focus. It announces the tab and the tabpanel's roles followed by their accessible names. For the <code>tab</code>, it also announces that it is selected.</li> <li>If you navigate by Arrow keys <strong>using VoiceOver navigation</strong>, VoiceOver announces the tabs and tabpanels when they receive focus. It also announces the state of the tabs. Navigating to a tab using this mode of navigation does not activate the tab. The selected tab will be announced as selected. The tabs that are not selected will not be announced as selected. Navigating away from the tabs, the active tabpanel is announced. The remaining tabpanels are hidden. A tabpanel only becomes accessible when its corresponding tab is activated.</li> </ul> </li> </ul> </figcaption> </figure> <p>So, in an automatic activation tab widget, moving keyboard focus (not screen reader focus) from one tab to the other activates the tab’s corresponding tabpanel. The other tabpanels are hidden from all users and are therefore also not accessible by keyboard.</p> <p><svg role="separator" style="display: block; max-width: 65%; margin: 4rem auto;" width="500px" height="32px" viewBox="0 0 794 51" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <polygon id="path-1" points="0.907103825 0 798.907104 0 798.907104 364 0.907103825 364"></polygon> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd"> <g id="36257-O0KTX6" transform="translate(0.000000, -212.000000)"> <g id="Group" transform="translate(-4.907104, 171.000000)"> <g id="Group-70"> <mask id="mask-2" fill="white"> <use xlink:href="#path-1"></use> </mask> <g id="Clip-67"></g> <path d="M610.198586,77 C699.286076,77 783.215114,73.4231956 849.907104,64.0083772 L849.765531,63.0342269 C783.152303,72.4392054 699.257163,76.0169938 610.198586,76.0160098 C468.900459,76.0169938 314.600519,67.0124993 195.629099,58.0080049 C136.142392,53.5062497 85.4880646,49.0035105 49.7059121,45.62744 C31.8148358,43.9389128 17.6415541,42.5318068 7.94178925,41.5478166 C3.09140831,41.0548375 -0.640344182,40.6681294 -3.15975068,40.40442 C-5.67816019,40.1407106 -6.98023573,40 -6.98422371,40 L-7.09289617,40.9780863 C-7.08292622,40.9790703 327.520578,77 610.198586,77 Z" id="Fill-68" fill="var(--hr-color)" mask="url(#mask-2)"></path> </g> <path d="M204.690104,47.32 C206.259104,42.729 211.015104,44.247 211.564104,45.397 C212.744104,47.867 213.564104,47.459 213.564104,47.459 C213.564104,47.459 211.475104,47.412 210.596104,49.717 C210.211104,50.727 208.816104,56.337 204.832104,58.949 C202.997104,60.153 200.700104,60.071 198.966104,61.194 C197.487104,62.163 196.569104,65.733 194.477104,65.784 C194.528104,65.733 194.579104,65.58 194.630104,65.479 C194.069104,65.835 193.355104,65.632 192.743104,65.632 C192.845104,65.376 192.590104,65.58 192.743104,65.325 C192.641104,65.428 192.641104,65.172 192.539104,65.223 C192.488104,64.764 192.284104,64.458 192.590104,64.101 C192.437104,64.254 192.590104,64.101 192.437104,64.203 C192.131104,63.285 196.633104,60.39 197.092104,57.891 C198.220104,51.746 202.522104,48.379 204.690104,47.32" id="Fill-73" fill="var(--hr-color)"></path> <path d="M237.461104,48.48 C240.188104,44.985 244.713104,48.584 244.314104,51.397 C248.502104,52.923 251.137104,63.152 251.621104,65.056 C251.621104,65.056 254.867104,70.497 254.733104,74.267 C254.250104,72.998 254.242104,74.368 253.553104,73.169 C253.483104,73.311 252.586104,72.323 252.517104,72.464 C251.439104,71.459 250.904104,67.897 250.103104,67.455 C249.301104,67.012 244.307104,65.92 241.012104,62.94 C237.824104,60.056 237.482104,56.195 237.693104,54.727 C238.377104,49.959 234.689104,50.147 234.689104,50.147 C234.689104,50.147 236.502104,49.709 237.461104,48.48" id="Fill-79" fill="var(--hr-color)"></path> <path d="M490.329073,53.2234708 C489.681073,52.5704708 487.329073,50.5564708 485.450073,51.0884708 C483.536073,51.6304708 481.516073,54.0154708 481.624073,56.5644708 L481.662073,56.5564708 C481.662073,56.5564708 480.546073,57.4964708 480.276073,57.7454708 C479.575073,58.3664708 475.473073,63.1004708 474.745073,69.2234708 C474.510073,71.1834708 474.352073,73.7664708 474.662073,75.0564708 C474.972073,76.3454708 473.722073,78.5464708 473.020073,79.9774708 C472.696073,80.5984708 469.438073,84.9064708 471.702073,83.9734708 C470.907104,85.7234708 472.329073,85.1394708 472.329073,85.1394708 C472.329073,85.1394708 471.827073,86.6204708 472.662073,86.6204708 C473.001073,86.6204708 473.995073,85.1394708 473.995073,85.1394708 C473.995073,85.1394708 473.995073,87.9734708 475.238073,84.4414708 C476.071073,87.7754708 477.917073,79.0904708 478.528073,77.9324708 C479.140073,76.7754708 481.459073,75.7964708 483.265073,74.8634708 C485.072073,73.9324708 486.663073,71.7724708 487.632073,69.7204708 C489.035073,66.8614708 489.662073,64.4804708 489.662073,61.3064708 C489.662073,54.2234708 492.971073,53.9734708 492.971073,53.9734708 C492.971073,53.9734708 490.976073,53.8764708 490.329073,53.2234708" id="Fill-87" fill="var(--hr-color)"></path> <path d="M177.695104,46.218 C179.252104,47.12 180.579104,51.973 180.214104,56.075 C179.708104,61.772 180.593104,64.226 180.581104,64.709 C180.567104,64.962 180.224104,63.94 180.126104,64.119 C180.028104,64.3 179.377104,66.022 179.377104,66.022 C178.314104,65.897 177.788104,64.477 177.788104,64.477 C177.788104,64.477 177.614104,65.198 177.439104,65.022 C176.716104,64.271 177.416104,61.669 177.165104,60.689 C176.433104,57.829 174.939104,58.128 173.502104,55.834 C171.625104,52.839 171.708104,49.822 172.127104,48.584 C172.433104,47.644 172.922104,46.91 172.226104,45.906 C171.746104,45.206 171.072104,45.652 170.403104,45.709 C170.714104,45.484 171.124104,45.445 171.434104,45.219 C171.788104,44.971 175.734104,39.39 177.695104,46.218" id="Fill-90" fill="var(--hr-color)"></path> <path d="M422.710104,55.938 C422.537104,55.574 421.710104,50.668 417.874104,51.317 C415.392104,51.738 413.710104,53.668 414.627104,57.084 C414.271104,59.936 412.264104,61.525 413.761104,67.05 C415.427104,73.204 419.112104,76.736 419.257104,78.691 C419.356104,80.243 419.127104,86.627 419.127104,86.627 C419.236104,86.528 420.370104,85.727 420.478104,85.628 C420.696104,85.909 420.845104,86.237 421.127104,86.501 C421.153104,86.355 421.604104,86.239 421.632104,86.093 C422.013104,86.465 422.516104,87.242 423.070104,87.497 C423.089104,87.079 423.173104,86.461 423.354104,86.134 C423.363104,86.406 424.202104,87.052 424.257104,87.243 C424.202104,87.052 424.678104,86.266 424.824104,85.33 C425.142104,85.72 426.640104,85.572 426.877104,85.918 C424.794104,82.584 424.141104,77.345 424.120104,75.908 C424.019104,72.43 426.519104,69.102 426.492104,65.673 C426.454104,60.903 422.710104,55.938 422.710104,55.938" id="Fill-98" fill="var(--hr-color)"></path> <path d="M302.845104,52.885 C302.750104,52.573 302.523104,47.483 298.347104,48.334 C296.386104,48.734 295.575104,51.737 296.049104,53.353 C295.437104,55.595 294.345104,56.846 294.877104,61.459 C295.470104,66.599 298.846104,68.692 298.732104,70.277 C298.630104,71.533 298.642104,73.546 298.598104,74.85 L298.535104,75.183 C298.632104,75.116 298.744104,75.156 298.841104,75.089 C298.980104,75.342 299.019104,76.129 299.210104,76.376 C299.248104,76.262 299.903104,75.988 299.529104,75.487 C299.785104,75.832 300.989104,77.493 301.170104,77.254 C301.146104,77.474 301.582104,76.552 301.603104,76.712 C301.582104,76.552 301.681104,76.558 301.906104,75.826 C302.110104,76.178 302.440104,76.195 302.585104,76.501 C303.798104,75.019 302.642104,71.989 302.793104,70.834 C303.120104,68.033 304.624104,64.088 305.002104,61.334 C305.529104,57.506 302.845104,52.885 302.845104,52.885" id="Fill-99" fill="var(--hr-color)"></path> <path d="M542.099104,60.139 C541.980104,59.748 542.402104,54.385 537.377104,54.3367902 C534.210104,54.307 533.793104,57.501 533.210104,60.084 C532.422104,62.891 530.212104,63.731 530.877104,69.501 C531.616104,75.928 535.338104,80.463 535.187104,82.446 C535.052104,84.019 534.894104,88.962 535.018104,88.878 C535.195104,89.194 535.379104,89.576 535.622104,89.884 C535.671104,89.741 535.845104,89.516 535.894104,89.374 C536.220104,89.805 537.677104,91.884 537.908104,91.584 C537.877104,91.859 538.835104,90.146 539.124104,89.228 C539.384104,89.669 539.485104,89.668 539.670104,90.051 C541.222104,88.192 540.271104,82.86 540.467104,81.415 C540.893104,77.911 543.928104,73.925 544.419104,70.48 C545.104104,65.688 542.099104,60.139 542.099104,60.139" id="Fill-100" fill="var(--hr-color)"></path> </g> </g> </g> </svg></p> <p>Now let’s go over a few of the examples in the CSS Carousel Gallery and check to see if they meet the requirements defined in the ARIA specification, and the expected semantics and behavior listed in the APG.</p> <p>For each example, I’m going to focus on specific aspects of accessibility more than others. For example, I will highlight keyboard navigation in one example, screen reader navigation in another, screen reader announcements in another, and so on and so forth, depending on what issue stands out for every particular example.</p> <p>Remember that, with the current implementation of scroll markers, each of these examples is supposed to be a Tabs widget.</p> <p>And, finally, remember that tabs and buttons, like every other interactive element, need an accessible name.</p> <p>With all of this said, let’s start going through the examples in the gallery.</p> <p>I’m going to start with the horizontal list—a typical carousel example.</p> <h3 id="the-horizontal-list-example" tabindex="-1"><a href="https://chrome.dev/carousel/horizontal/list/">The Horizontal List example</a></h3> <p>In this example, like all the other examples in the gallery, if you open the DevTools and inspect the accessibility tree you can see that the scroll markers are exposed as <code>tab</code>s contained in a <code>tablist</code>.</p> <p>However, there are no corresponding <code>tabpanel</code>s for these <code>tabs</code>.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/list-in-accTree.png" alt="A screenshot of the Chrome DevTools full-view accessibility tree for the horizontal list." /> <figcaption></figcaption> </figure> <p>As we mentioned earlier, the ARIA specification states that you "<em>MUST</em> ensure that if a <code>tab</code> is active, <strong>a corresponding <code>tabpanel</code> that represents the active <code>tab</code> is rendered."</strong> However, there are no <code>tabpanels</code> at all in this example. So this Tabs widget is already missing an integral part of what makes it a Tabs widget. <em>What do the tabs control?</em></p> <p>Looking at the individual tabs, you’ll find that all the tabs in the tablist share the same accessible name.</p> <p>Looking in the Styles panel, you will find that the accessible name for all of the <code>::scroll-marker</code>s is provided using the CSS <code>content</code> property:</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">.scroll-markers</span> <span class="token punctuation">{</span></span><br /> <span class="token selector">..<br /> &::scroll-marker</span> <span class="token punctuation">{</span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">""</span> / <span class="token string">"Carousel item marker"</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> ..</span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/list-scroll-markers-name.png" alt="A screenshot of the Chrome DevTools full-view accessibility tree for the horizontal list, and the style panel at the bottom showing the CSS declaration that applies the same name to all markers in the list." /> <figcaption></figcaption> </figure> <p>The notable thing here is that the name is provided as a <em>fallback</em> <code>alt</code> text. This is because the dots are not supposed to have visible text labels, so the content is left empty.</p> <aside role="note"> <p>Initially, the dots in this example didn’t have an accessible name at all. The code has since then been updated and a name was added to “fix” this example by giving the dots an accessible name (because they <em>require</em> one). Only the accessible name was provided using the CSS alt text.</p> </aside> <p>Now, <em>because</em> this declaration is provided as alt text for <em>all</em> scroll markers on <em>all</em> the list items, the alt text is exposed as the accessible name for <em>all</em> the scroll markers corresponding to all the list items.</p> <p><strong>Buttons, tabs, and other interactive elements that do different things should have unique names that describe their purpose.</strong> But what we have here is 16 tabs that share the same name. So how does a user know which item each “tab” corresponds to?</p> <aside role="note"> <p>Providing an accessible name to a button (or a tab in this case) is not enough to make the button usable. The name needs to help the user understand what will happen when they activate the button. And so providing the same name to all the tabs via CSS like this does not “fix” the issue with missing accessible names, and it does not make the tabs “accessible” because the name still does not help the user understand <em>which</em> item was shown.</p> <p>(This is also why automated accessibility checkers are not capable of catching all accessibility issues. Technically, you can pass an automated test by giving your buttons <em>a</em> name; but that doesn’t necessarily make the buttons usable.)</p> </aside> <p>Instead of providing one accessible name for all scroll markers, <strong>each marker should be given its own unique name.</strong> This means that you will want to select each list item and provide a unique accessible name for its marker. You can do that by providing a unique label in the <code>content</code> property, or, alternatively, you could provide the unique names in the form of HTML attributes in the markup and then reference the names in CSS using one declaration that uses the <code>attr()</code> function. We’ll see this declaration in action in another example.</p> <p>Now, looking at the carousel itself, the first thing that comes to mind is that unlike a Tabs widget, more than one item is shown in this carousel at a time.</p> <p>The APG description of a Tabs widget says that “Tabs are a set of layered sections of content, known as tab panels, <strong>that display one panel of content at a time.</strong>”</p> <p>The ARIA specification also states that “for a single-selectable <code>tablist</code>, authors should hide other tabpanel elements from the user until the user selects the tab associated with that tabpanel.”</p> <p>Now, tabs <em>can</em> be multi-selectable. The ARIA specification specifies a <strong>multi-selectable</strong> tabs widget as a kind of tabs where more than one tab can be selected at a time.</p> <p>However, when more than one tab can be selected at a time, the specification states that you should <q>ensure that the <code>tab</code> for each visible <code>tabpanel</code> has the <code>aria-expanded</code> attribute set to <code>true</code>, and that the tabs associated with the remaining hidden from all users tabpanel elements have their <code>aria-expanded</code> attributes set to <code>false</code>.</q></p> <p>If you inspect the accessibility tree, you can see that <strong>the scroll marker group is not a multi-selectable tablist.</strong> The browser sets the <code>multiselectable</code> attribute value to <code>false</code> on the list indicating that only one tab can be selected at a time. And if you inspect the <code>tab</code>s within the tab list, you can see that only one <code>tab</code> has <code>aria-selected=true</code> on it at a time.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/list-tablist-not-multi-selectable.png" alt="A screenshot of the Chrome DevTools full-view accessibility tree for the horizontal list showing the tablist has multiselectable property set to 'false'." /> <figcaption></figcaption> </figure> <p>If this were meant to be a multi-selectable tabs widget, then it should indicate that, and the browser should set the <code>aria-selected</code> and <code>aria-expanded</code> attributes to <code>true</code> on all the scroll markers of the visible items.</p> <p>However, even the CSS specification <a href="https://www.w3.org/TR/css-overflow-5/#active-scroll-marker">is specific about selecting only one scroll marker at a time</a>. It literally states that <q>exactly one scroll marker within each scroll marker group is determined to be active at a time.</q></p> <p>So, by definition, scroll markers are not designed to be used to create multi-selectable components, and especially not multi-selectable Tabs components.</p> <p>So, what we have here is scroll markers being exposed as single-selectable <em>tabs</em> in a carousel component where more than one item is “active” at a time. And even though more than one one item is active, only one scroll marker is “selected”.</p> <p>And this is not something that you, as the author of the code, can change because <strong>you have no control over the semantic output</strong> of the CSS properties you use.</p> <p>This indicates that the semantics exposed for the scroll markers are neither suitable nor representative of the component they are used to implement.</p> <p>Now, if you navigate to the carousel using a screen reader (I am using VoiceOver on macOS), you will notice that <strong>numeration inside the list is off</strong>. This is because the browser adds the scroll marker group as well as the Previous and Next scroll buttons as direct children to the list, as siblings to the list items. So the total number of items in the list is miscommunicated to the user and no longer represents the actual number of list items.</p> <p>Here is a video recording of how the carousel is announced with VoiceOver on macOS:</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/list-with-vo-keyboard.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> In this video recording, I am navigating the carousel using the <kbd>tab</kbd> key. </figcaption> </figure> <p>Furthermore, notice how the Scroll Left button remains focusable even though it is meant to be disabled.</p> <p>A disabled <code><button></code> will typically be removed from the sequential tab order of the page, and will be exposed as a disabled button to screen reader users. However, if you inspect the accessibility information of that button when it’s in the disabled state, you’ll find that the browser does not expose it as a disabled button.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/disabled-scroll-button-not-disabled.png" alt="A screenshot of the Chrome DevTools accessibility panel showing the scroll button's accessibility properties, but these properties do not include a property indicating that the button is disabled." /> <figcaption></figcaption> </figure> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/list-with-vo-navigation.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> In this video recording, I am navigating the carousel with VoiceOver navigation using Arrow Left and Arrow Right keys. <p>You may have noticed in this recording that some tabs were announced as selected even though their corresponding items were not active/visible. On the other hand, only one of the active tabs was announced as selected even though more than one item is active/visible at a time. </p> </figcaption> </figure> <p>So, this carousel example has several accessibility and usability issues.</p> <p>A blind screen reader user navigating this carousel would encounter a broken component and would likely be confused as to <em>what</em> it is they are interacting with, and what will happen when each of the “tabs” is activated.</p> <p>So, as a conclusion I would say that <strong>this implementation of a horizontal list carousel is not accessible, and not ready for production.</strong></p> <p>Moving on to the Cards example…</p> <h3 id="the-cards-example" tabindex="-1"><a href="https://chrome.dev/carousel/horizontal/cards/">The Cards example</a></h3> <p>If you pull up the browser DevTools again to get an overview of the accessibility information exposed to screen readers in the accessibility tree, you can see that, like with the previous example, the browser has appends a single-selectable <code>tablist</code> to the scroll container.</p> <p>Like with the horizontal list example, more than one Card is visible at a time, yet only one <code>tab</code> is <code>selected</code> at a time.</p> <p>The tablist in this example contains five <code>tab</code>s corresponding to the five different cards. Yet all the tabs have the same accessible name.</p> <p>The cards are implemented (and exposed) as <code>article</code>s. So, once again, there are no <code>tabpanel</code>s in this widget either.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/cards-accTree.png" alt="A screenshot of the Chrome DevTools accessibility panel showing the full-view accessibility tree of the Cards carousel." /> <figcaption></figcaption> </figure> <p>Since this example contains interactive elements inside the carousel items, let’s focus on how keyboard navigation works in the carousel. We will also be testing screen reader navigation separately.</p> <p>When I start to navigate the page using the <kbd>Tab</kbd> key, focus moves inside the carousel to the scroll marker group (the tablist).</p> <p>Pressing the <code>tab</code> key again moves focus outside the group into the next focusable element in the DOM, which is the Scroll Left button. Pressing the <code>tab</code> key again moves focus to the Scroll Right button. Normally, you would expect keyboard focus to move from the selected tab to the tab panel it controls (or a focusable element inside that panel). This is also the expected behavior stated and demonstrated in the APG.</p> <p>Now, when I press the <code>tab</code> key <em>again</em>, this is where the carousel starts to behave <em>erratically</em>.</p> <p>Here is a short video recording of me navigating the Cards example using keyboard, followed by some of the most important observations:</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/cards-with-keyboard.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> <p></p></figcaption><p></p> </figure> <p>First, when you’re in the scroll markers group and you press the <kbd>tab</kbd> key, focus does not necessarily move to the currently selected card (the “tabpanel”). Instead, it moves to the first focusable element in the first card inside the scroller. Sometimes it will move to the first <em>visible</em> card. Sometimes it will move to the first card in the container even when it’s not visible. And sometimes it will move to the expected card.</p> <p>Second, pressing <kbd>Shift + Tab</kbd> when you’re inside a card to navigate backwards does not return keyboard focus back to the tab that activated the card (which is the expected keyboard behavior for a Tabs widget). Instead, keyboard focus moves to the link in the previous card, and then to the link in the previous previous card, and then to the link in the previous previous previous card, and so on and so forth. This is because the invisible cards are not really hidden like they would be in a Tabs component. They are just scrolled out of view. As such, keyboard focus moves to an element that is not even supposed to be active or accessible.</p> <p>Third, you will also notice that <em>when</em> focus moves to the link inside a card, the card’s corresponding tab is not selected. A tab is only visually marked as selected when it scrolls into a certain position within the container. Because of that, the browser will skip a tab and visually select the one that comes after it.</p> <p>And lastly, if you inspect the accessibility information exposed to the user as you navigate the carousel using keyboard, you’ll also see that the state of the <code>tab</code>s is not correctly conveyed to the user. Even when a tab is visually marked as selected, its accessibility state is not updated. So a blind screen reader user navigating using a keyboard will not be getting the same feedback as a sighted user does.</p> <p>And a sighted screen reader user will also get a mismatch between what they see on screen and what the screen reader announces to them.</p> <p>Here is a video recording of navigating the Cards carousel using VoiceOver on macOS:</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/cards-with-vo.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> A couple of observations: <ul> <li>The scroll markers are not visually updated to indicate which card is currently selected when navigating through the cards with keyboard and VoiceOver navigation.</li> <li>Numeration in the list is wrong due to the fact that the scroll buttons and scroll marker group are included inside the list. So the number of items in the list is micsommunicated to the user.</li> </ul> </figcaption> </figure> <p>Both keyboard and screen reader navigation is broken in this example.</p> <p>The entire behavior of this widget is based on how any normal scrolling container containing focusable elements would behave. This carousel does not behave like Tabs because it is <em>not</em> a Tabs widget. The accessibility information exposed to screen reader users is generally misleading and mostly incorrect, making it unusable.</p> <aside role="note"> <p>There are also other aspects of accessibility that need to be addressed in this and other examples, such as the lack of unique accessible names on all the interactive elements (including the links inside the cards), the use of semantic heading elements for styling purposes, improperly hiding elements resulting in them being announced by screen readers even when they are not shown, using color alone to convey information, and a few more.</p> <p>But we won’t be diving into those issues because the primary goal of this post is to focus on how the new CSS Overflow module affects the accessibility information of the components it is used on, and what that entails in terms of accessibility and usability.</p> </aside> <p>Moving on to the Scroll Spy example…</p> <h3 id="the-scroll-spy-example" tabindex="-1"><a href="https://chrome.dev/carousel/vertical/scroll-spy/">The Scroll Spy example</a></h3> <p>The ScrollSpy example displays a series of content sections inside a vertically-scrolling container.</p> <p>This scrolling container contains what effectively looks like an article made up of a series of sections with headings, and that has a table of contents on the left side of the article. Only instead of having a table of contents—which is semantically structured using a list of links, this example is also implemented using CSS scroll markers. This means that instead of a list of links, the “article” has a group of <code>tab</code>s!</p> <p>If you inspect the accessibility information in the accTree, you’ll notice common issues with the previous examples:</p> <ol> <li>We have a single-selectable <code>tablist</code> with only one <code>tab</code> selected at a time, when more than one section is visible at a time.</li> <li>Like previous examples, there are no <code>tabpanel</code>s in what is supposed to be a Tabs widget. Instead, the sections are implemented as regions. This means that each section is exposed as a page landmark, which is very uncommon for a series of text sections like these. We’ll talk about <em>why</em> they are exposed as regions shortly.</li> </ol> <p>Once again, the browser is exposing semantics that are not representative of the pattern they are used to implement.</p> <p>Furthermore, the tabs in this example do not have accessible names.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/scrollspy-tabs-no-names.png" alt="A screenshot of the Chrome DevTools full-view accessibility tree for the ScrollSpy example." /> <figcaption></figcaption> </figure> <p>If you check the Styles panel, you’ll notice that the name of the markers is provided using the <code>attr()</code> function.</p> <p>The <code>:scroll-marker</code> of each <code>section</code> pulls its content (and by extension: its accessible name) from the <code>aria-label</code> attribute on that section.</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">section::scroll-marker</span> <span class="token punctuation">{</span></span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token function">attr</span><span class="token punctuation">(</span>aria-label<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>Even though the content of the <code>aria-label</code> attribute <em>is</em> visually rendered in the scroll markers, it is not exposed as a name for the <code>tab</code>s in the accessibility tree. So these markers have no accessible names.</p> <p>This is probably a bug.</p> <p>Now, the presence of <code>aria-label</code> on the <code><section></code>s provide these sections with an accessbile name. And, according to the specification, a <code><section></code> is <a href="https://w3c.github.io/html-aria/#el-section">exposed as a <code>region</code> landmark when it is given an accessible name</a>. This is why the sections in this component are exposed as landmark regions.</p> <p>Here is how VoiceOver on macOS announces the ScrollSpy example:</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/scrollspy-with-vo.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> <p></p></figcaption><p></p> </figure> <p>Notice how VoiceOver announces the tabs with no names.</p> <p>You will also notice in the recording that the tabs are announced as selected, even when their target sections are not scrolled into view.</p> <p>So the screen reader announces the presence of tabs only, but there is no other information describing the component to the user. A blind screen reader user will come across a list of controls that have no names and no indication of what they control.</p> <p>In addition to highlighting the scroll marker naming bug, I wanted to use this example as an opportunity to highlight another issue with <a href="https://www.w3.org/TR/css-overflow-5/#active-scroll-marker">the <code>:target-current</code> selector</a> introduced in the specification.</p> <p>Instead of using <code>::scroll-marker</code>s to implement this example, I would instead expect to be able to create a semantic table of contents using an HTML list of <code><a href=""></code>, and then use the <code>:target-current</code> pseudo-class to apply active styles to a link (the native scroll marker!) when its target is scrolled into view.</p> <p>However, that doesn’t seem to work at the moment. I created <strong>a reduced test case</strong> where I have a series of sections, and a list of links to those sections. I used the <code>:target</code> class to apply a yellow background color to the target section, and the new <code>:target-current</code> class to style the link associated with that section. But the styles are not applied to the link when its target section is “active”.</p> <figure class="wide"> <p class="codepen" data-height="601" data-default-tab="html,result" data-slug-hash="dPPWrqq" data-pen-title="target-current test" data-user="SaraSoueidan" data-token="c1d851b4133f567d5e9f4a3a3409fdb0" style="height: 601px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/SaraSoueidan/pen/dPPWrqq/c1d851b4133f567d5e9f4a3a3409fdb0"> target-current test</a> by Sara Soueidan (<a href="https://codepen.io/SaraSoueidan">@SaraSoueidan</a>) on <a href="https://codepen.io/">CodePen</a>.</span> </p> <script async="" src="https://public.codepenassets.com/embed/index.js"></script> </figure> <p>Unfortunately, even though the specification states that it <q>defines the ability to associate scroll markers with elements in a scroller</q>, <strong>the current implementation of the <code>:target-current</code> pseudo-class seems to work only for CSS-generated <code>::scroll-markers</code>, but not for native HTML ones.</strong></p> <p>Personally, I think <code>:target-current</code> is one of the most useful additions to the specification. It’s unfortunate that its current implementation is limited to the new pseudo-elements.</p> <aside role="note" class="update"> <p><strong>Update</strong>: See <a href="https://www.sarasoueidan.com/blog/css-carousels-accessibility/#update%3A-august-19th%2C-2025">the updates section</a> at the bottom of the article for an update about creating the Scrollspy effect using CSS.</p> </aside> <p>Moving on to one last example: the horizontal tabs example—the perfect candidate for a Tabs widget implementation.</p> <h3 id="the-horizontal-tabs-example" tabindex="-1"><a href="https://chrome.dev/carousel/horizontal/tabs/">The Horizontal Tabs example</a></h3> <p>If you inspect the accessibility tree for this example, you can see the scroll marker group exposed as a <code>tablist</code>, and each of the three scroll markers exposed as a <code>tab</code>.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/tabs-accTree.png" alt="A screenshot of the Chrome DevTools full-view accessibility tree for the Tabs example." /> <figcaption></figcaption> </figure> <p>The tabs don’t have an accessible name in this example either. We’ll inspect the CSS declaration for the markers shortly.</p> <p>Unlike the previous examples, this example does have three <code>tabpanels</code> exposed in the tree.</p> <p>We know by now that the browser does not add <code>tabpanel</code> roles to the items in a scroller. So these roles must be provided in the markup.</p> <p>And sure enough, if you inspect the HTML markup for this component, you can see that the <code>tabpanel</code> roles are hard-coded into the HTML.</p> <figure class="wide"> <img src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/tabpanel-roles-in-markup.png" alt="A screenshot of the Chrome DevTools Elements panel for the Tabs example showing each of the tabpanels as divs with role=tabpanel set on them." /> <figcaption></figcaption> </figure> <p>The panels are also given accessible names using <code>aria-label</code>, which is once again used to provide the contents and names for each of the scroll markers in CSS.</p> <pre class="language-css"><code class="language-css"><span class="highlight-line"><span class="token selector">.carousel--snap</span> <span class="token punctuation">{</span></span><br /> <span class="token selector">..<br /> &::scroll-marker</span> <span class="token punctuation">{</span><br /><span class="highlight-line"> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token function">attr</span><span class="token punctuation">(</span>aria-label<span class="token punctuation">)</span><span class="token punctuation">;</span></span><br /><span class="highlight-line"> ..</span><br /><span class="highlight-line"> <span class="token punctuation">}</span></span><br /><span class="highlight-line"><span class="token punctuation">}</span></span></code></pre> <p>This is why the <code>tab</code>s don’t have an accessible name. As we mentioned in the previous example, this is probably a bug.</p> <p>Let’s fire up VoiceOver and check how the Tabs are announced.</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/tabs-with-vo.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> Navigating the Tabs example with VoiceOver, first using keyboard navigation, then using VoiceOver navigation. </figcaption> </figure> <p>When using VoiceOver navigation to navigate to the tabs, all the tabs are announced as selected, and the <code>selected</code> state of the tabs is also updated in the accessbility tree. However, their visual styles are not updated to reflect that they are selected, and their corresponding tabpanels are not shown when they are selected.</p> <p>Additionally, using VoiceOver navigation (Right and Left Arrow keys) you are able to navigate between the tabpanels without needing to activate their corresponding tabs. That said, when you navigate to a tabpanel, the tabpanel is initially announced as empty. Pressing the Right Arrow key again, the screen reader announces the content inside the tabpanel just fine.</p> <p>It is possible to also navigate through the tabpanels using keyboard Arrow keys, without needing to use the tabs at all.</p> <figure class="wide"> <video controls="" src="https://www.sarasoueidan.com/assets/images/css-carousels-accessibility/tabs-with-keyboard.mp4" width="auto" style="width: 100%;"> Sorry, your browser doesn't support embedded videos. </video> <figcaption> Navigating the Tabs example with keyboard <kbd>tab</kbd> and Arrow keys. </figcaption> </figure> <p>As the ARIA specification notes, you should hide other <code>tabpanel</code>s from the user until the user selects the tab associated with that tabpanel. In this case, the tab panels were accessible and were shown even when their corresponding tabs were not activated. Again, this is because what we have here is technically still a scrolling container, not a Tabs widget.</p> <p>In my opinion, allowing the tab panels to be accessed by scrolling defeats the purpose of using Tabs to begin with. What is the purpose of the tabs if the content is already accessible without them? Selecting one tab does not really hide the tabpanels associated with the other tabs, it only scrolls it out of view. So this Tabs example only <em>partially</em> behaves like a Tabs widget, and partially like a typical scrolling container.</p> <p>Now, as we mentioned earlier, <strong>the <code>tabpanel</code> roles are hard-coded into HTML</strong> in this particular example. It is not the browser that is adding and exposing these roles.</p> <p>I don’t know <em>why</em> this example has the <code>tabpanel</code> roles hard-coded in. However, what I do know is that you, as a developer, are also expected to add these roles to your markup.</p> <p>This also means that you should be aware of the fact that these roles are missing in what is otherwise being exposed as a Tabs widget. <em>Yet</em>, as we mentioned earlier, these exposed semantics are not specified in the CSS specification.</p> <p>This is why it is very important for you to be responsible for the code you write/use and always, always check how it is exposed to screen reader users, and to test it to ensure that it is usable.</p> <p>And this is why it is critical that you understand the role of the accessibility tree and the information it carries, understand how ARIA roles work, as well as understand the requirements for the widgets you are creating to ensure they are operable and that they meet the user expectations.</p> <p>If you didn’t know that scroll markers are exposed as tabs and that you needed to add <code>tabpanel</code> roles to the widget you’re creating, then you would end up with a widget that’s broken in many ways, like the examples we examined earlier.</p> <!-- In other words, instead of creating_ elements from CSS, I would prefer if the specification was designed to hook into existing HTML elements when these elements map to meaningful roles, and then provide visual scroll marker affordances for these elements, without modifying the underlying structure and semantics.* --> <p>That being said, even hard-coding the <code>tabpanel</code> roles into your markup has its downsides because the <code>tablist</code> and <code>tab</code> roles are added via CSS. So what happens when CSS is not available? What happens if the user is viewing your page in Reader Mode, for example?</p> <p>What I think would be a little more foolproof is if the browser added these visual affordances and behavior <em>only</em> when all the required ARIA roles are present in the markup.</p> <p>In other words, the features defined in the specification could be made useful for <em>some</em> common use cases, not all—because one size almost never fits all; and then you would make sure you have the important accessibility bits taken care of in your markup.</p> <p>That being said, what I personally think we <em>really</em> need instead is a <strong>standardized</strong> HTML markup structure.</p> <p>Wouldn’t it be nice if we could just write HTML and have the browser <em>just know</em> what ARIA roles to expose to AT, and have it provide all the necessary keyboard interactions for free?</p> <p>We can already do that for native interactive elements like a <code><button></code> and a <code><a></code> and a <code><details></code> element, to name a few.</p> <p>So what we really need is native HTML elements with built-in semantics and interactive behavior for creating UI patterns that currently have no equivalents in HTML. This <em>includes</em> Tabs, sliders, and carousels.</p> <p>This brings me to the end of this examination. So, what’s the conclusion here?</p> <h2 id="conclusion-and-closing-thoughts" tabindex="-1">Conclusion and closing thoughts</h2> <p>All the issues I have covered in this post are specific to screen reader and keyboard navigation. I haven’t discussed how the tabs and scroll buttons could be cumbersome to operate for speech control users, particularly when they don’t have text labels. We haven’t talked about how the tabs could become invisible in Forced Colors Modes if they are styled using CSS background colors alone. And we haven’t tested the names of the tabs to see if they actually translate into other languages. And what happens when CSS is not available?</p> <p>As developers, we are responsible for the code we write. And we are responsible for testing the components we create.</p> <p>That said, I think the specification should be more explicit about how the new features it defines affects and <em>don’t</em> affect the accessibility of the content they are used on. That would make it easier for developers to know where there are gaps that need to be filled, and issues that need to be resolved before using these features in production.</p> <p>Knowing the capabilities <em>and</em> limitations of a new feature is critical to understanding when to use it and what it is appropriate for.</p> <p>In its current state, the specification adds a layer of abstration on top of HTML semantics that, dare I say, is quite risky, especially because these features are introduced as accessible by default.</p> <!-- The fact that these features were introduced as accessible because "the browser handles accessibility for you" is inaccurate at best, and misleading at worst. --> <p>There’s a lot the browser doesn’t currently do and that <em>you</em> need to take care of yourself if you want to use these new features in your projects.</p> <p>If you don’t know better, you could end up creating inaccessible and unusable user interface elements with these new features, all the while assuming that the browser is “taking care of accessibility” for you.</p> <p>While abstractions are often convenient for us developers, this <strong>convenience must not be delivered to us at the cost of user experience and accessibility.</strong> As responsible developers, it is on us to push back when necessary and require new features to be inclusive of the users we are creating <em>user</em> interfaces <em>for</em>.</p> <p>The browser is currently creating lists of scroll markers <strong>for our convenience</strong> and it exposes them as tabs, <em>regardless</em> of whether the pattern they are used to create is actually a Tabs widget or not. And it does that because—<em>surprise, surprise!</em>—CSS is not where semantics are defined. How does the browser know what an element is? It knows that from HTML.</p> <p>Semantics should be defined in HTML. And styles and visual affordances should follow from there.</p> <p>As I mentioned earlier, what I believe we need is native HTML elements with built-in semantics and interactive behavior for creating other UI patterns that currently have no equivalents in HTML. This includes Tabs, sliders, and carousels. And CSS could provide an additional layer of visual affordance on top of that. That would be great!</p> <p>The OpenUI has already started <a href="https://open-ui.org/components/tabs.research/">research on a native Tabs component</a> long ago, as well as a new <a href="https://open-ui.org/components/carousel.research/">carousel</a> and <a href="https://open-ui.org/components/slider.research/">slider</a> component. And there are current discussions already happening about <a href="https://open-ui.org/components/menu.explainer/">a native <code><menu></code> element</a> (which replaces the <em>current</em> HTML <code><menu></code> element which <a href="https://www.scottohara.me/blog/2021/10/21/menu.html">is essentially just a list</a>).</p> <p>It would be great if more resources were allocated for doing proper research and user testing for the work being done by the OpenUI group, so that these well-researched and accessibility-reviewed features are implemented sooner than later.</p> <h2 id="outro" tabindex="-1">Outro</h2> <p>So, there you have it. CSS Carousels are highly experimental, not currently accessible, and therefore, not ready for production.</p> <p>But this is not the only insight I want you to take away from this post. After all, this post isn’t merely about highlighting the current issues with CSS Carousels.</p> <p>Rather, it’s about <strong>awareness</strong>.</p> <p>If there is one thing you take away from this post let it be to learn how to <strong>think critically about new features</strong>, and to always question the accessibility and usability of a new feature before using it in production.</p> <p>Put your users front and center, and measure how useful a feature is by how it affects the usability of their interfaces. This is especially true for new features that have a direct impact on the accessibility information of the page.</p> <p>And how do you know if a feature affects the accessibility information of a page?</p> <p>Learn more about semantic HTML, and why it is important. Learn more about <em>what</em> makes semantic HTML accessible. Learn more about how ARIA affects HTML, and how it doesn’t! And learn about the proper use of ARIA in HTML.</p> <p>Then, learn about how CSS can affect accessibility.</p> <p>And most importantly, learn about your <em>users</em>, and all the diverse ways that they access the Web, and how the code you write affects their experience of the Web.</p> <p>There’s so much to learn and to be inspired by, and that will make you a better developer.</p> <p>I know this sounds overwhelming. But I promise you it’s not. Once you understand the foundations of accessibility, these things become second nature,and it becomes easier to spot accessibility issues and to fix most of them (if not all) on the spot.</p> <p>Feel free to use the knowledge we covered in this post to go over the rest of the examples, inspect their accessibility information, test them using keyboard and a screen reader, and get an idea of how usable they are. Maybe even try to have some fun by imagining how you could improve them and make them more usable. (That can sometimes be by <em>removing</em> features!)</p> <p><svg role="separator" style="display: block; max-width: 65%; margin: 4rem auto;" width="500px" height="32px" viewBox="0 0 794 51" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <polygon id="path-1" points="0.907103825 0 798.907104 0 798.907104 364 0.907103825 364"></polygon> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd"> <g id="36257-O0KTX6" transform="translate(0.000000, -212.000000)"> <g id="Group" transform="translate(-4.907104, 171.000000)"> <g id="Group-70"> <mask id="mask-2" fill="white"> <use xlink:href="#path-1"></use> </mask> <g id="Clip-67"></g> <path d="M610.198586,77 C699.286076,77 783.215114,73.4231956 849.907104,64.0083772 L849.765531,63.0342269 C783.152303,72.4392054 699.257163,76.0169938 610.198586,76.0160098 C468.900459,76.0169938 314.600519,67.0124993 195.629099,58.0080049 C136.142392,53.5062497 85.4880646,49.0035105 49.7059121,45.62744 C31.8148358,43.9389128 17.6415541,42.5318068 7.94178925,41.5478166 C3.09140831,41.0548375 -0.640344182,40.6681294 -3.15975068,40.40442 C-5.67816019,40.1407106 -6.98023573,40 -6.98422371,40 L-7.09289617,40.9780863 C-7.08292622,40.9790703 327.520578,77 610.198586,77 Z" id="Fill-68" fill="var(--hr-color)" mask="url(#mask-2)"></path> </g> <path d="M204.690104,47.32 C206.259104,42.729 211.015104,44.247 211.564104,45.397 C212.744104,47.867 213.564104,47.459 213.564104,47.459 C213.564104,47.459 211.475104,47.412 210.596104,49.717 C210.211104,50.727 208.816104,56.337 204.832104,58.949 C202.997104,60.153 200.700104,60.071 198.966104,61.194 C197.487104,62.163 196.569104,65.733 194.477104,65.784 C194.528104,65.733 194.579104,65.58 194.630104,65.479 C194.069104,65.835 193.355104,65.632 192.743104,65.632 C192.845104,65.376 192.590104,65.58 192.743104,65.325 C192.641104,65.428 192.641104,65.172 192.539104,65.223 C192.488104,64.764 192.284104,64.458 192.590104,64.101 C192.437104,64.254 192.590104,64.101 192.437104,64.203 C192.131104,63.285 196.633104,60.39 197.092104,57.891 C198.220104,51.746 202.522104,48.379 204.690104,47.32" id="Fill-73" fill="var(--hr-color)"></path> <path d="M237.461104,48.48 C240.188104,44.985 244.713104,48.584 244.314104,51.397 C248.502104,52.923 251.137104,63.152 251.621104,65.056 C251.621104,65.056 254.867104,70.497 254.733104,74.267 C254.250104,72.998 254.242104,74.368 253.553104,73.169 C253.483104,73.311 252.586104,72.323 252.517104,72.464 C251.439104,71.459 250.904104,67.897 250.103104,67.455 C249.301104,67.012 244.307104,65.92 241.012104,62.94 C237.824104,60.056 237.482104,56.195 237.693104,54.727 C238.377104,49.959 234.689104,50.147 234.689104,50.147 C234.689104,50.147 236.502104,49.709 237.461104,48.48" id="Fill-79" fill="var(--hr-color)"></path> <path d="M490.329073,53.2234708 C489.681073,52.5704708 487.329073,50.5564708 485.450073,51.0884708 C483.536073,51.6304708 481.516073,54.0154708 481.624073,56.5644708 L481.662073,56.5564708 C481.662073,56.5564708 480.546073,57.4964708 480.276073,57.7454708 C479.575073,58.3664708 475.473073,63.1004708 474.745073,69.2234708 C474.510073,71.1834708 474.352073,73.7664708 474.662073,75.0564708 C474.972073,76.3454708 473.722073,78.5464708 473.020073,79.9774708 C472.696073,80.5984708 469.438073,84.9064708 471.702073,83.9734708 C470.907104,85.7234708 472.329073,85.1394708 472.329073,85.1394708 C472.329073,85.1394708 471.827073,86.6204708 472.662073,86.6204708 C473.001073,86.6204708 473.995073,85.1394708 473.995073,85.1394708 C473.995073,85.1394708 473.995073,87.9734708 475.238073,84.4414708 C476.071073,87.7754708 477.917073,79.0904708 478.528073,77.9324708 C479.140073,76.7754708 481.459073,75.7964708 483.265073,74.8634708 C485.072073,73.9324708 486.663073,71.7724708 487.632073,69.7204708 C489.035073,66.8614708 489.662073,64.4804708 489.662073,61.3064708 C489.662073,54.2234708 492.971073,53.9734708 492.971073,53.9734708 C492.971073,53.9734708 490.976073,53.8764708 490.329073,53.2234708" id="Fill-87" fill="var(--hr-color)"></path> <path d="M177.695104,46.218 C179.252104,47.12 180.579104,51.973 180.214104,56.075 C179.708104,61.772 180.593104,64.226 180.581104,64.709 C180.567104,64.962 180.224104,63.94 180.126104,64.119 C180.028104,64.3 179.377104,66.022 179.377104,66.022 C178.314104,65.897 177.788104,64.477 177.788104,64.477 C177.788104,64.477 177.614104,65.198 177.439104,65.022 C176.716104,64.271 177.416104,61.669 177.165104,60.689 C176.433104,57.829 174.939104,58.128 173.502104,55.834 C171.625104,52.839 171.708104,49.822 172.127104,48.584 C172.433104,47.644 172.922104,46.91 172.226104,45.906 C171.746104,45.206 171.072104,45.652 170.403104,45.709 C170.714104,45.484 171.124104,45.445 171.434104,45.219 C171.788104,44.971 175.734104,39.39 177.695104,46.218" id="Fill-90" fill="var(--hr-color)"></path> <path d="M422.710104,55.938 C422.537104,55.574 421.710104,50.668 417.874104,51.317 C415.392104,51.738 413.710104,53.668 414.627104,57.084 C414.271104,59.936 412.264104,61.525 413.761104,67.05 C415.427104,73.204 419.112104,76.736 419.257104,78.691 C419.356104,80.243 419.127104,86.627 419.127104,86.627 C419.236104,86.528 420.370104,85.727 420.478104,85.628 C420.696104,85.909 420.845104,86.237 421.127104,86.501 C421.153104,86.355 421.604104,86.239 421.632104,86.093 C422.013104,86.465 422.516104,87.242 423.070104,87.497 C423.089104,87.079 423.173104,86.461 423.354104,86.134 C423.363104,86.406 424.202104,87.052 424.257104,87.243 C424.202104,87.052 424.678104,86.266 424.824104,85.33 C425.142104,85.72 426.640104,85.572 426.877104,85.918 C424.794104,82.584 424.141104,77.345 424.120104,75.908 C424.019104,72.43 426.519104,69.102 426.492104,65.673 C426.454104,60.903 422.710104,55.938 422.710104,55.938" id="Fill-98" fill="var(--hr-color)"></path> <path d="M302.845104,52.885 C302.750104,52.573 302.523104,47.483 298.347104,48.334 C296.386104,48.734 295.575104,51.737 296.049104,53.353 C295.437104,55.595 294.345104,56.846 294.877104,61.459 C295.470104,66.599 298.846104,68.692 298.732104,70.277 C298.630104,71.533 298.642104,73.546 298.598104,74.85 L298.535104,75.183 C298.632104,75.116 298.744104,75.156 298.841104,75.089 C298.980104,75.342 299.019104,76.129 299.210104,76.376 C299.248104,76.262 299.903104,75.988 299.529104,75.487 C299.785104,75.832 300.989104,77.493 301.170104,77.254 C301.146104,77.474 301.582104,76.552 301.603104,76.712 C301.582104,76.552 301.681104,76.558 301.906104,75.826 C302.110104,76.178 302.440104,76.195 302.585104,76.501 C303.798104,75.019 302.642104,71.989 302.793104,70.834 C303.120104,68.033 304.624104,64.088 305.002104,61.334 C305.529104,57.506 302.845104,52.885 302.845104,52.885" id="Fill-99" fill="var(--hr-color)"></path> <path d="M542.099104,60.139 C541.980104,59.748 542.402104,54.385 537.377104,54.3367902 C534.210104,54.307 533.793104,57.501 533.210104,60.084 C532.422104,62.891 530.212104,63.731 530.877104,69.501 C531.616104,75.928 535.338104,80.463 535.187104,82.446 C535.052104,84.019 534.894104,88.962 535.018104,88.878 C535.195104,89.194 535.379104,89.576 535.622104,89.884 C535.671104,89.741 535.845104,89.516 535.894104,89.374 C536.220104,89.805 537.677104,91.884 537.908104,91.584 C537.877104,91.859 538.835104,90.146 539.124104,89.228 C539.384104,89.669 539.485104,89.668 539.670104,90.051 C541.222104,88.192 540.271104,82.86 540.467104,81.415 C540.893104,77.911 543.928104,73.925 544.419104,70.48 C545.104104,65.688 542.099104,60.139 542.099104,60.139" id="Fill-100" fill="var(--hr-color)"></path> </g> </g> </g> </svg></p> <p>If you want to learn accessibility in-depth and learn how to find and fix accessibility issues by yourself, I have created a comprehensive, structured curriculum in the form of a self-paced video course that is aimed to equip you with the knowledge you need to confidently create more accessible websites and web applications today.</p> <p>The course is called Practical Accessibility, and you can enroll in it today at <a href="https://practical-accessibility.today/">practical-accessibility.today</a>.</p> <h2 id="update%3A-may-29th%2C-2025" tabindex="-1">Update: May 29th, 2025</h2> <p>This post started an excellent discussion on the topic of CSS Carousels within the community, and some have blogged their thoughts on their personal websites.</p> <p>Vale <a href="https://vale.rocks/micros/20250524-1145">shared her thoughts</a> on the topic asked the same question I asked when I first heard of them: <em>Why?</em></p> <blockquote> <p>It completely fails with regard to separation of concerns by using CSS for structure, rather than HTML. I don’t know how to address that other than asking <em>why</em>? I’ve noticed a lot of new CSS features, especially ones with the Chrome team’s influence, are getting a little too markup-y for my liking. David Bushell has covered this and the overreliance of pseudo-elements with touchings on the carousel kerfuffle.</p> </blockquote> <p>In David’s <a href="https://dbushell.com/notes/2025-05-07T09:51Z/">first Note</a> he mentions not being a fan of the overuse of pseudo-elements in CSS specifications, because <q>the developer ergonomics aren’t fun</q> and because <q>we’re losing “separation of concerns” with content and accessibility semantics getting mixed into CSS </q>.</p> <p>David shares more of his thoughts in <a href="https://dbushell.com/2025/05/23/pseudo-elements/">another post on pseudo-elements</a>:</p> <blockquote> <p>These demos are a pretty sales pitch but I see problems for practical use.</p> <p>Text content in CSS is code smell. A clear violation of separation of concerns. Content in CSS is a pain for content management systems. It plays havoc with translation tools too. These issues can be alleviated using attr() to move content back into HTML but not without pains.</p> <p>You know what I want to see? […] I just want to see HTML. We have interactive elements like <code><button></code>, <code><details></code>, and <code><dialog></code> these are immensely useful. Give us more primitives to build upon. […] Where pseudo-elements shine are implementations like ::selection and ::first-letter. Where it’s impossible or impractical to add additional HTML. As primitives to build a carousel they’re too limiting. They’re a dead-end.</p> <p>Can we stay in HTML, please?</p> </blockquote> <p>Adrian Roselli wrote a post with a general <a href="https://adrianroselli.com/2025/05/my-request-to-google-on-accessibility.html#Comments">request to Google on accessibility</a>:</p> <blockquote> <p>Please, if your team cannot explain how the thing satisfies all WCAG Success Criteria at Level AA, then don’t release the thing.</p> <p>If the thing is a new feature for the web platform (HTML, CSS, ARIA, SVG, etc.), then don’t even propose the thing until you have its WCAG conformance sorted.</p> <p>Then understand WCAG is the bare minimum, is only table stakes, and does not in itself guarantee the thing is accessible. Which means be prepared for the thing to still contain barriers that must be addressed before it goes any further. Which means you’ll field some questions.</p> </blockquote> <p>The post is not specifically about CSS Carousels, but CSS Carousels are one of the examples Adrian mentions of Google-led Web platform features that are not accessible.</p> <p>Adrian’s post does, however, include a series of updates—of which are specifically about CSS carousels and how the feature is progressing.</p> <h2 id="update%3A-august-19th%2C-2025" tabindex="-1">Update: August 19th, 2025</h2> <p>Since I published this post, a new property <a href="https://github.com/WebKit/standards-positions/issues/514">has been proposed</a> and added to the specification. This property is named <code>scroll-target-group</code> and it “<em>enriches HTML anchor elements functionality to match the pseudo elements one</em>”, which makes it possible to use the <code>:target-current</code> selector to highlight links when their respective targets are in view.</p> <p>Learn about this new property and how to use it in the <a href="https://www.sarasoueidan.com/blog/css-scrollspy/">“CSS-only scrollspy effect using scroll-marker-group and :target-current”</a> article.</p>
Are 'CSS Carousels' accessible?
Read the full piece at https://www.sarasoueidan.com/blog/css-carousels-accessibility/