<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Josh Allan on Medium]]></title>
        <description><![CDATA[Stories by Josh Allan on Medium]]></description>
        <link>https://medium.com/@joshhallan?source=rss-d2f146dad6b6------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*Gf5V9GMGG8v5BALNW37oLg.jpeg</url>
            <title>Stories by Josh Allan on Medium</title>
            <link>https://medium.com/@joshhallan?source=rss-d2f146dad6b6------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 10:44:41 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@joshhallan/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Accessibility by Default: Single Page Applications]]></title>
            <link>https://levelup.gitconnected.com/accessibility-by-default-single-page-applications-0097ddf2d9e4?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/0097ddf2d9e4</guid>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[html]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 24 Sep 2026 12:19:21 GMT</pubDate>
            <atom:updated>2026-09-25T14:26:08.720Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dLVmLyghlrDNKasOWDhJHA.jpeg" /></figure><p>Hi and welcome to Accessibility by Default. If you’re a returning reader, thank you so much for coming back, and if you’re a new reader, welcome in!</p><p>Our past articles have been deep dives into <a href="https://www.designsystemscollective.com/accessibility-by-default-comboboxes-9ed5fc6ab3cb">comboboxes</a>, <a href="https://medium.com/@joshhallan/accessibility-by-default-links-e4df59815743">links</a>, <a href="https://medium.com/@joshhallan/accessibility-by-default-buttons-5e7214da9e4c">buttons</a>, as well as looking into <a href="https://medium.com/@joshhallan/accessibility-by-default-dialogs-be60a3708d29">dialogs</a>. So, seeing as we know how to create our components accessibly, I think it’s time that we have a look at actual application accessibility and what we need to think about when we put those components together to create something bigger.</p><h3>What is a single page application?</h3><p>A single page application (SPA) is a web application that can update the content of the page and change the URL without requiring the browser to reload the page.</p><p>Really, it will look like you’ve navigated to a new page, with the content updating and the URL changing, but it’s JavaScript that updates the content on the page and updates the URL to reflect the new location within the application.</p><p>And if you’re not really navigating away from where you currently are, but everything is changing, therein lies our first accessibility issue — how does someone using assistive technology know that everything on the page has changed without the page actually reloading?</p><h3>Accessibility Considerations</h3><h4>Routing</h4><p>Remember that links should be used to navigate and buttons should be used for actions. Just because you’re not actually loading a new page (but you sort of are), you should still use a link for navigating the user throughout the application. Even if your framework gives you a routing function that you can call from a click handler, that doesn’t mean you should turn everything into a button.</p><p>As mentioned, the content updates as well as the URL, so make sure they both reflect appropriately where the user is. If the user is on a dashboard, the URL should reflect that. Again, just because the user is (sort of) staying in the same place, that doesn’t mean that the URL should stay the same.</p><p>And if you are updating the URL, that means that the browser history should work, so the backwards and forwards buttons should behave as expected.</p><p>Remember that SPA routing should still feel like navigating to the user — even if you’re technically staying in the same place.</p><h4>Focus Management</h4><p>After the route changes, the question then becomes — where does the focus go?</p><p>Realistically, because the browser hasn’t reloaded the page, but has just updated the content, we need to think about the most appropriate place to move the focus to. That could be the main page heading, or the main content of the new “page” in some cases, but please don’t just move the focus around willy nilly.</p><p>We should give the user a clear starting point for the new content without unexpectedly throwing their focus somewhere else.</p><h4>Page Titles</h4><p>As we’re navigating around and the route is changing, as well as thinking about where the focus goes, we also need to think about the page title.</p><p>If we think of an online store scenario — we’re looking at buying some shoes, so we’re on the shoes page. We click to add them to the basket and then we want to proceed to the checkout.</p><p>When we move from the shoes page to the checkout page, we need the page title to communicate that change. If the page title is just “Josh’s Online Shop” continuously, that isn’t going to communicate very well. We need something like “Josh’s Online Shop — Shoes” or “Josh’s Online Shop — Checkout”.</p><p>This can be one of those things that’s easy to miss when you’re primarily thinking about what the page looks like. The content has changed, the URL has changed, and visually everything might look completely different, but if the page title hasn’t changed, we’re missing another important piece of information about where the user currently is.</p><h4>Announcements</h4><p>Now that we’re on a new “page”, the focus has moved to the most appropriate place and we’ve updated the page title, we now need to think about how we announce to the user that all of this has happened.</p><p>We must remember that just because visually the page has changed and been updated, that doesn’t mean that someone using assistive technology will know what has changed.</p><p>If I click a link to go from the Shoes page to the Checkout page, I can see that the content has changed, I can see that the URL has changed, and the page title has changed. But what does the screen reader user get told?</p><p>Ideally, they should have some indication that navigation has happened and that they’re now looking at different content. This could be through moving focus to an appropriate location, as we’ve already discussed, but there are also situations where we may need to provide an announcement to the user.</p><p>This is where things like live regions can come in handy. We can use them to communicate dynamic changes to someone using assistive technology without having to move the focus.</p><p>But this is where we need to be careful. We don’t want to announce every single change that happens on the page, because that can quickly become a lot for the user and make the application difficult to use.</p><p>The questions become — what actually needs to be communicated, who needs to know about it, and when it needs to be announced.</p><p>We’ll take a much deeper look at live regions, announcements and communicating dynamic content in a future article (SPOILER ALERT), but for now the important thing to remember is that just because something has changed on screen, that doesn’t mean everyone using your application has been told about it.</p><h4>Scroll Restoration</h4><p>When a user navigates from one page to another, we need to consider how far down the original page they had scrolled before they navigated and where they should go when they land on the new page.</p><p>If you consider that a user is halfway down a page and they then click a link to navigate to a new page, the route has changed and the new content has appeared. Where should the user be in the scroll?</p><p>We also need to consider what happens if they decide to navigate back to the original page. Should they be taken back to where they were before, perhaps halfway down the page where the link they clicked was, or should they be taken back to the top of the page?</p><p>This is something that sighted users will notice immediately, but to someone using assistive technology, unexpected scroll behaviour can also make the new content harder to understand and navigate. This is all a part of creating and maintaining a predictable navigation experience.</p><p>Ideally, when navigating to a new page, the user should be taken to an appropriate starting position, usually the top of the new content. When navigating back or forward, however, restoring the previous scroll position can provide a much more predictable experience, particularly when returning to a page where the user had already been browsing.</p><p>As with focus management, though, this isn’t something we should blindly apply to every navigation. The behaviour should make sense for the type of navigation that’s taking place.</p><h3>Closing thoughts</h3><p>As you can see, building accessible components is only one part of the job. Once those components become part of an application, that is when we need to think about what happens when the application changes around the user.</p><p>In a traditional application, the browser will handle a lot of this for us when a new page loads. In a single page application, we’re taking more responsibility for that behaviour ourselves.</p><p>And if we’re taking responsibility for it, we need to make sure we’re doing it in a way that works for everyone.</p><blockquote>Accessibility by Default is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0097ddf2d9e4" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/accessibility-by-default-single-page-applications-0097ddf2d9e4">Accessibility by Default: Single Page Applications</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Carousels]]></title>
            <link>https://levelup.gitconnected.com/accessibility-by-default-carousels-7af502243213?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/7af502243213</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[frontend-development]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 17 Sep 2026 06:01:01 GMT</pubDate>
            <atom:updated>2026-09-18T14:10:53.700Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*svSBOxIlh31XS3iJmWyxRQ.jpeg" /></figure><p>Hi, and welcome back to Accessibility by Default. If you’re new here, welcome in, and if you’re a returning reader, thank you so much for coming back. This week we’re going to be looking at the implementation of the carousel component.</p><p>Don’t implement it. Just don’t do it. It’s an accessibility nightmare and will cause more headache and frustration for you than what it’s worth.</p><p>The end.</p><p>Thanks for stopping by. See you next week!</p><p>Why are you still here? There’s nothing else, no more than that.</p><p>Ok fine, right, let’s just talk about it, because if you really <em>really</em> have to implement one, you’ll have to do it correctly.</p><h3>What is a carousel?</h3><p>A carousel is a collection of related pieces of content, often referred to as slides, that users can move through one at a time. These might contain things like images, text, cards or other content.</p><p>Users can move through the different items using controls such as previous and next buttons, pagination indicators or other interaction methods.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-dWU5eo6RJ3k6GCekxwGng.png" /></figure><p>A carousel isn’t directly inaccessible, just a badly implemented carousel is.</p><h3>When should you use it?</h3><p><strong>You shouldn’t. Don’t.</strong></p><p>Ok, sorry, I’ll stop making that joke now.</p><p>Carousels can be useful when you have a collection of related content that you want to present in a limited amount of space. For example, you might have a set of related promotional cards or a gallery of images where displaying everything at once would take up a significant amount of space.</p><p>But just because you can put something in a carousel doesn’t mean you should.</p><p>Before reaching for a carousel, ask yourself whether hiding some of the content behind previous and next controls actually improves the experience for the user.</p><p>If the content is important, would it be better to show everything at once?</p><p>If the user needs to compare several items, would a grid or list be easier?</p><p>If the content is constantly changing, is automatic rotation actually helping the user, or is it just making things harder to follow?</p><p>A carousel should be used because it genuinely improves the user experience, not simply because someone decided that there wasn’t enough room on the page.</p><h3>Accessibility considerations</h3><h4>Don’t automatically rotate content</h4><p>I think that this is the main one when we’re looking at the accessibility behind carousels.</p><p>If the carousel is going to automatically move through the slides then you need to give the users a way to pause or stop it. I’ve seen so many carousels that automatically rotate and, to me, that’s annoying. There have been so many times that I haven’t even processed the information before it’s automatically moved on. And if the user is trying to interact with something on the current slide but then it suddenly changes, that isn’t going to be a great user experience either.</p><h4>Give users control</h4><p>If you must implement a carousel, then the users should be able to navigate through it manually. The previous and next controls should be keyboard accessible and they should have clear accessible names. “Previous slide” and “Next slide” are going to be much more useful to screen reader users than just using “Previous” and “Next”. Screen reader users need to have additional context for what the buttons are going to achieve.</p><p>And, if you’re going to use pagination indicators, the current slide should also be communicated to the user.</p><h4>Keyboard interaction</h4><p>As always, a user shouldn’t need a mouse or touchscreen to use the component.</p><p>Every interactive control within the carousel needs to be keyboard accessible, and the user should be able to move through the carousel without getting trapped inside it.</p><p>If the carousel contains links or buttons within each slide, those need to be reachable in the expected tab order too. If a slide contains multiple tabbable elements, tabbing past the last one should move focus on to the next control in the carousel, such as the “next slide” button, rather than looping back to the start of the same slide, and rather than jumping the user out to unrelated content elsewhere on the page.</p><h4>Focus management</h4><p><strong>This</strong>. Now this is where the fun begins.</p><p>When the carousel slide changes, you shouldn’t start throwing the focus around willy nilly. If a user is interacting with the carousel and then we move the focus to somewhere else every time the slide changes, that experience is going to be so bad for the user.</p><p>Focus should <em>generally</em> remain where the user expects it to be, and changing the visible slide shouldn’t unexpectedly steal focus from the user.</p><h4>Make the current slide clear</h4><p>The user needs to know which slide they’re currently on, especially if you have several.</p><p>For example, if you have five slides, someone using a screen reader should be able to know that they’re currently looking at slide 2 of 5.</p><p>We also need to make sure that the content from the inactive slides isn’t being exposed in a way that makes the experience confusing. If a slide is no longer active, we need to consider whether the content should still be available to assistive technology.</p><p>This is one of those areas where adding aria-hidden=&quot;true&quot; can cause problems. If the slide contains focusable content, we need to make sure we&#39;re not hiding something from assistive technology while still allowing keyboard users to reach it.</p><h4>Don’t forget reduced motion</h4><p>If the carousel uses animations or transitions when moving between slides, we should respect the user’s reduced-motion preference.</p><p>CSS gives us a way to detect this with prefers-reduced-motion, allowing us to reduce or remove animations for users who have requested less motion.</p><p>There’s not much point in making our carousel technically accessible if we’re then making someone feel like they’re on a fairground ride whenever the slide moves.</p><h4>Touch and swipe</h4><p>A lot of carousels rely on swiping on mobile devices, but we shouldn’t make that the only way to navigate them.</p><p>Touch users should still have access to clear controls, and any swipe interaction should work alongside the other methods of navigation rather than replacing them.</p><p>Something to keep in mind is the conflict between swipe gestures and screen reader navigation. Both VoiceOver and TalkBack use swipe gestures to move between elements on a page, which can clash with a carousel’s own swipe to change slide behaviour. It’s usually best to provide visible on-screen buttons as an alternative, and make sure they’re not too small or easily missed because if users can’t find them, they can’t use them.</p><h3>Final Thoughts</h3><p>Carousels aren’t inherently inaccessible. They can be made accessible, but they do introduce a lot of additional things to think about that you might not need to think about if you were to use something else like a list, a grid or just showing all of the content.</p><p>So, before you start building one, just stop and ask yourself whether you actually need it and whether the trauma is going to be worth it.</p><p>If you do, then make sure you’re thinking about how it works for everyone, not just how it looks when someone clicks through it with a mouse.</p><p>And if you don’t need to use one?</p><p>Smile and be happy.</p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7af502243213" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/accessibility-by-default-carousels-7af502243213">Accessibility by Default: Carousels</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Sliders and Tooltips]]></title>
            <link>https://levelup.gitconnected.com/accessibility-by-default-sliders-and-tooltips-f4b5014980f8?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/f4b5014980f8</guid>
            <category><![CDATA[frontend-development]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 10 Sep 2026 10:05:58 GMT</pubDate>
            <atom:updated>2026-09-11T14:27:48.433Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*veCs6-8xOhB8HnW_m1v1xA.jpeg" /></figure><p>Hi, and welcome back to Accessibility by Default. If you’re new here, welcome, and if you’re a returning reader, thank you so much for coming back.</p><p>If you’re a regular reader of the series, you may have noticed things have been a little quiet over the past couple of weeks. I’ve been on holiday with my family, and I decided to take a proper break — no work (only <em>occasionally </em>checking emails and teams), no Medium, no accessibility, and no worrying about what I should be writing next.</p><p>It was really important to me to properly switch off, spend some time with my family and come back feeling refreshed rather than heading straight towards burnout.</p><p>Now that we’re back, though, it’s time to get stuck in. This week we’re going to be looking at sliders and tooltips.</p><p>So, these are two components that, as always, look fairly simple on the surface. One is essentially a control for selecting a value, while the other is commonly used to provide additional information when a user hovers over or focuses on something.</p><p>They <em>can</em> seem simple, but both components can become surprisingly complex when you start looking at the accessibility behind them. There’s a lot more involved than simply making them look and behave correctly visually. We also need to consider how they function for people using assistive technology, keyboard users, touch users, and anyone else who may interact with them in a different way.</p><h3>What is it?</h3><h4>What is a slider?</h4><p>When we talk about sliders we immediately think of an input range bar. It allows you to choose a number from a predefined range, usually by dragging a handle along a track.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/464/1*ZjWIJ_U4Blo_UZSX6QEqrw.png" /></figure><h4>What is a tooltip?</h4><p>A tooltip gives the user additional information about an element without requiring them to navigate away from what they’re currently doing. It’s usually displayed when an element is hovered or receives keyboard focus.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/547/0*CDQyE8YjrDqJhkhB" /></figure><h3>When to use it</h3><h4>Sliders</h4><p>A slider is a useful input when a user needs to select a value from a continuous or set range, usually when the exact value isn’t the most important part of the interaction.</p><p>A common example of a slider would be selecting a volume level. A user might want the volume to be somewhere between 0 and 100, but they don’t necessarily care about the exact number (unless you’re my wife who <strong>must </strong>have the volume at a number divisible by 5). Being able to move the slider along a track can give a quick and intuitive way of making their selection.</p><p>Other examples might include things like:</p><ul><li>Volume controls</li><li>Brightness</li><li>Price ranges</li><li>Zoom levels</li><li>Playback position</li><li>Adjusting image or video settings</li></ul><p>However, there is an important point to remember here — a slider isn’t necessarily the best choice just because you have a number. If the user needs to enter an exact value such as their age, their postcode or a specific monetary amount; a text or number input is probably a better choice. Imagine asking a user to drag a slider to exactly £57.24, that isn’t going to be the best user experience.</p><h4>Tooltips</h4><p>Tooltips are useful when you need to provide short, contextual information about something on the page without interrupting the user’s current task.</p><p>For example, a button containing an unfamiliar icon might have a tooltip explaining what the button does. A form control might have a short piece of additional information that can help the user to understand its purpose.</p><p>But, tooltips aren’t a replacement for visible labels or instructions. If the information is important for the user to complete the task you shouldn’t be hiding it behind a tooltip. The same applies if the content is lengthy, interactive or if the user is required to spend significant time reading it. In those situations you’re best off going with a popover or dialog (which you can <a href="https://medium.com/@joshhallan/accessibility-by-default-dialogs-be60a3708d29">read about in my previous post</a>)</p><h3>Accessibility considerations</h3><h4>Sliders</h4><p>As this is what I say with all of my component deep dives — if you can use native elements, then you should. When you consider how much &lt;input type=”range”&gt; actually gives you</p><ul><li>A recognised form control</li><li>A semantic role</li><li>Keyboard interaction</li><li>Focus behaviour</li><li>A value that can be exposed to assistive technology</li><li>Browser/OS accessibility integration</li></ul><p>Remember, the less accessibility that we have to code ourselves means less opportunities for us to get it wrong</p><p>For both native and custom sliders, these are the main accessibility considerations to keep in mind</p><p><strong>Give the slider an accessible name</strong></p><p>As with all inputs, the input should have a label that tells the user the purpose</p><pre>&lt;label for=&quot;volume&quot;&gt;Volume&lt;/label&gt;<br>&lt;input id=&quot;volume&quot; type=&quot;range&quot; min=&quot;0&quot; max=&quot;100&quot; value=&quot;50&quot;&gt;</pre><p>A screen reader might announce something along the lines of “Slider, 50”, but that gives the user no indication of what the slider controls. Ideally, it should be announced with its accessible name as well, for example “Volume, slider, 50”.</p><p><strong>Make the value meaningful</strong></p><p>For a native range input, the browser handles a lot for us. But for a custom solution we have to expose</p><pre>role=&quot;slider&quot;<br>aria-valuemin=&quot;0&quot;<br>aria-valuemax=&quot;100&quot;<br>aria-valuenow=&quot;50&quot;</pre><p>And sometimes aria-valuetext if the numerical value isn&#39;t meaningful by itself.</p><p>For example, a temperature slider might expose:</p><pre>aria-valuenow=&quot;20&quot;<br>aria-valuetext=&quot;20 degrees Celsius&quot;</pre><p>Although, I’d be cautious about using aria-valuetext unnecessarily when you have a numerical value that already communicates everything to the user.</p><p><strong>Keyboard interaction</strong></p><p>This is a big one for custom sliders.</p><p>A user shouldn’t have to use a mouse or touch screen to operate a slider.</p><p>At minimum, we need to think about:</p><ul><li>Arrow Up / Arrow Right → increase</li><li>Arrow Down / Arrow Left → decrease</li><li>Home → minimum</li><li>End → maximum</li></ul><p><strong>Focus</strong></p><p>The user should be able to focus on the slider and it should be obvious where the focus is. As always, with native control we get this built in.</p><p>With a custom component, we have to make sure that the interactive element can receive focus — usually by making the slider itself focusable with tabindex=&quot;0&quot;</p><p><strong>Orientation</strong></p><p>A horizontal slider is fairly obvious as that is pretty much the standard. But once we start creating vertical sliders, that is where things start to get interesting.</p><p>A custom slider needs to communicate its orientation and the keyboard behaviour needs to be predictable. That is where aria-orientation=&quot;vertical&quot; comes in.</p><p>This isn’t just about adding the attribute for the fun of it. The visual and keyboard behaviour need to agree with what we’re exposing.</p><h4>Tooltips</h4><p>Tooltips have a completely different set of problems.</p><p><strong>Don’t rely on hover</strong></p><p>I think that this is probably the biggest one when you think about the accessibility considerations surrounding this component. If the tooltip only appears when the mouse hovers over it, then people who use keyboard navigation may never see it.</p><p>Anything that triggers a tooltip via hover should also make it available when the associated element gets focus as well.</p><p>That gives us:</p><p><strong>Mouse:</strong> hover = tooltip<br><strong>Keyboard:</strong> focus = tooltip</p><p><strong>Don’t use title as your tooltip implementation</strong></p><pre>&lt;button title=&quot;Delete item&quot;&gt;</pre><p>The browser may display a native tooltip, but we have very little control over its behaviour or consistency.</p><p>We can’t reliably control things like:</p><ul><li>How long it stays visible</li><li>How it looks</li><li>How it behaves on touch devices</li><li>How it behaves with keyboard interaction</li><li>Whether its content is exposed in the way we actually need</li></ul><p>Remember, title shouldn’t be used as a replacement for a proper accessible name or description</p><p><strong>Make the relationship between the element and tooltip clear</strong></p><p>If the tooltip provides additional information about an element, assistive technology needs a relationship between the two.</p><p>This is when we use aria-describedby</p><pre>&lt;button aria-describedby=&quot;tooltip&quot;&gt;<br>  Delete<br>&lt;/button&gt;<br>&lt;div id=&quot;tooltip&quot; role=&quot;tooltip&quot;&gt;<br>  Permanently deletes this item<br>&lt;/div&gt;</pre><p>Now the tooltip can provide additional information about the button rather than becoming the button’s name.</p><p><strong>Keyboard dismissal</strong></p><p>A tooltip shouldn’t trap the user or require them to perform some awkward interaction to get rid of it.</p><p>For a tooltip triggered by focus, moving focus away should cause it to disappear.</p><p>For hover-triggered tooltips, the user should be able to move the pointer between the trigger and tooltip where appropriate without the tooltip vanishing immediately.</p><p><strong>Timing</strong></p><p>Tooltips need to remain visible for long enough for the user to actually read them.</p><p>We shouldn’t make them disappear the instant the pointer moves slightly away from the trigger, particularly if the tooltip itself needs to be reached with the pointer.</p><p>We also need to consider users who may need more time to read or interact with the content.</p><p><strong>Touch and mobile</strong></p><p>Hover isn’t really an interaction available on a touchscreen in the same way it is with a mouse.</p><p>If the information contained within a tooltip is important, we shouldn’t rely on hover as the only way of accessing it. We need to consider how the same information will be made available to someone using a touch device.</p><h3>Final Thoughts</h3><p>Sliders and tooltips are very different components, but they share the same accessibility lesson: the visual component isn’t the whole component.</p><p>A slider isn’t accessible because it looks like a slider.</p><p>A tooltip isn’t accessible because it appears when you hover.</p><p>We need to consider semantics, interaction, keyboard access, focus, assistive technology and the actual information being communicated.</p><p>I know that this is basically the philosophy of the series, but I think that it’s worth repeating. Whether you’re building a slider, a tooltip, or any other component, accessibility isn’t something that should be added once the component is finished. The way the component is named, focused, operated and communicated to assistive technology is part of the component itself.</p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f4b5014980f8" width="1" height="1" alt=""><hr><p><a href="https://levelup.gitconnected.com/accessibility-by-default-sliders-and-tooltips-f4b5014980f8">Accessibility by Default: Sliders and Tooltips</a> was originally published in <a href="https://levelup.gitconnected.com">Level Up Coding</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Comboboxes]]></title>
            <link>https://www.designsystemscollective.com/accessibility-by-default-comboboxes-9ed5fc6ab3cb?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/9ed5fc6ab3cb</guid>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[frontend-development]]></category>
            <category><![CDATA[html]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 13 Aug 2026 06:01:02 GMT</pubDate>
            <atom:updated>2026-08-14T13:59:44.253Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F-BaABkHf7PDv02ERfcU6g.png" /></figure><p>Hi, and welcome back to Accessibility by Default. If you’re new here, welcome! If you’re a returning reader, thank you so much for coming back.</p><p>This week, we’re going to look into comboboxes. I feel that, on the surface, these components can appear very simple, but when you actually start to examine the functionality and accessibility behind them, it can become quite complicated.</p><h3>What is it?</h3><p>So, what do we mean by a combobox?</p><p>A combo box is a bit like if an input box and a select box had a baby, and then that baby was babysat by an accordion because it takes bits and pieces of all three of those components and puts them into one.</p><p>It has the look of an input, the selection behaviour of a select, and some of the disclosure and state concepts you see in an accordion.</p><p>At its simplest, a combobox is an input associated with a popup containing possible values. That popup might contain suggestions that change as the user types, or it might contain a predefined set of values that the user can choose from.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/580/1*wJyB08r7QXUiAeNqLAPlQA.gif" /></figure><h3>When to use it?</h3><p>The main use of these components is to improve the user experience by allowing users to type into the input while the space below populates with relevant, selectable options.</p><p>A good use case for this component is when a user needs to select their country from a large list. Rather than making them scroll through a huge list of countries, they can start typing, and the combobox can filter the available options to show relevant suggestions. This is an example of a combobox with list autocomplete, where the suggestions change based on what the user types.</p><p>Comboboxes can also be useful when users are allowed to enter their own value, but suggestions are provided to help them get there more quickly. Search fields are a good example of this.</p><p>Comboboxes should be used to solve a real problem. If the user simply needs to select one value from a small, predictable list, a native &lt;select&gt; may be a much better option.</p><p>We should also understand the difference between a combobox and a filterable dropdown. Both components can look similar, but each has its own purpose. A combobox allows the user to enter a value while providing suggestions or options through a popup. A filterable list is primarily used for narrowing down an existing set of options, rather than allowing the user to enter an arbitrary value. Before trying to build and implement a combobox, think about what the user is trying to do.</p><h3>Accessibility Considerations</h3><p>When we look at the accessibility behind comboboxes, there are some similarities to accordions and disclosure that we’ve spoken about in <a href="https://medium.com/@joshhallan/accessibility-by-default-tabs-accordions-and-disclosures-08c7ee1103f8">my tabs, accordions and disclosures article</a>, mostly around opening and closing the popup. But a combobox <em>does </em>have its own interaction and accessibility requirements.</p><h4>Aria roles</h4><p>Initially, your input should have role=&quot;combobox&quot; and aria-expanded=&quot;false&quot;. When the popup containing the suggestions is opened, aria-expanded should change to true.</p><p>As with all input boxes for proper accessibility, it should have an associated label with it (check out <a href="https://medium.com/@joshhallan/accessibility-by-default-form-controls-87210f277441">my form controls article here</a>). And then we have aria-autocomplete, which should match the autocomplete behaviour of your combobox.</p><p>There are four possible values for the aria-autocomplete:</p><ul><li>None — the popup contains options, but what appears doesn’t change based on what the user types</li><li>List — the popup presents a list of suggestions that correspond to what the user has typed</li><li>Inline — the component provides an inline completion for the value the user is entering</li><li>Both — You get filtered suggestions and the inline completion.</li></ul><p>These values describe different types of combobox functionality, so you need to make sure that the value matches the behaviour your component actually provides.</p><p>aria-activedescendant identifies which option is currently active while focus remains on the combobox input. This allows users to move through the suggestions with the arrow keys without actually moving the DOM focus into the listbox.</p><p>The popup itself should have an appropriate role. For the example we’re looking at, that’s role=&quot;listbox&quot;, with each suggestion represented by an option.</p><p>The currently active option and the selected option aren’t necessarily the same thing. aria-activedescendant identifies the option currently being navigated to, while aria-selected communicates selection state where the pattern requires it.</p><p>Here’s a simplified example:</p><pre>&lt;label for=&quot;country&quot;&gt;Country&lt;/label&gt;<br>&lt;div class=&quot;combobox&quot;&gt;<br>  &lt;input<br>    id=&quot;country&quot;<br>    type=&quot;text&quot;<br>    role=&quot;combobox&quot;<br>    aria-expanded=&quot;false&quot;<br>    aria-controls=&quot;country-options&quot;<br>    aria-autocomplete=&quot;list&quot;<br>    /&gt;<br>  &lt;ul id=&quot;country-options&quot; role=&quot;listbox&quot; hidden&gt;<br>    &lt;li<br>      id=&quot;country-option-1&quot;<br>      role=&quot;option&quot;<br>      aria-selected=&quot;false&quot;<br>      &gt;<br>      Canada<br>    &lt;/li&gt;<br>    &lt;li<br>      id=&quot;country-option-2&quot;<br>      role=&quot;option&quot;<br>      aria-selected=&quot;false&quot;<br>      &gt;<br>      Cambodia<br>    &lt;/li&gt;<br>    &lt;li<br>      id=&quot;country-option-3&quot;<br>      role=&quot;option&quot;<br>      aria-selected=&quot;false&quot;<br>      &gt;<br>      Cameroon<br>    &lt;/li&gt;<br>  &lt;/ul&gt;<br>&lt;/div&gt;</pre><p>In your JavaScript, you need a state mechanism that will keep the attributes in sync. When the user focuses on the input and starts to type, the script should toggle the aria-expanded to true, remove the hidden attribute from the listbox, and then change the aria-activedescendant to match the option index the user uses the arrow keys to navigate to. Then, when they blur or press the escape key, it will revert them so that aria-expanded is false, listbox receives hidden again, and aria-activedescendant is cleared.</p><p>Remember that aria-controls should reference the popup that the combobox controls. It is valid for the referenced element to be hidden when the combobox is collapsed. A listbox is the default popup type for a combobox, so aria-haspopup=&quot;listbox&quot; is implied and doesn&#39;t need to be added in this example. If you were using a different popup type, such as a grid, tree, or dialog, then aria-haspopup should identify that type.</p><h4>Keyboard interactions</h4><p>This is where a combobox can become more complicated than a normal input. When the user is focused on the combobox, the down arrow can open the popup and move the active option to the first option in the dropdown, and the up arrow can be used to navigate to the last option in the dropdown. Once the user is navigating the list of options the up arrow and down arrow should move between options, and enter will accept the currently active option. If the user presses Escape, then the popup should be closed (if it is open). When an autocomplete suggestion is active, Enter will accept that suggestion and update the value of the combobox.</p><p>The main thing to remember here is that behaviour can vary depending on the type of combobox you’re implementing, but keyboard interaction should be predictable and consistent.</p><h4>Focus management</h4><p>Focus management, in my opinion, is one of the hardest things when it comes to comboboxes. These are the things that you must remember</p><ul><li>Focus is initially owned by the input</li><li>Arrow keys are used to change the active option</li><li>aria-activedescendant is used to communicate the active option while the DOM focus remains on the input</li><li>Focus doesn’t necessarily move into the listbox</li><li>Closing the popup should return you to a predictable state, such as returning the user to the input box</li></ul><p>One of the important things to understand here is the difference between DOM focus, visual focus, and selection. A user can be moving through the suggestions while the browser’s actual focus stays on the input. By using aria-activedescendant, this allows assistive technologies to understand which option is active.</p><p>This is the part where the component can look visually correct, but it can become confusing or unusable for assistive technologies if the state isn’t correct.</p><h4>What happens when an option is selected?</h4><p>Another thing that we need to think about is what happens when the user selects one of the options. In the simplest example, the value of the selected option becomes the value of the input, and the list of selectable options closes so that the user can continue interacting with the rest of the page.</p><p>But this isn’t always quite as straightforward because we also need to consider whether the user needs to select one of the suggested options or whether they can enter their own value. If we go back to the country example, we don’t want the user to enter something that isn’t a country, so the suggestions are there to help them find the correct option.</p><p>On the other hand, if we’re using the combobox for something like a search field, the suggestions might just be there to help the user find what they’re looking for. They should still be able to enter their own search term, even if it isn’t one of the suggestions.</p><p>This is a distinction to make when designing the component because a combobox isn’t necessarily just a fancy select box. The behaviour of the component needs to match what we’re actually asking the user to do.</p><p>And if all we need is for the user to select one value from a predefined list, we should also ask ourselves whether we need a combobox at all.</p><p>A native &lt;select&gt; already gives us a well-understood and accessible interaction without having to recreate all of that behaviour ourselves.</p><p>Sometimes, the most accessible solution is simply not building the complicated thing in the first place.</p><h4>Announcements and dynamic updates</h4><p>When a user types into the input, the suggestions can change, the popup can open or close, the active option can change, and the selected value can be updated. For someone using a screen reader, we need to make sure they have enough information to understand what is happening.</p><p>This doesn’t mean that we should announce every single change. In fact, doing that could make the experience incredibly noisy and frustrating for someone using a screen reader.</p><p>Instead, we need to make sure that the state changes are exposed correctly through the combobox pattern.</p><p>For example, if a user types Can and a list of suggestions appears, they should be able to understand that suggestions are available and then use their arrow keys to move through them. As they move through the list, the active option should be communicated to them, and when they make a selection, the updated value should be available to assistive technology.</p><p>We also need to think about situations where the suggestions are loaded dynamically. If the user types something and the component needs to make a request before displaying the results, we shouldn’t leave the user wondering whether anything is happening.</p><p>Loading, no results, and updated suggestions are all states that need to be considered as part of the component.</p><p>The thing to remember is that accessibility isn’t just about making the options available — it’s about making the state of the component understandable as it changes.</p><h3>Common mistakes</h3><p>I think that this is one of those components that can be very JavaScript heavy, simply because of the interactions and the amount of state that needs to be managed.</p><p>You need to update different states depending on what the user is doing with the combobox. If they’re actively typing, you need to populate the suggestions and update the popup state. If they’re navigating through the options, you need to keep the active option and aria-activedescendant in sync. You also need to make sure the keyboard interactions work as expected while keeping focus predictable.</p><p>Another common mistake is that screen readers aren’t being given enough information about what has changed.</p><p>If suggestions appear, disappear, or change without the component exposing the appropriate state, users can be left wondering what is happening. Focus can also become problematic if it unexpectedly moves to the wrong element or disappears altogether.</p><p>And the biggest one is creating and using this whole element when you don’t need to.</p><p>If you have a selection of 20 different known and predictable values, you probably don’t need a whole combobox implementation. You could probably get away with using a native &lt;select&gt; with those options.</p><p>However, if you’ve got a list of over 100 countries to choose from, I’d help the user out by allowing them to narrow down their selection by using a combobox.</p><h3>Key takeaways</h3><p>My key takeaway from this component is that you need to keep track of all of the different states and interactions to make sure that the user doesn’t get lost while using the component, regardless of whether they’re using a keyboard or a mouse.</p><p>I also think that one of the big things to remember is that a combobox might look like a single component, but there are several moving parts underneath. You have the input, the popup, the options, the filtering, the focus management, and the interactions.</p><p>Think of it like a jigsaw.</p><p>Each piece might be straightforward, but the accessibility problems appear when you try to make all of those pieces work together.</p><p>And before you build one, ask yourself whether you actually need one. If a native control already gives you the behaviour you need, use it.</p><h3>References</h3><p><a href="https://www.w3.org/WAI/ARIA/apg/patterns/combobox/">WAI-ARIA Authoring Practices Guide — Combobox Pattern</a></p><p><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/combobox_role">MDN — ARIA: combobox role</a></p><p><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/listbox_role">MDN — ARIA: listbox role</a></p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ed5fc6ab3cb" width="1" height="1" alt=""><hr><p><a href="https://www.designsystemscollective.com/accessibility-by-default-comboboxes-9ed5fc6ab3cb">Accessibility by Default: Comboboxes</a> was originally published in <a href="https://www.designsystemscollective.com">Design Systems Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Tabs, Accordions and Disclosures]]></title>
            <link>https://medium.com/@joshhallan/accessibility-by-default-tabs-accordions-and-disclosures-08c7ee1103f8?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/08c7ee1103f8</guid>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[frontend-development]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 06 Aug 2026 06:01:01 GMT</pubDate>
            <atom:updated>2026-08-07T09:46:30.149Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oyErqaqDMHe_FhOCWiDgxg.png" /></figure><p>Hi and welcome back to Accessibility by Default. This week we’re looking at three components that are usually grouped together because they all show and hide content: tabs, accordions and disclosures.</p><p>Although they all reveal and hide content, these components aren’t interchangeable. Each has a specific purpose, and choosing the right one changes how users navigate your content and what they expect when interacting with it.</p><h3><strong>What are tabs, accordions and disclosures?</strong></h3><h4><strong>Tabs</strong></h4><p>Tabs are used to switch between related sections of content so only one panel is visible at a time. Usually you’ll have a navigation panel at the top of the section and depending on which you choose will show the relevant information. These could be used if you visit an online store you may have a tab for description, a tab for reviews and a tab for related products.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G8Q7RYEYoijZFJgViVf3og.png" /></figure><h4>Accordion</h4><p>Accordions are comprised of a vertically stacked list of sections that can be expanded and collapsed. You have a header section which is a button to trigger the expand or collapse of the hidden information. Depending on the implementation it could be configured so that when one section expands it’ll collapse all of the others or it could be configured so that all sections can be open at the same time. These are great if you have a long list of FAQs</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/736/1*BsH8IauT8cB2AoTXLyesvQ.png" /></figure><h4>Disclosures</h4><p>In my opinion these are the simplest of the three. You have one control, one section of hidden content, and you toggle it open or closed. This to me is like having a single section of an accordion as opposed to multiple sections. These can be used if you only have one piece of hidden information such as “Show advanced options”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H6pSp4F1nng38n4eNcRapA.png" /></figure><h4>When should you use each?</h4><p>So now that we know what each component is let’s look at the different use cases for them because as I said before these aren’t interchangeable.</p><p>Tabs</p><ul><li>best for small number of equal sections</li><li>avoid when there are large amounts of content</li></ul><p>Accordions</p><ul><li>best for independent expandable sections</li><li>avoid when users want to compare information side by side</li></ul><p>Disclosure</p><ul><li>best for optional supporting information</li><li>avoid using this for primary page content</li></ul><p>Tabs work well when users are switching between related information or want to compare the different information. Accordions should be used when users only care about one section that is independent from the rest (but the information can still be, somewhat, related). Disclosure is ideal when its not information that users need to see all of the time.</p><p>One of the biggest accessibility mistakes isn’t forgetting an aria attribute <em>(but please don’t do that)</em> it’s choosing the wrong component.</p><p>A good rule of thumb is to ask yourself a simple question:</p><p>Can users benefit from seeing more than one section at once?</p><ul><li>If yes, consider an accordion.</li><li>If no, tabs may be the better choice.</li><li>If it’s only a single optional piece of information, use disclosure.</li></ul><h3>Accessibility considerations</h3><h4>Tabs</h4><p>So when we’re looking at the accessibility side of tabs we need look at the roles of each different part. The element that is the tab component container should have role=”tablist”, each element that is a tab should have role=”tab” and the section that contains the hidden content should have role=”tabpanel”. Now that everything has a role let’s look at the aria side. Each element that has a role=”tab” should also have an aria-controls which will refer to the tabpanel component, the active tab should have aria-selected=”true” and the non-selected should aria-selected=”false”. Each element with role=”tabpanel” should have aria-labelledby which refers to the associated tab. And finally, depending on the orientation of the tablist, if it is vertical it should have aria-orientation=”vertical” (the default is horizontal so no need if that is your orientation).</p><p>Now, those are just the roles and arias needed but there are other accessibility considerations such as the left and right arrows should move to either the previous tab or the next tab. Space or enter should activate the tab that you’re currently on. Those are the mandatory interactions but there are also some optional ones; home and end should move to either the first or last tab, delete should close the current tab and set focus to the following tab</p><pre>&lt;div role=&quot;tablist&quot; aria-label=&quot;Product information&quot;&gt;<br>  &lt;button<br>    id=&quot;tab-description&quot;<br>    role=&quot;tab&quot;<br>    aria-selected=&quot;true&quot;<br>    aria-controls=&quot;panel-description&quot;&gt;<br>    Description<br>  &lt;/button&gt;<br><br>  &lt;button<br>    id=&quot;tab-reviews&quot;<br>    role=&quot;tab&quot;<br>    aria-selected=&quot;false&quot;<br>    aria-controls=&quot;panel-reviews&quot;&gt;<br>    Reviews<br>  &lt;/button&gt;<br><br>  &lt;button<br>    id=&quot;tab-related&quot;<br>    role=&quot;tab&quot;<br>    aria-selected=&quot;false&quot;<br>    aria-controls=&quot;panel-related&quot;&gt;<br>    Related Products<br>  &lt;/button&gt;<br>&lt;/div&gt;<br><br>&lt;div<br>  id=&quot;panel-description&quot;<br>  role=&quot;tabpanel&quot;<br>  aria-labelledby=&quot;tab-description&quot;&gt;<br>  &lt;p&gt;This is the description panel.&lt;/p&gt;<br>&lt;/div&gt;<br><br>&lt;div<br>  id=&quot;panel-reviews&quot;<br>  role=&quot;tabpanel&quot;<br>  aria-labelledby=&quot;tab-reviews&quot;<br>  hidden&gt;<br>  &lt;p&gt;This is the reviews panel.&lt;/p&gt;<br>&lt;/div&gt;<br><br>&lt;div<br>  id=&quot;panel-related&quot;<br>  role=&quot;tabpanel&quot;<br>  aria-labelledby=&quot;tab-related&quot;<br>  hidden&gt;<br>  &lt;p&gt;This is the related products panel.&lt;/p&gt;<br>&lt;/div&gt;</pre><h4>Accordion</h4><p>After that exhaustive list on tabs, thankfully the accordion pattern isn’t as big. For roles and arias you need the title of each header should be contained in an element with role=”button”, if the associated panel is visible the header button should have aria-expanded=”true” otherwise it should be set to “false”, the button element should have aria-controls set to the ID of the element containing the accordion panel content.</p><p>And in terms of keyboard interaction, space / enter should active the accordion section to either show or hide the associated content, tab should move to the next focusable element and shift + tab should take you to the previous focusable element.</p><pre>&lt;h2&gt;<br>  &lt;button<br>    aria-expanded=&quot;false&quot;<br>    aria-controls=&quot;faq-1&quot;<br>    id=&quot;faq-button-1&quot;&gt;<br>    What is your returns policy?<br>  &lt;/button&gt;<br>&lt;/h2&gt;<br><br>&lt;div<br>  id=&quot;faq-1&quot;<br>  role=&quot;region&quot;<br>  aria-labelledby=&quot;faq-button-1&quot;<br>  hidden&gt;<br>  &lt;p&gt;<br>    You can return any item within 30 days of purchase.<br>  &lt;/p&gt;<br>&lt;/div&gt;</pre><h4>Disclosure</h4><p>Unlike tabs, disclosure already has a native HTML implementation using &lt;details&gt; and &lt;summary&gt;. This gives us much of the accessibility behaviour for free, making it my preferred approach whenever it meets the design requirements. For roles and states, the element that shows and hides should have a role=”button” and when the content is visible it should have aria-expanded=”true” otherwise it should be aria-expanded=”false” and optionally the element with role button has a value specified for aria-controls that refers to the element that contains all the content that is shown or hidden. For keyboard interaction it should expand / collapse using either space or enter.</p><p>If you are using native &lt;details&gt; and &lt;summary&gt;, you do not need to manually add role=&quot;button&quot; or aria-expanded. The browser&#39;s user agent stylesheet and accessibility tree handle the toggle semantics and expansion state natively out of the box.</p><p>If you’re building a custom disclosure (e.g., using a &lt;div&gt; or a &lt;button&gt; controlling an external panel), then you need aria-expanded.</p><pre>&lt;details&gt;<br>  &lt;summary&gt;Show advanced options&lt;/summary&gt;<br><br>  &lt;p&gt;<br>    These settings are intended for advanced users and should only<br>    be changed if you understand their impact.<br>  &lt;/p&gt;<br>&lt;/details&gt;</pre><p>The great thing about using &lt;details&gt; and &lt;summary&gt; is the built-in accessibility that comes with it, because its using the native HTML.</p><h3>Common mistakes</h3><p>These are the most common mistakes that i see people make when implementing these components</p><ul><li>Using tabs for unrelated content</li><li>Making the accordion headers not focusable (remember role=”button”)</li><li>Hiding the content visually but keyboard users are still able to access it</li><li>Using accordions for everything</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/672/0*ZuzAy0XvF8mR6ynA.jpg" /></figure><h3>Key takeaways</h3><p>These are my key takeaways for these components. Remember to use the component based on your need not just for styling options. Use native HTML where possible as you get the accessibility for free. Don’t forget about keyboard support and interaction and communicate aria when required</p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=08c7ee1103f8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Dialogs]]></title>
            <link>https://medium.com/@joshhallan/accessibility-by-default-dialogs-be60a3708d29?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/be60a3708d29</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[frontend-development]]></category>
            <category><![CDATA[a11y]]></category>
            <category><![CDATA[accessibility]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 30 Jul 2026 06:01:01 GMT</pubDate>
            <atom:updated>2026-07-30T06:01:01.827Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tKq1FLnpwanise7IuVdS_w.png" /></figure><p>Hi, and welcome to Accessibility by Default. If you’re new, this series is all about building web components with accessibility in mind from the very beginning — understanding what native HTML gives us for free, and where we need to add a little extra ourselves. If you’re a returning reader, welcome back!</p><p>This week we’re looking at dialogs, and to be honest this is probably the topic that I’ve debated more than any throughout my career. I’ve had many, many conversations with many, many people who say, “Dialogs aren’t accessible,” but I personally don’t think that’s true.</p><p><strong>Poorly implemented dialogs aren’t accessible. Well-implemented dialogs can be.</strong></p><p>I often see teams remove dialogs altogether in favour of navigating users to an entirely new page because it feels like the “accessible” option. In reality, that can introduce unnecessary friction by taking users away from the task they were in the middle of just to display a small amount of related information, before expecting them to navigate all the way back again.</p><p>I’ve also seen dialogs replaced with accordions. Sometimes that’s the right decision, but sometimes it simply means exposing far more content on the page than users actually need which can, in turn, make the page looked cluttered. A dialog can allow that additional information to remain hidden until it’s requested.</p><p>Like every component we’ve looked at in this series, dialogs aren’t inherently accessible or inaccessible. The difference comes down to how they’re built.</p><h3><strong>What is a dialog?</strong></h3><p>Sorry, I may have jumped the gun slightly there by going straight into dialog accessibility before actually explaining what a dialog is.</p><p>A dialog is a component that displays additional information or requests an action from the user without navigating them away from the current page. Dialogs are usually triggered by an action on the page, such as clicking a button or interacting with a control.</p><p>Once triggered, the dialog appears above the existing page content and temporarily becomes the focus of the user’s attention.</p><p>There are two common dialog patterns we need to understand:</p><ul><li><strong>Dialog</strong> — used for displaying additional information or collecting input from a user.</li><li><strong>Alert dialog</strong> — used for important messages that require the user’s attention before continuing.</li></ul><p>Dialogs can also have different behaviours:</p><ul><li><strong>Modal dialogs</strong> prevent interaction with the rest of the page until the dialog is closed.</li><li><strong>Non-modal dialogs</strong> allow users to continue interacting with the rest of the application.</li></ul><p>A dialog might be used for many different reasons. This could be editing information, displaying additional details, or allowing a user to complete a task without leaving the current page.</p><p>A common example is an enterprise application such as a CMS system. A database record might contain many fields:</p><ul><li>ID</li><li>First name</li><li>Last name</li><li>Phone number</li><li>Address</li><li>Postcode</li><li>Country</li><li>Additional metadata</li></ul><p>However, the purpose of a user interface isn’t always to expose every available piece of data.</p><p>Displaying every field in a table would quickly become overwhelming and make scanning difficult. Instead, the table might display the information users need most frequently, such as the ID, name and phone number, with an action allowing the user to open a dialog containing the additional information when required.</p><p>This allows users to quickly scan the information they need while still providing access to the full record when requested.</p><pre>&lt;button&gt;<br>    View information for Josh Allan<br>&lt;/button&gt;</pre><p>Accessibility isn’t about showing everything to everyone at all times. It’s about making sure users can access the information they need in a way that matches their task.</p><h4><strong>Modal dialogs</strong></h4><p>A modal dialog is a dialog that temporarily prevents interaction with the rest of the page until the user has completed an action or closed the dialog.</p><p>When a modal opens, the user’s focus is moved away from the page and into the dialog. While the dialog is open, users should not be able to interact with the content behind it.</p><p>Common examples include:</p><ul><li>confirmation messages</li><li>login forms</li><li>editing details</li><li>checkout steps</li><li>destructive actions</li></ul><p>For example:</p><p>“Are you sure you want to delete this customer?”</p><p>This isn’t something where we want the user accidentally continuing to interact with the page behind it. The modal creates a temporary interruption where the user must make a decision before continuing.</p><h4><strong>Alert dialogs</strong></h4><p>So if a modal dialog requires a user’s attention, what makes it different from an alert dialog?</p><p>In reality, they can overlap.</p><p>A modal describes how a dialog behaves. An alert dialog describes the importance and purpose of the information being presented. An alert dialog intentionally interrupts the user’s workflow because the information requires immediate attention.</p><p>Common examples include:</p><ul><li>a critical error that prevents the user from continuing</li><li>a warning about an action’s consequences</li><li>a security-related message</li><li>a payment failure</li></ul><p>For example:</p><blockquote>“Your session has expired. Please sign in again to continue.”</blockquote><p>or:</p><blockquote>“Your payment could not be processed. Please update your payment method before placing your order.”</blockquote><p>These are different from a standard dialog where the user has intentionally requested more information.</p><p>For example:</p><p><strong>Dialog</strong></p><ul><li><strong>Use case</strong>: Viewing customer details</li><li><strong>Purpose</strong>: Provide additional information.</li><li><strong>Use case</strong>: Editing profile details</li><li><strong>Purpose</strong>: Allow user input.</li></ul><p><strong>Alert dialog</strong></p><ul><li><strong>Use case: </strong>Payment failed</li><li><strong>Purpose:</strong> Require immediate attention.</li><li><strong>Use case: </strong>Session expired</li><li><strong>Purpose:</strong> Inform the user they must act before continuing.</li></ul><p>A common mistake is using alert dialogs for every message because they feel like the more accessible option.</p><p>However, alert dialogs are designed to interrupt the user. Overusing them can create a frustrating experience.</p><h4>Native HTML dialogs</h4><p>As with every component we’ve looked at throughout this series, the first question should always be:</p><p>“Does the platform already provide this behaviour?”</p><p>Modern browsers provide the native &lt;dialog&gt; element.</p><pre>&lt;dialog&gt;<br>  &lt;h2&gt;<br>  Customer details<br>  &lt;/h2&gt;<br>  &lt;p&gt;<br>    Additional customer information.<br>  &lt;/p&gt;<br>&lt;/dialog&gt;</pre><p>The native element provides many behaviours developers often recreate manually, including modal behaviour through:</p><p>dialog.showModal();</p><p>and closing through:</p><p>dialog.close();</p><p>That doesn’t mean accessibility is automatically solved. You still need to consider accessible names, focus management and the content inside the dialog.</p><p>However, starting with native HTML gives you a much stronger foundation.</p><p>The good thing about using</p><p>All of these are things to think about when implementing a custom dialog solution, but remember that native HTML gives you much of this for free out of the box.</p><h3><strong>Main accessibility points</strong></h3><p>As I mentioned earlier, many conversations I’ve had about dialogs are around whether they are accessible. The important thing is understanding the behaviours that need to exist.</p><p>The key areas are:</p><ul><li>Opening a dialog</li><li>Focus trapping</li><li>Closing a dialog</li><li>Returning focus</li><li>Background inertness</li><li>Announcing dialogs</li></ul><p>I’m aware that that <em>does </em>sound like a lot of different things to keep track of but once you read through, they do sound pretty obvious.</p><h4><strong>Opening a dialog</strong></h4><p>When a dialog is triggered by an action on the page, focus should move inside the dialog.</p><p>The exact location depends on the purpose of the dialog.</p><p>For example:</p><ul><li>A form dialog may move focus to the first input.</li><li>An information dialog may move focus to the heading.</li><li>A destructive confirmation may move focus to the least destructive action.</li></ul><p>There is no single correct answer for every dialog. The important part is that focus moves into the dialog and the user immediately understands what has opened.</p><h4><strong>Focus trapping</strong></h4><p>Once focus has moved into a modal dialog, users should not be able to accidentally navigate behind it.</p><p>This is known as focus trapping.</p><p>Keyboard users and assistive technology users should be able to move between the controls inside the dialog, but pressing Tab at the final focusable element should return them to the first focusable element.</p><p>I’ve covered a simplified focus trap implementation in my <a href="https://medium.com/@joshhallan/accessibility-by-default-foundational-principles-for-building-accessible-interfaces-ab9cf3ee2ceb">first accessibility article</a>.</p><pre>// Note: This is a simplified conceptual model to demonstrate the mechanics. <br>// In production architecture, use a battle-tested primitive (like Radix or <br>// React Aria) <br>// to handle edge cases like shadow DOM, hidden elements, and Safari bugs.<br><br>/**<br> * Returns a list of elements within the specified element that can be focused<br> * @param {HTMLElement} containerElement Parent element to search inside<br> * @return {NodeList} The focusable elements within containerElement<br> */<br>const getFocusableElements = (containerElement) =&gt; {<br>  return containerElement.querySelectorAll(<br>    `a[href], area[href], input:not([disabled]),<br>    select:not([disabled]), textarea:not([disabled]),<br>    button:not([disabled]), [tabindex=&quot;0&quot;]`,<br>  );<br>};<br><br>// Focus the first focusable element if it exists<br>if (focusableElements.length &gt; 0) {<br>  focusableElements[0].focus();<br>}<br><br>/**<br> * Handles tab key event with given focusable elements<br> * to keep focus within the modal.<br> */<br>const handleTabKey = (event, activeElement, focusableElements) =&gt; {<br>  if (event.key !== &#39;Tab&#39;) return;<br>  if (focusableElements.length &lt; 2) {<br>    event.preventDefault();<br>    return;<br>  }<br>  if (event.shiftKey &amp;&amp; activeElement === focusableElements[0]) {<br>    event.preventDefault();<br>    focusableElements[focusableElements.length - 1].focus();<br>  } else if (<br>    !event.shiftKey &amp;&amp;<br>    activeElement === focusableElements[focusableElements.length - 1]<br>  ) {<br>    event.preventDefault();<br>    focusableElements[0].focus();<br>  }<br>};</pre><p>The important concept is:</p><ul><li>Focus enters the dialog.</li><li>Focus stays within the dialog.</li><li>Focus returns when the dialog closes.</li></ul><p><em>If you’re using the native </em>&lt;dialog&gt;<em> element with </em>showModal()<em>, this is handled for you automatically.</em></p><h4><strong>Closing a dialog</strong></h4><p>Dialogs should provide clear ways for users to close them.</p><p>Common methods include:</p><ul><li>A close button.</li><li>Pressing Escape.</li><li>An optional backdrop click.</li></ul><p>Backdrop clicking should be used carefully. Some dialogs contain important information or forms where accidental dismissal could cause frustration.</p><p>When a dialog closes, focus should return to the element that opened it.</p><p>If a user clicked a button to open a dialog, that button should regain focus once the dialog closes.</p><p>This allows users to continue exactly where they left off.</p><p>I still remember the first custom modal I built. During testing, our tester repeatedly pressed Enter to open the modal, checked that focus moved to the first focusable element (the close button in this case), activated it, then verified that focus returned to the original trigger. It felt repetitive at the time, but it’s exactly the kind of behaviour that’s easy to miss when you’re only testing with a mouse.</p><h4><strong>Background inertness (inert vs aria-hidden)</strong></h4><p>One of the most common mistakes with modal dialogs is only thinking about what users can see.</p><p>A modal might visually appear above the page, but if the content behind it is still available to keyboard users, then the experience is broken.</p><p>For example:</p><pre>&lt;div class=&quot;modal&quot;&gt;<br>  &lt;h2&gt;<br>    Delete customer?<br>  &lt;/h2&gt;<br><br>  &lt;button&gt;<br>    Cancel<br>  &lt;/button&gt;<br><br>  &lt;button&gt;<br>    Delete<br>  &lt;/button&gt;<br>&lt;/div&gt;</pre><p>Visually, this may look like a modal.</p><p>However, a keyboard user can still potentially press Tab and move through the page behind the dialog.</p><p>This creates confusion because the user has entered a modal state visually, but the browser still allows interaction with the content underneath.</p><p>This is where the inert attribute becomes useful.</p><pre>&lt;main inert&gt;<br>  Page content<br>&lt;/main&gt;</pre><p>The inert attribute tells the browser that the content should not:</p><ul><li>receive focus</li><li>respond to user interaction</li><li>be exposed to assistive technology</li></ul><p>A common older approach was using:</p><p>aria-hidden=”true”</p><p>However, these solve different problems.</p><p>aria-hidden only removes content from the accessibility tree. It does <strong>not</strong> prevent keyboard users from reaching interactive elements.</p><p>For example:</p><pre>&lt;button aria-hidden=&quot;true&quot;&gt;<br>  Submit<br>&lt;/button&gt;</pre><p>The button may still be focusable even though assistive technology cannot announce it.</p><p>For modal dialogs, the goal is not just hiding content from screen readers. The goal is preventing interaction with the rest of the page.</p><p>This is why inert is the better solution.</p><p><em>Native</em> &lt;dialog&gt; <em>opened via</em> showModal() <em>also makes the rest of the page inert automatically — you don&#39;t need to add this yourself.</em></p><h3><strong>Announcing dialogs</strong></h3><p>Moving focus into a dialog is only part of the experience. Users also need to understand what has opened and why.</p><p>A dialog needs an accessible name.</p><p>The preferred approach is usually using aria-labelledby connected to a visible heading.</p><p>Example:</p><pre>&lt;dialog aria-labelledby=&quot;customer-details-title&quot;&gt;<br>  &lt;h2 id=&quot;customer-details-title&quot;&gt;<br>    Customer details<br>  &lt;/h2&gt;<br><br>  &lt;p&gt;<br>    Additional information about this customer.<br>  &lt;/p&gt;<br>&lt;/dialog&gt;</pre><p>A screen reader can now announce something like:</p><blockquote>“Customer details, dialog”</blockquote><p>rather than simply:</p><blockquote>“Dialog”</blockquote><p>which provides no useful context.</p><p>If there isn’t a visible heading, you can provide a name using:</p><p>aria-label</p><p>Example:</p><pre>&lt;dialog aria-label=&quot;Delete customer&quot;&gt;</pre><p>However, visible text is generally preferred because it keeps the visual and accessible experiences aligned.</p><p>You can also provide additional context using:</p><p>aria-describedby</p><p>Example:</p><pre>&lt;dialog aria-labelledby=&quot;delete-title&quot; aria-describedby=&quot;delete-description&quot;&gt;</pre><p>With:</p><pre>&lt;h2 id=&quot;delete-title&quot;&gt;<br>  Delete customer<br>&lt;/h2&gt;<br><br>&lt;p id=&quot;delete-description&quot;&gt;<br>  This action cannot be undone.<br>&lt;/p&gt;</pre><p>A screen reader user receives:</p><ul><li>the name of the dialog</li><li>the purpose of the dialog</li><li>any additional instructions</li></ul><p>The goal is the same as with every accessible component:</p><p>The user should understand what they are interacting with, what state they are in, and what action they need to take.</p><h3><strong>Key takeaways</strong></h3><p>Accessible dialogs come down to a few core principles:</p><ul><li>Use the correct dialog pattern for the situation.</li><li>Move focus into the dialog when it opens.</li><li>Keep focus within modal dialogs.</li><li>Return focus when the dialog closes.</li><li>Prevent background interaction.</li><li>Provide a clear accessible name.</li><li>Prefer native HTML where possible.</li></ul><p>Like every component we’ve looked at in this series, accessibility is rarely about adding more complexity. Often, it is about understanding the behaviour users need and using the tools the platform already provides.</p><p>If there’s one thing I hope you take away from this article, it’s that dialogs aren’t inaccessible — they’re just one of the easiest components to implement incorrectly. Understanding focus management, user context and native browser behaviour will get you much further than trying to avoid dialogs altogether.</p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=be60a3708d29" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Form Controls]]></title>
            <link>https://medium.com/@joshhallan/accessibility-by-default-form-controls-87210f277441?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/87210f277441</guid>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[web-accessibility]]></category>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[web-development]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 23 Jul 2026 10:11:15 GMT</pubDate>
            <atom:updated>2026-07-23T14:23:16.123Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DFVgcIDB-llEgLoeyXj0wQ.png" /></figure><p>Hi! If this is your first time here welcome! This is a series in which we dive into the accessibility side of frontend development from an engineers perspective. We’re working through common UI components to understand the building blocks that make them accessible from the start. And, if you’re a returning reader, welcome back! This week we’re going to be looking into form controls.</p><p>Forms are one of the most common places that users can interact with a website, from contact details to orders but they can also be one of the easiest places that can accidentally introduce accessibility barriers.</p><p>A form that looks visually correct can still be difficult or impossible to use for different types of assistive technology such as screen readers, keyboard navigation, voice control software or cognitive accessibility tools.</p><p>The foundation of accessible forms is simple:</p><p><strong>Every control needs a clear purpose, a clear name, and a clear state.</strong></p><p>In this article we’ll cover the building blocks of accessible form controls:</p><ul><li>text inputs</li><li>labels</li><li>checkboxes</li><li>radio groups</li><li>switches</li><li>helper text</li><li>required fields</li><li>grouping related controls with fieldset and legend</li></ul><h3><strong>Text Inputs</strong></h3><p>One major flag that causes an accessibility issue is a text input without a label.</p><pre>&lt;input type=&quot;text&quot; /&gt;</pre><p><strong>Always provide an accessible name</strong></p><p>The most common solution is a visible &lt;label&gt;.</p><pre>&lt;label for=&quot;email&quot;&gt;<br>  Email address<br>&lt;/label&gt;<br>&lt;input id=&quot;email&quot; type=&quot;email&quot; /&gt;</pre><p>The relationship is created through the matching attributes:</p><ul><li>label for</li><li>matching input id</li></ul><p>Now assistive technology can announce:</p><p>“Email address”</p><h4><strong>Avoid placeholder-only labels</strong></h4><p>A common pattern is:</p><pre>&lt;input placeholder=&quot;Email address&quot; /&gt;</pre><p>This is not a replacement for a label.</p><p>Problems:</p><ul><li>disappears when the user starts typing</li><li>lower contrast</li><li>users with cognitive difficulties may forget the purpose</li><li>screen reader support varies</li></ul><p>Use placeholders for examples:</p><pre>&lt;label for=&quot;postcode&quot;&gt;<br> Postcode<br>&lt;/label&gt;<br>&lt;input id=&quot;postcode&quot; placeholder=&quot;Example: SR1 1AA&quot; /&gt;</pre><p>Another pattern that is accessible for inputs and other form controls is to nest the input within the label as this gives implicit association between the elements.</p><pre>&lt;label class=&quot;field-container&quot;&gt;<br>  &lt;span class=&quot;label-text&quot;&gt;<br>    Email Address<br>  &lt;/span&gt;<br>  &lt;span class=&quot;hint&quot;&gt;<br>    We will never share your email.<br>  &lt;/span&gt; &lt;input type=&quot;email&quot; name=&quot;email&quot;&gt;<br>&lt;/label&gt;<br><br>&lt;label&gt; Username &lt;input type=&quot;text&quot; name=&quot;username&quot;&gt; &lt;/label&gt;</pre><p>While nesting is perfectly valid HTML, I generally prefer explicit for/id associations. As labels become more complex—with helper text, icons, or additional markup—the relationship is easier to understand, maintain and debug when it is explicit.</p><h3><strong>Checkboxes</strong></h3><p>Checkboxes represent independent choices.</p><pre>&lt;label&gt;<br>  &lt;input type=&quot;checkbox&quot; /&gt;<br>  Subscribe to newsletter<br>&lt;/label&gt;</pre><p>The label wraps the input, creating the relationship automatically.</p><p>Alternatively:</p><pre>&lt;input id=&quot;terms&quot; type=&quot;checkbox&quot; /&gt;<br>&lt;label for=&quot;terms&quot;&gt;<br>  I agree to the terms and conditions<br>&lt;/label&gt;</pre><h3><strong>Checkbox groups</strong></h3><p>When multiple checkboxes belong together, provide a group name.</p><pre>&lt;fieldset&gt;<br>  &lt;legend&gt;<br>    Preferred contact methods<br>  &lt;/legend&gt;<br><br>  &lt;label&gt;<br>    &lt;input type=&quot;checkbox&quot; /&gt;<br>      Email<br>    &lt;/label&gt;<br><br>  &lt;label&gt;<br>    &lt;input type=&quot;checkbox&quot; /&gt;<br>      Phone<br>  &lt;/label&gt;<br>&lt;/fieldset&gt;</pre><p>With this pattern the inputs are grouped together using &lt;fieldset&gt; the &lt;legend&gt; acts as a label for the group. The user will now hear “Preferred contact methods, group” before reaching the checkboxes.</p><h3><strong>Radio Groups</strong></h3><p>Radio buttons represent a single choice from multiple options.</p><pre>&lt;fieldset&gt;<br>  &lt;legend&gt;<br>    Account type<br>  &lt;/legend&gt;<br><br>  &lt;label&gt;<br>    &lt;input type=&quot;radio&quot; name=&quot;account&quot; value=&quot;personal&quot; /&gt;<br>    Personal<br>   &lt;/label&gt;<br><br>  &lt;label&gt;<br>    &lt;input type=&quot;radio&quot; name=&quot;account&quot; value=&quot;business&quot; /&gt;<br>    Business<br>   &lt;/label&gt;<br>&lt;/fieldset&gt;</pre><p>The important parts of this pattern are</p><ul><li>Same name: each of the inputs use the same name so this tells the browser that these controls belong together</li><li>Fieldset and legend: we’re using the fieldset and legend here again to group the inputs and give the group an accessible label</li></ul><h3>Switches</h3><p>Switches are intended for toggling a setting or preference. Common examples include:</p><ul><li>Enabling notifications</li><li>Switching between light and dark mode</li><li>Turning automatic updates on or off</li></ul><p>A switch represents a binary or boolean state — <strong>on/off</strong> or <strong>true/false</strong>.</p><pre>&lt;button<br>  id=&quot;notifications-switch&quot;<br>  role=&quot;switch&quot;<br>  aria-checked=&quot;true&quot;<br>&gt;<br>  Notifications<br>&lt;/button&gt;</pre><pre>const switchElement = document.getElementById(&quot;notifications-switch&quot;);<br><br>switchElement.addEventListener(&quot;click&quot;, () =&gt; {<br>  const isChecked =<br>    switchElement.getAttribute(&quot;aria-checked&quot;) === &quot;true&quot;;<br>  switchElement.setAttribute(<br>    &quot;aria-checked&quot;,<br>    String(!isChecked)<br>  );<br>});</pre><p>The important parts are:</p><ul><li><strong>Accessible name</strong> → Notifications</li><li><strong>Role</strong> → switch</li><li><strong>State</strong> → aria-checked=&quot;true&quot; or aria-checked=&quot;false&quot;</li></ul><p>A screen reader should announce:</p><p>“Notifications, switch, on”</p><p>or</p><p>“Notifications, switch, off”</p><h4>Don’t use switches for actions</h4><p>A switch changes the state of something.</p><p>A button performs an action.</p><ul><li>“Save changes” as a switch is incorrect</li><li>“Enable notifications” as a switch is correct</li></ul><h3><strong>Helper Text</strong></h3><p>Helper text provides additional information:</p><p>Examples:</p><ul><li>formatting requirements</li><li>password rules</li><li>explanations</li></ul><pre>&lt;label for=&quot;password&quot;&gt;<br>  Password<br>&lt;/label&gt;<br>&lt;input id=&quot;password&quot; aria-describedby=&quot;password-help&quot; /&gt;<br>&lt;p id=&quot;password-help&quot;&gt;<br>  Must contain at least 8 characters.<br>&lt;/p&gt;</pre><p>aria-describedby connects the additional information.</p><p>A screen reader announces:</p><p>“Password, edit text, Must contain at least 8 characters”</p><h3><strong>Required Fields</strong></h3><p>Required fields should communicate both visually and programmatically.</p><p>Visual:</p><pre>&lt;label&gt;<br>  Email address *<br>&lt;/label&gt;</pre><p>is not enough.</p><p>The asterisk has no meaning to assistive technology.</p><p>Use:</p><pre>&lt;input required /&gt;</pre><p>and make sure the label explains that is is required:</p><pre>&lt;label&gt;<br>  Email address &lt;span aria-hidden=&quot;true&quot;&gt;*&lt;/span&gt;<br>&lt;/label&gt;</pre><h3><strong>Validation Messages</strong></h3><p>Errors should be associated with the input.</p><pre>&lt;input aria-invalid=&quot;true&quot; aria-describedby=&quot;email-error&quot; /&gt;<br>&lt;p id=&quot;email-error&quot;&gt;<br>  Enter a valid email address.<br>&lt;/p&gt;</pre><p>The user receives:</p><ul><li>what is wrong</li><li>where it is wrong</li><li>how to fix it</li></ul><p>Validation should not rely on colour alone and error messages should appear at the point the error occurs, not just at the top of the page.</p><h3><strong>Grouping Controls with Fieldset and Legend</strong></h3><p>I know we’ve mentioned using fieldset and legend in the previous examples for checkboxes and for radios but fieldset and legend are often overlooked.</p><p>They are not just visual containers, they provide semantic relationships between the elements.</p><pre>&lt;fieldset&gt;<br>  &lt;legend&gt;<br>    Delivery address<br>  &lt;/legend&gt;<br><br>  &lt;label&gt;<br>    Street address<br>  &lt;/label&gt;<br>  &lt;input /&gt;<br><br>  &lt;label&gt;<br>    City<br>   &lt;/label&gt;<br>  &lt;input /&gt;<br>&lt;/fieldset&gt;</pre><p>Screen readers announce the group name before users navigate through the controls, helping provide context for the fields that follow.</p><h3><strong>Key take aways</strong></h3><p>Accessible forms have a few core principles:</p><ul><li>Every control has an accessible name</li><li>Labels are always associated with inputs using either explicit (for/id) or implicit (nested) labelling</li><li>Groups should have meaningful names and an accompanying legend</li><li>States are communicated programmatically</li><li>Helper text is connecting using relationships</li><li>Errors should be associated with the input and should explain what went wrong and how to fix it</li></ul><p>One thing you’ll probably have noticed throughout this article — and hopefully throughout this series — is just how much accessibility comes for free when we lean on native HTML. Before reaching for ARIA or custom components, ask yourself whether a native element already provides the behaviour you need. More often than not, the browser has already solved the accessibility problem for you.</p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=87210f277441" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Links]]></title>
            <link>https://medium.com/@joshhallan/accessibility-by-default-links-e4df59815743?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/e4df59815743</guid>
            <category><![CDATA[web-accessibility]]></category>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[web-development]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 16 Jul 2026 10:48:33 GMT</pubDate>
            <atom:updated>2026-07-23T14:23:30.512Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oFC-zVPf4EYyj56XMe4vWQ.png" /></figure><p>Welcome back to Accessibility by Default. If this is the first post you’re reading, welcome in and be sure to check out the previous posts in the series. This series explores how we can build components with an accessibility-first approach. Accessibility shouldn’t be an afterthought; it should be considered from the very beginning of development.</p><p>Last week we looked at buttons, and one of the key principles was simple: use native HTML first. This week we’re continuing that idea with another fundamental component — links.</p><p>I know what you’re thinking: “Links are easy, it’s just an &lt;a&gt; tag.” But, like most things in frontend development, there’s more complexity hiding beneath the surface.</p><p>In this article, we’ll look at native links, links vs buttons, accessible link names, icon and image links, and some of the common mistakes that can make a seemingly simple component less accessible.</p><h3><strong>Native links vs everything else</strong></h3><p>As with every article in this series, if you can use a native HTML element, you probably should. Native elements already provide a huge amount of functionality and accessibility behaviour without requiring additional code.</p><p>When using a native &lt;a&gt; element, you get a lot of behaviour built into the browser:</p><h4><strong>Keyboard interaction</strong></h4><p>Native links are automatically keyboard accessible and follow the natural document order. Users can navigate between links using their keyboard without developers needing to manually recreate interactions or add custom event handlers.</p><h4><strong>Focus behaviour</strong></h4><p>Browsers already understand how links should receive focus and how users should interact with them. By replacing a link with a custom element, developers take responsibility for recreating that behaviour themselves.</p><h4><strong>Browser behaviour</strong></h4><p>Native links work with the features users already expect from the web. Users can open links in new tabs, copy URLs, use browser history, and understand where a link will take them before interacting with it.</p><h4><strong>Context for assistive technologies</strong></h4><p>Assistive technologies understand the difference between links and other interactive elements. A screen reader user can navigate between links, understand their purpose, and move through a page more efficiently.</p><h4><strong>Expected interaction patterns</strong></h4><p>Users have built mental models around how interfaces work. A link should take someone somewhere, while a button should perform an action. Keeping these behaviours consistent creates a predictable experience for everyone.</p><p>The more we replace native HTML with custom implementations, the more responsibility we take on ourselves. The browser has spent decades solving these problems — whenever possible, we should let it.</p><h3><strong>Links vs buttons</strong></h3><p>One of the most common accessibility mistakes in modern applications is confusing links and buttons.</p><p>They can often look identical visually, but they represent completely different interactions.</p><p>The simplest way to think about the difference is:</p><p><strong>Links navigate. Buttons perform actions.</strong></p><p>If something takes a user to another page, route, or location, it should usually be a link:</p><pre>&lt;a href=&quot;/settings&quot;&gt;<br>  Settings<br>&lt;/a&gt;</pre><p>If something performs an action on the current page, it should usually be a button:</p><pre>&lt;button&gt;<br>  Save changes<br>&lt;/button&gt;</pre><p>This distinction matters because assistive technologies communicate these elements differently, and users build expectations based on those announcements.</p><p>A screen reader user navigating through a page expects a link to take them somewhere. They expect a button to perform an action.</p><p>When we use the wrong element, we create confusion even if the component looks visually correct.</p><h3><strong>When buttons become links</strong></h3><p>A common pattern is using a button for navigation:</p><pre>&lt;button onclick=&quot;window.location=&#39;/profile&#39;&quot;&gt;<br>  View profile<br>&lt;/button&gt;</pre><p>Technically, this works. The user can click it and arrive at the correct page.</p><p>However, it is still the wrong semantic choice.</p><p>The browser no longer understands that this is navigation. As a result, users lose expected link behaviour such as:</p><ul><li>opening the link in a new tab</li><li>copying the destination URL</li><li>using browser history naturally</li><li>understanding the destination before activating it</li></ul><p>The better solution is to use the element that already represents the behaviour:</p><pre>&lt;a href=&quot;/profile&quot;&gt;<br>  View profile<br>&lt;/a&gt;</pre><h3><strong>When links become buttons</strong></h3><p>The opposite problem is also common:</p><pre>&lt;a href=&quot;#&quot; onclick=&quot;openModal()&quot;&gt;<br>  Open settings<br>&lt;/a&gt;</pre><p>Again, it might work visually, but this is an action, not navigation.</p><p>The user isn’t going anywhere. They are triggering something on the current page.</p><p>This should be a button:</p><pre>&lt;button&gt;<br>  Open settings<br>&lt;/button&gt;</pre><p>By choosing the correct element, we get the correct semantics, keyboard behaviour, and interaction patterns without needing to recreate them ourselves.</p><h3><strong>Accessible link names</strong></h3><p>Just like buttons, links need a clear accessible name.</p><p>For many links, the visible text already provides this:</p><pre>&lt;a href=&quot;/account&quot;&gt;<br>  Account settings<br>&lt;/a&gt;</pre><p>A screen reader user will understand where the link will take them.</p><p>However, problems start when link text becomes vague:</p><pre>&lt;a href=&quot;/guide&quot;&gt;<br>  Click here<br>&lt;/a&gt;</pre><p>or:</p><pre>&lt;a href=&quot;/article&quot;&gt;<br>  Read more<br>&lt;/a&gt;</pre><p>These might make sense visually when surrounded by content, but they lose meaning when someone navigates through a list of links.</p><p>Imagine hearing:</p><blockquote><em>“Link, click here”</em></blockquote><blockquote><em>“Link, click here”</em></blockquote><blockquote><em>“Link, read more”</em></blockquote><p>There is no context about where those links go.</p><p>A better approach is to make the link text describe the destination:</p><pre>&lt;a href=&quot;/info&quot;&gt;<br>  Read more about our approach<br>&lt;/a&gt;</pre><p>The goal is not to add extra information specifically for screen readers. The goal is to make the link meaningful for everyone.</p><h3><strong>Icon, image, and logo links</strong></h3><p>Modern applications frequently use links without visible text.</p><p>Examples include:</p><ul><li>logo links back to the homepage</li><li>social media icons</li><li>search icons</li><li>account icons</li></ul><p>Similar to icon-only buttons, icon-only links can cause the same issues. Visually, these can be obvious. However, without text, assistive technologies need another way to understand the purpose.</p><p>For example:</p><pre>&lt;a href=&quot;/search&quot;&gt;<br>  &lt;svg&gt;<br>    …<br>  &lt;/svg&gt;<br>&lt;/a&gt;</pre><p>Without an accessible name, a screen reader user may only hear:</p><p><em>“Link”</em></p><p>The purpose of the link has been lost. This is where techniques like alternative text or ARIA labels can help.</p><p>For example:</p><pre>&lt;a href=&quot;/search&quot; aria-label=&quot;Search&quot;&gt;<br>  &lt;svg&gt;<br>    …<br>  &lt;/svg&gt;<br>&lt;/a&gt;</pre><p>The important point is the same as with buttons: ARIA should restore missing meaning, not replace native semantics.</p><h3><strong>Links that aren’t really links</strong></h3><p>A pattern that still appears in many codebases is using links as generic clickable elements:</p><pre>&lt;a href=&quot;#&quot;&gt;<br>  Open menu<br>&lt;/a&gt;</pre><p>or:</p><pre>&lt;a onclick=&quot;openModal()&quot;&gt;<br>  Settings<br>&lt;/a&gt;</pre><p>These are not really links.</p><p>They don’t navigate anywhere. They perform actions.</p><p>The problem with this approach is that developers often end up fighting against the browser:</p><ul><li>preventing default behaviour</li><li>manually handling keyboard interaction</li><li>managing focus</li><li>recreating button behaviour</li></ul><p><strong>If something performs an action, use a button.</strong></p><p><strong>If something navigates, use a link.</strong></p><p>Choosing the right element from the start removes a lot of unnecessary complexity (and headache further down the line!).</p><p><strong>Opening links in new tabs</strong></p><p>Another area where links can become confusing is opening new browser contexts.</p><p>For example:</p><pre>&lt;a href=&quot;/report&quot; target=&quot;_blank&quot;&gt;<br>  Annual report<br>&lt;/a&gt;</pre><p>Opening a new tab isn’t automatically wrong. There are legitimate reasons to do it.</p><p>However, unexpected changes in context can be confusing, especially for users who rely on assistive technologies.</p><p>If a link opens somewhere new, make that clear:</p><pre>&lt;a href=&quot;/report&quot; target=&quot;_blank&quot;&gt;<br>  Annual report<br>  &lt;span class=&quot;sr-only&quot;&gt;opens in new tab&lt;/span&gt;<br>&lt;/a&gt;</pre><p>As you can see from this example, visually the link will read “Annual report” but for screen readers it will read “Annual report opens in a new tab”. The sr-only class visually hides the text while keeping it available to assistive technologies. The important thing is not avoiding new tabs completely. It is ensuring users understand what will happen before they activate the link.</p><h3><strong>Skip links</strong></h3><p>Skip links are a great example of how a simple native link can solve a real accessibility problem.</p><p>On many websites, the first thing in the DOM is navigation:</p><ul><li>company logo</li><li>navigation menu</li><li>search</li><li>account controls</li><li>other repeated content</li></ul><p>For mouse users, this usually isn’t an issue. They can simply click where they need to go.</p><p>For keyboard users, however, this means having to navigate through the same content every time they visit a page.</p><p>A skip link provides a way to bypass that repeated content and jump directly to the main content:</p><pre>&lt;a href=&quot;#main-content&quot;&gt;<br>  Skip to main content<br>&lt;/a&gt;<br><br>…<br><br>&lt;main id=&quot;main-content&quot;&gt;<br>  Page content<br>&lt;/main&gt;</pre><p>When a keyboard user focuses this link and activates it, the browser moves focus directly to the main content.</p><p>The important thing here is that we don’t need to create a custom component or complex JavaScript solution. A native link already provides exactly the behaviour we need.</p><p>Skip links are often visually hidden until they receive focus, allowing them to stay out of the way for users who don’t need them while remaining available to keyboard users.</p><p>They are a great reminder that accessibility improvements don’t always need to be complicated. Sometimes the simplest solution is using the HTML element that already exists for the job.</p><h3><strong>The Principle: use native HTML first</strong></h3><p>Links are one of the simplest examples of where native HTML already provides the correct behaviour.</p><p>When something is navigation, use a link.</p><p>When something performs an action, use a button.</p><p>By choosing the correct element first, we get accessibility, browser behaviour, and expected interaction patterns without needing to add extra complexity.</p><p>Accessibility is not about adding more code. It’s about making better decisions earlier in the development process.</p><blockquote><strong><em>Accessibility by Default</em></strong><em> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</em></blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e4df59815743" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Buttons]]></title>
            <link>https://medium.com/@joshhallan/accessibility-by-default-buttons-5e7214da9e4c?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/5e7214da9e4c</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[web-accessibility]]></category>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[accessibility]]></category>
            <category><![CDATA[html]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 09 Jul 2026 13:21:26 GMT</pubDate>
            <atom:updated>2026-07-23T14:23:41.492Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LGwjEnFLxFIvb9RfStlC4Q.png" /></figure><p>Welcome back to Accessibility by Default. In the first post, we talked about getting the foundations right — structure, semantics, keyboard support, and focus management. Now we’re going to start looking at building more practical components.</p><p>To start with, we’re going to look at buttons. Even with things that look as simple as a button — I’ve seen them go wrong! Most developers look and think, “it’s just a button, that’s all there is to it,” but they’re wrong. Buttons can be one of those deceptively simple components where accessibility issues show up very quickly if you get the fundamentals wrong. A lot of that comes down to the number of variations we end up building in real applications.</p><h3>Native buttons vs everything else</h3><p>When you think about a button as just a visual element with a click handler, it feels simple. But from an accessibility perspective, there’s a lot more going on:</p><ul><li>Keyboard support</li><li>Focus behaviour</li><li>Accessible name behaviour</li><li>Interaction semantics</li></ul><p>These are all things you get “for free” when you use a native &lt;button&gt; element. That’s really the key point here: native HTML already solves most of this problem for you.</p><blockquote><strong><em>Architectural Tip:</em></strong><em> Never leave a button’s default type to browser guesswork. If a native </em><em>&lt;button&gt; lives inside a </em><em>&lt;form&gt;, the browser defaults its type to </em><em>submit. If a developer adds a click handler to execute local state changes without calling </em><em>e.preventDefault(), the entire page will unexpectedly reload. Make it an engineering standard to explicitly declare </em><em>&lt;button type=&quot;button&quot;&gt; or </em><em>&lt;button type=&quot;submit&quot;&gt; every single time.</em></blockquote><p>Where things start to go wrong is when developers recreate buttons using something like a &lt;div role=&quot;button&quot;&gt; and then try to rebuild all of that behaviour manually. That’s not impossible—but it’s easy to miss edge cases, especially around keyboard interaction and focus management. Browsers have had decades of work put into making native controls accessible. Replacing them means taking responsibility for all of that behaviour yourself—and trust me, some of that can get lost when you’re doing it yourself!</p><h3>Icon buttons and accessible names</h3><p>One of the first places buttons start to get complicated in real applications is with icon-only buttons. They are everywhere:</p><ul><li>search buttons</li><li>close buttons</li><li>menu toggles</li><li>settings icons</li></ul><p>Visually, they make sense. The icon usually communicates intent clearly to sighted users. But for assistive technologies, this is where issues start to appear. If there is no text, there is nothing to describe what the button does. Without that, you’ll often just get something like: <em>“button”</em>.</p><p>That’s not a useful experience for anyone. Imagine using a screen reader to go through a page and all you hear is <em>“button” “button” “button”</em> with no indication of what each one does.</p><h3>Where semantic HTML stops being enough</h3><p>A native &lt;button&gt; will automatically use its text content as its accessible name:</p><pre>&lt;button type=&quot;button&quot;&gt;Search&lt;/button&gt;</pre><p>But once you remove visible text and replace it with just an icon, that text context is lost:</p><pre>&lt;button type=&quot;button&quot;&gt;<br>  &lt;svg&gt;…&lt;/svg&gt;<br>&lt;/button&gt;</pre><p>At that point, you’ve lost the accessible name entirely, leaving the screen reader user with no context for what the button does. This is where developers usually reach for ARIA:</p><pre>&lt;button type=&quot;button&quot; aria-label=&quot;Search&quot;&gt;<br>  &lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;…&lt;/svg&gt;<br>&lt;/button&gt;</pre><p>And this is a valid use case. The point here isn’t that ARIA is good or bad — it’s that it should be used to restore meaning that would otherwise be lost, not to replace something that should already exist.</p><p>Notice that we also explicitly added aria-hidden=&quot;true&quot; and focusable=&quot;false&quot; to the child &lt;svg&gt;. Without this, some browsers and assistive technologies will dive into the SVG subtree and read internal path data or random IDs, polluting the screen reader announcement.</p><p>The first question should always be: <em>“can native HTML solve this problem?”</em></p><h3>Disabled buttons (and why they’re not always harmless)</h3><p>Another area that gets overlooked is disabled buttons. On the surface, disabling a button seems simple:</p><p>HTML</p><pre>&lt;button type=&quot;submit&quot; disabled&gt;Submit&lt;/button&gt;</pre><p>But there are a few subtle implications:</p><ul><li>Disabled buttons are completely removed from the browser’s tab order.</li><li>They may not be encountered or read in the same way by assistive technologies.</li><li>Users may not understand why the action is unavailable.</li></ul><p>This often leads to a poor experience when a form button is disabled without explanation, the state change isn’t communicated clearly, and users don’t know what they need to fix to continue.</p><p>A disabled button isn’t always the best experience. In many cases, it’s better to allow the interaction and explain what needs to be fixed, rather than silently preventing it altogether via the native disabled attribute.</p><p>HTML</p><pre>&lt;button type=&quot;submit&quot; aria-disabled=&quot;true&quot;&gt;Submit&lt;/button&gt;</pre><p>Using aria-disabled=&quot;true&quot; is a powerful compromise for form validation. It keeps the button focusable in the keyboard layout so screen reader and keyboard-only users can still locate the control and discover it exists. You can then style it to look inactive visually, communicate requirements via inline validation or helper text, and prevent the actual execution via JavaScript. The goal is not just to prevent interaction—it’s to communicate state clearly.</p><h3>Toggle buttons</h3><p>Buttons are not always just “do something once and done”. Sometimes they represent a persistent state. For example:</p><ul><li>favourite / liked</li><li>expand / collapse</li><li>bold / italic in text editors</li></ul><p>In these cases, a button is no longer just an action — it’s a toggle. This is where aria-pressed becomes useful:</p><pre>&lt;button type=&quot;button&quot; aria-pressed=&quot;true&quot;&gt;<br>  Favourite<br>&lt;/button&gt;</pre><p>This tells assistive technologies that:</p><ul><li>the button holds a state</li><li>that state can change</li><li>what the current state is</li></ul><p>Without this, a screen reader user just hears <em>“button”</em>, with no indication that it represents something persistent that can be toggled on or off.</p><h3>Buttons that aren’t really buttons</h3><p>A pattern you still see in older codebases (and unfortunately still in some modern ones) is using a non-interactive element and trying to “upgrade” it:</p><pre>&lt;div role=&quot;button&quot; tabindex=&quot;0&quot;&gt;<br>  Click me<br>&lt;/div&gt;</pre><p>This can be made to work, but it comes with immense trade-offs:</p><ul><li>you must manually handle keyboard interaction (listening for both Enter and Space keys)</li><li>you must manage focus behaviour yourself</li><li>you must ensure consistency across layout engines and devices</li><li>you must maintain all of this manual boilerplate forever</li></ul><p>And the most important point is that you’re reinventing something the browser already gives you for free. In almost all cases, a native button is simpler, safer, and more predictable:</p><pre>&lt;button type=&quot;button&quot;&gt;Click me&lt;/button&gt;</pre><p>The guiding principle is simple: If it behaves like a button, use a &lt;button&gt;.</p><h3>Buttons vs links</h3><p>This is a common area of confusion, and I’ve seen it go wrong on plenty of projects. Sometimes you end up with links behaving like buttons, and buttons behaving like links.</p><p>The simplest way to think about it is:</p><ul><li>Buttons — Triggers an action on the current page</li><li>Links (&lt;a&gt;) — Navigates the user to a new page, anchor, or route</li></ul><p>If something takes the user to a new page or route, it should be a link:</p><pre>&lt;a href=&quot;/settings&quot;&gt;Settings&lt;/a&gt;</pre><p>If it triggers a state change or operation on the current page, it should be a button:</p><pre>&lt;button type=&quot;button&quot;&gt;Save changes&lt;/button&gt;</pre><p>Where things go wrong is when these roles get swapped purely for styling reasons (e.g., links styled as buttons, or buttons used for navigation). This affects more than just visuals — it changes how screen readers announce the element, shifts the expected keyboard behaviour, and breaks user mental models.</p><p>You can style either element however you like visually to match your design system, but the underlying element must reflect the actual programmatic intent.</p><h3>The Principle: Use Native HTML First</h3><p>The biggest takeaway from buttons is simple: start with the HTML element that already matches the behaviour you need. Native buttons provide keyboard support, focus behaviour, and interaction semantics without needing custom code. The more we replace these built-in behaviours, the more engineering responsibility we take on ourselves.</p><p>Accessibility is not about adding complexity — it’s about making the right structural decisions early.</p><blockquote><strong><em>Accessibility by Default</em></strong><em> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</em></blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5e7214da9e4c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accessibility by Default: Foundational Principles for Building Accessible Interfaces]]></title>
            <link>https://www.designsystemscollective.com/accessibility-by-default-foundational-principles-for-building-accessible-interfaces-ab9cf3ee2ceb?source=rss-d2f146dad6b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/ab9cf3ee2ceb</guid>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[ux-design]]></category>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[accessibility]]></category>
            <dc:creator><![CDATA[Josh Allan]]></dc:creator>
            <pubDate>Thu, 02 Jul 2026 16:21:42 GMT</pubDate>
            <atom:updated>2026-08-07T08:16:07.823Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dBE6RFUNOEH-a5uI0ovs9Q.png" /></figure><p>For some developers, accessibility isn’t at the forefront when building new components, webpages or applications. Sometimes it isn’t considered until an accessibility issue is picked up in code review or later in testing.</p><p>In an ideal world, accessibility should be one of the first things you think about when looking at a new design or working through functionality. It shouldn’t be a box-ticking exercise you deal with at the end of a sprint when someone reminds you it exists.</p><p>Instead, it should be part of the engineering defaults set out by the team — something you do by default when building, so that everything is usable by as many people as possible from day one.</p><p>Most accessibility issues don’t come from edge cases. They come from incorrect foundations.</p><p>I’ve seen a handful of developers (not naming any names) who will sometimes hotfix accessibility issues on the fly, whereas if the component had been structured correctly in the first place, the issue wouldn’t exist.</p><p>Get the structure, semantics, and interaction patterns right early, and a surprising number of accessibility problems never show up in the first place.</p><h4>Semantic HTML vs ARIA</h4><p>This is probably one of the most misunderstood parts of accessibility, and also one of the easiest places to accidentally make things worse.</p><p>The easiest way to think about it is:</p><p><strong>HTML gives meaning. ARIA tries to patch meaning on top.</strong></p><p>If you can use a native HTML element, you should. When we have a bunch of different elements, why would we look at overcomplicating the built in functionality that is already there? If you consider in HTML we already have &lt;button&gt;, &lt;input&gt; and &lt;nav&gt;; these elements already tell assistive technologies how to behave and interact with the page.</p><p>When you start to replace them with generic elements such as &lt;div&gt; and then layer a bunch of ARIA roles on top, you’re basically reinventing the wheel when that behaviour already exists — and there’s a good chance that some of the key functionality will be missed.</p><p>That isn’t to say that ARIA is bad — it’s more so that it has a specific job and misusing it can cause headaches and issues down the line.</p><p>ARIA is useful when:</p><ul><li>You’re building something doesn’t exist in HTML</li><li>You need to enhance meaning for assistive technologies</li><li>You can’t achieve the behaviour with native elements</li></ul><p>ARIA isn’t good for:</p><ul><li>Fixing broken HTML structure</li><li>Replacing native elements</li><li>Sprinkling in roles just in case</li></ul><h4>Hierarchy and Document Structure</h4><p>I feel as though this is one of those areas that doesn’t get much attention, but it can easily affect everything else.</p><p>When people think about accessibility, 9 times out of 10 they jump straight into ARIA, but the structure of the page underneath is usually what can make or break the user experience.</p><p>At a basic level, a webpage isn’t just the visual layout — there’s a hierarchy that assistive technologies rely on to make sense of what is going on.</p><p>Headings are probably the simplest example of this. They’re not just for styling or making text look bigger — they create a map of the page. This map is what assistive technologies use to navigate coherently around the webpage.</p><p>The same can be applied to landmarks such as &lt;main&gt;, &lt;nav&gt; and &lt;footer&gt;. These elements give structure to the page and help users understand where they are in the flow of the page without needing to interpret the visual layout.</p><p>One of the most common misuses I see is when heading levels are chosen based on visual size, rather than the structure of the page. For example, jumping from an &lt;h1&gt; to an &lt;h4&gt; just because “it looks right” visually. That might not seem like a big deal, but it breaks the logical flow of the document.</p><p>A good way to think about it is:</p><p><strong>If you took away all of the styling, would the webpage still make sense in the right order?</strong></p><p>If the answer is no, then the structure probably needs work.</p><p>One thing I find myself doing often is turning on a screen reader and closing my eyes to navigate a page. If I can do that and understand what’s going on, and I know where I am, then we’re in a good place.</p><h4>Keyboard Accessibility</h4><p>If there’s one area that instantly exposes accessibility issues within a system, it’s keyboard navigation.</p><p>It can sound basic, but if something doesn’t work properly with a keyboard, it’s usually a sign that something more fundamental has been missed.</p><p>A lot of users don’t use a mouse at all, and some will switch between keyboard and assistive technologies depending on the task. So if your webpage can only be operated with a mouse, you’re effectively limiting how much of it can be used.</p><p>At a minimum, every interactive element should be reachable and usable with a keyboard. That includes buttons, links, form controls, menus, and anything custom that behaves like a control.</p><p>Native HTML elements handle this for you by default, which is another reason they’re so important. A &lt;button&gt; can be focused, activated with Enter or Space, and behaves consistently across browsers without any extra work. When you move away from native elements, you have to rebuild all of that yourself — and that’s where things often start to break.</p><p>One of the most common problems is interactive elements that are technically clickable, but not actually focusable or operable via keyboard. It might work fine with a mouse, but if you tab through the page and hit a dead end, that’s a problem.</p><p>It’s at this point in the development process that custom components start to show their complexity. Things like dropdowns, modals, and tabs all need to behave in a predictable way when interacted with using a keyboard, not just mouse clicks.</p><h4>Focus Management</h4><p>If keyboard accessibility is what lets users move around a page, focus management is what tells them where they are when things change.</p><p>This becomes especially important in more dynamic applications, where content updates without a full page reload — things like single page applications, modals, dialogs, and routing changes.</p><p>In traditional multi-page websites, focus naturally resets because the page reloads. In SPAs, that doesn’t happen — so you have to be intentional about where focus goes after a change in state.</p><p>A simple rule of thumb is:</p><p><strong>When something significant changes in the UI, the user’s focus should usually move with it.</strong></p><p>That might mean:</p><ul><li>moving focus to a newly opened modal</li><li>shifting focus to a page heading after navigation</li><li>returning focus to the triggering element when something closes</li></ul><h3>Modals and focus trapping</h3><p>Modals are a good example of where this becomes obvious. When a modal opens, focus should move into it and stay contained within it until it is closed. If focus can escape behind the modal, users can end up interacting with content that is no longer relevant or visible.</p><p>This is usually referred to as <strong>focus trapping</strong> — keeping keyboard focus inside the modal while it is active.</p><p>While you should almost always lean on battle-tested architectural primitives (like Radix, headless UI, or React Aria) to handle focus trapping in production, understanding the raw underlying mechanics is essential for any frontend engineer:</p><ul><li>find all focusable elements inside the modal</li><li>move focus to the first one</li><li>intercept Tab and Shift + Tab to cycle within the modal</li></ul><pre>// Note: This is a simplified conceptual model to demonstrate the mechanics. <br>// In production architecture, use a battle-tested primitive (like Radix or <br>// React Aria) <br>// to handle edge cases like shadow DOM, hidden elements, and Safari bugs.<br><br>/**<br> * Returns a list of elements within the specified element that can be focused<br> * @param {HTMLElement} containerElement Parent element to search inside<br> * @return {NodeList} The focusable elements within containerElement<br> */<br>const getFocusableElements = (containerElement) =&gt; {<br>  return containerElement.querySelectorAll(<br>    `a[href], area[href], input:not([disabled]),<br>    select:not([disabled]), textarea:not([disabled]),<br>    button:not([disabled]), [tabindex=&quot;0&quot;]`,<br>  );<br>};<br><br>// Focus the first focusable element if it exists<br>if (focusableElements.length &gt; 0) {<br>  focusableElements[0].focus();<br>}<br><br>/**<br> * Handles tab key event with given focusable elements<br> * to keep focus within the modal.<br> */<br>const handleTabKey = (event, activeElement, focusableElements) =&gt; {<br>  if (event.key !== &#39;Tab&#39;) return;<br>  if (focusableElements.length &lt; 2) {<br>    event.preventDefault();<br>    return;<br>  }<br>  if (event.shiftKey &amp;&amp; activeElement === focusableElements[0]) {<br>    event.preventDefault();<br>    focusableElements[focusableElements.length - 1].focus();<br>  } else if (<br>    !event.shiftKey &amp;&amp;<br>    activeElement === focusableElements[focusableElements.length - 1]<br>  ) {<br>    event.preventDefault();<br>    focusableElements[0].focus();<br>  }<br>};</pre><p>It’s not the prettiest or most “modern” implementation, but the underlying idea is what matters — keeping focus behaviour predictable inside a contained interaction.</p><p>The same idea applies to route changes in SPAs. If a user navigates to a new view but focus stays on the element that triggered the navigation, they may not realise anything has changed.</p><p>In those cases, moving focus to something meaningful — usually the main heading or primary landmark — helps re-orient the user.</p><p>Another common issue is not returning focus to where it came from. For example, opening a menu or dialog and then closing it, but leaving focus somewhere unexpected instead of restoring it to the original trigger.</p><p>That kind of break in continuity can make an interface feel disjointed, especially for keyboard users who rely on predictable navigation.</p><p>Focus management is really about continuity. It’s not just about where focus goes next — it’s about making sure the user always understands their position in the interface as things change.</p><p>If keyboard accessibility is about movement, focus management is about orientation.</p><h4>Accessible Names</h4><p>One of the easiest accessibility issues to introduce is giving an interactive element no meaningful name.</p><p>If you’ve ever tabbed onto a button with a screen reader and heard “button” instead of “Search button” or “Close button”, you’ve experienced the problem first hand.</p><p>An accessible name is just how assistive technologies identify an element to a user. If that name doesn’t exist, or it isn’t descriptive enough, then the user is left guessing what the functionality should be.</p><p>This is another reason why native HTML should always be your first choice. As we discussed earlier, HTML gives you a lot of accessibility for free, and accessible names are a great example of that. If we look at a &lt;button&gt; , this button gets its accessible name from the text inside of it. A form field gets its name from the associated &lt;label&gt; . If you’re using semantic HTML correctly then you’re already most of the way there.</p><pre>// Example 1<br>&lt;label for=&quot;expire&quot;&gt;Expiration date (MM/YYYY): &lt;/label&gt;<br>&lt;input type=&quot;text&quot; name=&quot;expire&quot; id=&quot;expire&quot;&gt;<br><br>// Example 2<br>&lt;label<br>  &gt;I agree to the terms and conditions.<br>  &lt;input type=&quot;checkbox&quot; id=&quot;terms&quot; name=&quot;terms&quot; /&gt;<br>&lt;/label&gt;</pre><p>Both examples achieve exactly the same outcome. In the first example, the &lt;input&gt; is associated with the &lt;label&gt; using the for and id attributes. In the second example, the &lt;input&gt; is nested inside the &lt;label&gt;, which creates the association automatically (this is something we’ll cover in a later post). While both approaches are spec-compliant, explicit binding (Example 1) is the gold standard for enterprise architecture. Nesting inputs implicitly (Example 2) can still confuse certain voice-dictation software and older screen readers. Stick to explicit IDs to ensure bulletproof coverage.</p><p>If we think of an icon only button, it may only have an icon depicting a magnifying glass and no text. Visually, this might make sense to a user to know that it’s a search button, however, for screen readers it will be announced just as “button” leaving the user wondering what this button does.</p><p>This is where ARIA becomes useful. Adding in an aria-label=”Search” gives the button a meaningful accessible name without changing what is displayed visually. The important thing isn’t that you’ve used ARIA — it’s that you’ve give users enough information to understand what the button actually does.</p><h4>ARIA — When it’s Appropriate (and When it Isn’t)</h4><p>If there’s one thing I hope that you take away from this article, it’s that ARIA isn’t the starting point for accessibility.</p><p>It’s an incredibly useful technology when it’s used for the right reasons, but it should never be the first tool you reach for.</p><p>If HTML already has an element that does what I need, use it.</p><p>A &lt;button&gt; is already a button</p><p>A &lt;nav&gt; is already navigation</p><p>An labelled &lt;input&gt; already shows it purpose to assistive technologies</p><p>Adding ARIA to these elements often provides no additional benefit and, in some cases, can actually make the experience worse if it’s implemented incorrectly.</p><p>Where ARIA can really help is when you’re building something that HTML doesn’t provide out of the box, things such as:</p><ul><li>Complex tab interfaces</li><li>Comboboxes</li><li>Autocompletes</li><li>Custom listboxes</li></ul><p>In those situations, ARIA helps communicate information to assistive technologies that native HTML alone can’t express.</p><p>One phrase you’ll hear a lot in accessibility circles is</p><blockquote>No ARIA is better than bad ARIA</blockquote><p>Use ARIA when you genuinely need it, not because you feel like every accessible component should contain it.</p><h4>Conclusion</h4><p>If you’ve made it this far, hopefully one thing has become obvious:</p><p>Accessibility isn’t about memorising hundreds of WCAG success criteria or throwing ARIA attributes everywhere.</p><p>Its about building on good foundations.</p><ul><li>Use semantic HTML</li><li>Create logical structure</li><li>Make everything work with a keyboard</li><li>Manage focus when your interface changes</li><li>Give controls meaningful names</li></ul><p>Once those become habits rather than afterthoughts, you’ll find that many accessibility issues never appear in the first place. Accessibility isn’t something you need to add at the end of the project and it isn’t something that QA should remind you about. It is a principle that should baked into development from the very start. If accessibility becomes one of your engineering defaults, you’ll spend far less time fixing problems later — and you’ll build experiences that work for far more people from day one.</p><p>This article isn’t intended to replace the specifications or guidance from W3C. It’s a set of principles that I’ve found myself coming back to time and time again throughout my career and I wanted to share some of the lessons I’ve learnt over the years in the hope that they help other developers build accessibility into their work from the very beginning. If you’re new to accessibility, I’d encourage you to start here. Once you get these foundations right, everything else will become a lot easier.</p><blockquote><strong>Accessibility by Default</strong> is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.</blockquote><blockquote>You can find the full series on my profile</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ab9cf3ee2ceb" width="1" height="1" alt=""><hr><p><a href="https://www.designsystemscollective.com/accessibility-by-default-foundational-principles-for-building-accessible-interfaces-ab9cf3ee2ceb">Accessibility by Default: Foundational Principles for Building Accessible Interfaces</a> was originally published in <a href="https://www.designsystemscollective.com">Design Systems Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>