I’ve been covering the WCAG guidelines and accessibility in design, but what about developers? 🤔 What can a developer do to ensure a site is accessible? A lot, Actually! Developers are given many tools to provide the best experience to users, and many work right out of the box. If you write solid HTML and CSS, chances are your site is already partially accessible because semantic HTML handles many standard use cases. But when we need to go further, ARIA is there to help. To set the stage for the practical strategies that follow, let’s first understand how modern web technologies like ARIA extend accessibility beyond what semantic HTML can offer.
Understanding ARIA and Its Role
Accessible Rich Internet Applications (ARIA) is a set of roles and attributes that define ways to make web content and web applications (especially JavaScript-heavy ones) more accessible to people with disabilities .
Think of ARIA as the DOM API’s accessibility counterpart—it’s so closely tied that sometimes the two feel merged, even though they affect different trees: the DOM Tree and the Accessibility Tree. Understanding the impact of ARIA and semantic HTML requires us to explore the underlying model that assistive technologies rely on: the Accessibility Tree.
The Accessibility Tree Explained
The Accessibility Tree is generated from more than just ARIA roles—it’s a distinct model that could benefit from a visual representation. Imagine two trees side by side: one showing every node in the DOM, and another showing only those nodes with semantic meaning or accessibility relevance. This second tree—cleaned, trimmed, and optimized—is the Accessibility Tree. It draws from a combination of factors:
- Semantic HTML (like
<button>
,<nav>
,<article>
) - ARIA attributes
- Visibility-related CSS (e.g.,
display: none
,visibility: hidden
) - Element state and content
This tree is separate from the DOM and represents how assistive technologies interpret your page. Not everything in the DOM makes it into the Accessibility Tree—only elements considered meaningful for accessibility. Understanding this difference is key to debugging issues that aren’t apparent visually but affect screen readers or keyboard navigation.
To bridge this gap, the Accessibility Object Model (AOM) is being proposed to let developers programmatically access and manipulate the accessibility tree itself. This initiative could lead to greater alignment between visual UI frameworks and assistive tech experiences, particularly in JavaScript-heavy or component-based UIs.
The Accessibility Tree acts as an accessibility-focused map of your page that assistive technologies like screen readers use. It’s built alongside the DOM, drawing from it, but tailored with specific roles, descriptions, names, and states. Ignored nodes—those that don’t carry useful accessibility info—are pruned, letting their children take their place and keeping the tree clean and efficient.
How to Read the Accessibility Tree
Reading the Accessibility Tree feels similar to reading the DOM. First, make sure it’s visible in DevTools: if you see the accessibility icon in the Elements panel, you’re set; if not, head to Settings > Experiments and enable Enable Full Accessibility Tree View in the Elements Panel
. Each node kicks off with the element’s role, followed by the text announced by a screen reader (either the element’s content or inherited from a child). Then come the node’s attributes—see the full list if you’re curious.
✨ Fun note: skip over nodes labeled as Ignored
in the tree—they only exist for DOM parity and won’t affect the assistive tech experience.
For example, imagine a parent node with a contentinfo
role but no content—it lacks ARIA attributes and doesn’t inherit content from children, so it shows up empty. In contrast, a link node inherits content from its child image because it’s the lowest focusable node.
Sometimes, the Tree View isn’t enough. If you want detailed info on a node, use the Accessibility Tab alongside the Tree View. It reveals role, content, attributes, and ARIA-specific tweaks all in one spot. For an overview of how this information may eventually become standardized across browsers, check out the Accessibility Object Model (AOM) explainer. But accessibility isn't just about structure—it's about movement and interaction. That’s where the concept of Source Order versus Tab Order comes in.
Source Order vs. Tab Order
We also need to talk about Source Order vs. Tab Order—they’re more different than you’d expect 👀. Source Order is the sequence of nxodes inside their parent in the DOM. Tab Order is the sequence of focusable nodes navigated using the Tab key. If a focusable element lacks tabindex
, it follows a depth-first traversal of Source Order (visualized conceptually as a traversal path). Want to visualize Source Order? Enable the source order overlay in the Accessibility Tab while a parent node is selected. But to see Tab Order, you’ll need a third-party tool like Taba11y or Polypane 🔍.
⚠️ Important: <p>
tags aren’t focusable by default. Add tabindex="0"
if you want them included in Tab Order. The cleaner your Accessibility Tree, the smoother alternative navigation will be.
Even with the added power of ARIA, it’s semantic HTML that continues to anchor accessible design—enabling meaning to be conveyed clearly and automatically.
The Power of Semantic HTML
Semantic HTML represents a foundational paradigm in accessible web development, wherein HTML elements are selected not merely for their visual or layout properties, but for the intrinsic semantic information they convey to both user agents and assistive technologies. When properly implemented, semantic elements such as <nav>
, <article>
, <button>
, and <dialog>
act as declarative annotations of structural and functional intent, enabling the accessibility tree to be constructed with minimal ambiguity.
Empirical evaluations and standardized practices suggest that a judicious application of semantic HTML alone can satisfy approximately 80% of the WCAG 2.1 A and AA success criteria for static or content-driven web applications. Consider the following illustrative comparison:
<!-- Manual implementation -->
<div
id="button"
role="button"
aria-describedby="#button"
tabIndex="0"
>
Re-Implemented Button
</div>
<style>
#button {
padding: 2px 7px;
display: inline-block;
border-radius: 2px;
background-color: #EFEFEF;
border: solid 1px #767676;
font-family: Arial;
font-size: 0.833rem;
user-select: text;
cursor: default;
&:hover {
border-color: #4F4F4F;
background-color: #E5E5E5;
}
&[button-held="true"] {
background-color: #F5F5F5;
border-color: #8D8D8D;
}
}
</style>
<script type="application/javascript">
const button = document.querySelector('#button')
let pressed = false
button.addEventListener("click", event => {
event.preventDefault()
doSomething() // Random Function to Trigger
})
button.addEventListener("keydown", event => {
switch(event.code) {
case "Space":
button.ariaPressed = true
pressed = true
break
case "NumpadEnter":
case "Enter":
button.ariaPressed = true
button.click()
break
}
})
button.addEventListener("keyup", () => {
button.ariaPressed = false
if(pressed && !is_touch) {
button.click()
pressed = false
}
})
button.addEventListener('mousedown', () => {
button.ariaPressed = true
button.style["user-select"] = "none"
button.setAttribute("button-held", true)
})
button.addEventListener('mouseup', () => {
button.ariaPressed = false
button.style["user-select"] = "text"
button.setAttribute("button-held", false)
})
button.addEventListener('touchstart', () => {
button.ariaPressed = true
button.setAttribute("button-held", true)
})
button.addEventListener('touchend', () => {
button.ariaPressed = false
button.setAttribute("button-held", false)
})
</script>
<!-- Semantic implementation -->
<button onclick="doSomething()">Click me</button>
While the first approach syntactically mimics a button, it demands additional scripting to emulate keyboard and touch behavior (e.g., support for Enter
and Space
), and risks semantic misalignment. The second example, however, leverages the native capabilities of the <button>
element—conveying purpose, affordance, and interaction model without the need for redundant ARIA annotations or bespoke keyboard event handling.
Beyond isolated elements, semantic HTML contributes to page-level comprehension. Elements such as <header>
, <footer>
, <section>
, and <main>
articulate the document's rhetorical and navigational structure, allowing screen readers to construct a robust mental model for users. For instance:
<header>
<h1>Title</h1>
</header>
<aside>
<nav>nav stuff</nav>
</aside>
<main>
<article>
<h2>Title</h2>
<p>Text</p>
<h3>Section</h3>
<p>Text 2</p>
</article>
</main>
<footer>
<ul>
<li>footer content</li>
</ul>
</footer>
The cognitive and functional affordances embedded in these tags are interpreted not only by browsers but also by accessibility APIs that underpin screen readers and other assistive agents. Thus, semantic HTML does not merely beautify the DOM; it elevates the experience to one of inclusivity and interoperability across diverse modalities of interaction.
Fine-Tuning the Accessibility Tree
Once you've structured content semantically, the next level of accessibility involves curating exactly how it's represented and interacted with by assistive technologies. Developers aren’t limited to passive compliance when it comes to accessibility—they can actively shape how assistive technologies perceive and interact with their content. This fine-tuning of the Accessibility Tree allows us to curate what should be read aloud, what should be ignored, and how content is announced. The goal isn’t just to comply with guidelines—it’s to craft a clean, meaningful, and navigable experience for everyone.
Hiding Non-Essential Elements with aria-hidden
Before introducing the code snippet, it’s worth understanding why we might want to hide certain elements from assistive technologies. Not all content is useful for screen readers or keyboard users—decorative imagery, redundant text, or offscreen navigation elements can create a noisy experience. The aria-hidden
attribute allows developers to suppress elements from the accessibility tree, ensuring that only meaningful, actionable content is announced.
<div aria-hidden="true">Decorative content</div>
This code snippet removes the div
and its children from the accessibility tree entirely. Although it remains visible and functional for sighted users, it is completely ignored by assistive technologies like screen readers. It’s ideal for background visuals, purely decorative SVGs, or redundant labels that would otherwise be announced multiple times.
🚫 Making Regions Inert
Sometimes, it’s not enough to hide content from screen readers—we also want to prevent user interaction entirely. This is where the inert
attribute comes in. Unlike aria-hidden
, which only affects the accessibility layer, inert
removes an element and its children from both the accessibility tree and the focus/navigation flow. This makes it perfect for modals, drawers, or other UI sections that should be temporarily disabled 😌.
<section inert>
<p>This modal is inactive.</p>
</section>
This example demonstrates a section of the page that is inert—meaning that it is both inaccessible to assistive technologies and non-interactive for all users. It's especially useful for managing complex interactive states, like disabling a background while a modal is open, without requiring extra JavaScript or state toggles.
Not familiar with inert
? MDN has a detailed explanation here 📝.
Additionally, it's worth noting that the native <dialog>
element implicitly supports this pattern. When a dialog is open using .showModal()
, all other content on the page is automatically set to an inert state. This means users cannot interact with anything outside the modal until it is closed, streamlining both accessibility and user experience without additional scripting. This built-in behavior ensures keyboard focus remains inside the dialog, and screen readers ignore the rest of the page—effectively mirroring the manual use of the inert
attribute.
Ensuring Labels Exist for Icon Buttons
Not every visual element comes with built-in text. Sometimes icons are used in buttons without visible labels. While minimal and clean, this can introduce a serious accessibility problem—screen readers need a name to announce the purpose of that button. That’s where aria-label
comes in, offering a programmatic name that doesn’t clutter the visual UI.
<button aria-label="Search">
<svg><!-- icon --></svg>
</button>
In this example, the screen reader will announce “Search,” even though the button is visually only an icon. This makes the interaction clear without requiring visible text, preserving the UI’s design while ensuring it remains accessible to all 👍.
Managing Expandable Content
Expandable content components—like accordions, toggles, and collapsible sections—are another area where accessibility often breaks down. To make these patterns inclusive, developers must manage state visibility, keyboard support, and ARIA attributes.
For instance, each collapsible region should be associated with a trigger element, typically a button. Use aria-expanded
on the button to communicate the open/closed state, and link it to the collapsible panel using aria-controls
. The collapsible panel should also have a unique ID for this relationship.
<!-- Collapse Content -->
<button aria-expanded="false" aria-controls="section1">Toggle Section</button>
<div id="section1" hidden>
<p>Expandable content goes here.</p>
</div>
<!-- Collapse Multiple Section -->
<button aria-expanded="false" aria-controls="section1 section2">Toggle Section</button>
<div id="section1" hidden>
<p>Expandable content goes here.</p>
</div>
<div id="section2" hidden>
<p>Expandable content goes here.</p>
</div>
When the button is clicked, update aria-expanded
to reflect the current state and toggle the hidden
attribute or CSS visibility of the associated content. Properly implemented, this pattern ensures that screen readers and keyboard users understand the interactive structure and can engage with the content meaningfully.
📢 Announcing Dynamic Updates with Live Regions
Dynamic updates present a unique challenge for accessibility. Content that updates without a full page reload—like chat messages, alerts, or status indicators—may go unnoticed by users relying on screen readers unless explicitly announced. ARIA live regions enable these updates to be communicated automatically. Think of aria-live="polite"
as gently raising your hand in a meeting—wait your turn, don’t interrupt. Meanwhile, aria-live="assertive"
is like pulling the fire alarm—everything stops and the announcement takes priority.
<div aria-live="polite">New message received.</div>
Here, aria-live="polite"
ensures assistive technologies will announce any change in the element at the next available opportunity, without interrupting ongoing speech. You can also use aria-live="assertive"
for high-priority updates that should be announced immediately. The key is to ensure that dynamic content doesn't silently slip by users who depend on auditory cues 🫣. Through deliberate use of ARIA, inert
, and live regions, developers take control of how content is communicated—shaping an intentional, inclusive experience.
CSS and Accessibility
While accessibility often starts in HTML, CSS has a surprisingly powerful role to play in reinforcing clarity, comfort, and control for users. Its influence on accessibility is both subtle and profound. Thoughtful styling can dramatically improve usability for keyboard and screen reader users, especially when it comes to focus visibility, motion sensitivity, and content visibility. Now let’s explore how CSS, often thought of as purely visual, can significantly enhance inclusive experiences. One of the most immediate ways CSS intersects with accessibility is through focus management—specifically, how clearly users can see which element is currently selected or active on the page.
Provide Clear Focus Indicators
Focus is the mechanism that determines which element on a web page is currently ready to receive user input—like key presses or screen reader commands. Focused elements typically include links, buttons, form fields, and any custom widgets built with JavaScript. Proper focus management is essential for keyboard navigation, screen reader usability, and maintaining orientation within the UI. There are three primary focus pseudo-classes developers should understand:
-
Focus (
:focus
) applies any time an element receives focus, no matter the input method—keyboard, mouse, or programmatically via JavaScript. -
Focus-visible (
:focus-visible
) provides a more nuanced condition: it only applies focus styles when the user is navigating with the keyboard or assistive technology. This prevents distracting outlines for mouse users. -
Focus-within (
:focus-within
) allows you to style a parent container when any of its child elements have focus. It's especially useful for grouping related form fields, cards, or interactive sections.
For users who navigate via keyboard, visual cues are essential for knowing where the current focus lies. Unfortunately, many developers disable default focus outlines for aesthetic reasons, often leaving users disoriented and unable to navigate effectively 😣. While visible focus is crucial for orientation, motion also plays a surprising role in accessibility. For some users, animations aren’t just distracting—they’re debilitating.
🌀 Respect Reduced Motion Preferences
Animations and transitions can enhance visual appeal but may cause discomfort or cognitive overload for users with vestibular disorders or motion sensitivity. The prefers-reduced-motion
media query lets us tailor experiences based on user system preferences—a powerful way to demonstrate empathy through code.
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
This CSS snippet disables motion effects when a user has requested reduced motion, creating a calmer, safer interface without sacrificing functionality. Just as motion preferences help tailor experiences to user sensitivities, screen-reader-only content enables tailored experiences for users navigating by sound rather than sight.
👻 Hide Visually, Expose Semantically
In some cases, you want to provide helpful content to screen readers without cluttering the UI. This is where the classic .sr-only
(screen-reader only) utility comes in—it visually hides content while keeping it accessible to assistive technologies.
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
clip: rect(0 0 0 0);
overflow: hidden;
}
This technique is perfect for adding descriptive labels or guidance for users without altering your visual layout. For example:
<button>
<svg><!-- icon --></svg>
<span class="sr-only">Submit form</span>
</button>
Here, screen readers will announce "Submit form," even though the button is visually represented only by an icon. This keeps your interface sleek while ensuring it remains understandable and actionable to all users. However, not all hidden content is equal. When developers use pseudo-elements like ::before
or ::after
for essential information, they risk creating visual cues that never make it into the accessibility tree.
❗ Don’t Rely on ::before
or ::after
for Essential Content
Pseudo-elements like ::before
and ::after
are great for decorative touches, but their content is not exposed to assistive tech. If you use them for critical information—like marking required fields—screen reader users may be left out.
<label for="email">Email <span aria-hidden="true">*</span></label>
<input type="email" id="email" aria-required="true">
In this pattern, the asterisk is visible for sighted users but hidden from screen readers (aria-hidden="true"
), while the aria-required="true"
attribute conveys necessity programmatically. This dual strategy ensures clarity for all users, regardless of how they access the page 💡. As these examples show, CSS isn’t just about looks—it’s an accessibility tool in its own right, capable of reinforcing or breaking inclusive experiences depending on how it's used.
Accessibility as a Mindset
Accessibility is more than a standard or compliance metric—it’s a practice that refines our programming, strengthens our design choices, and broadens our audience reach. Thinking inclusively encourages us to anticipate diverse needs, clarify intent through better code structure, and create user experiences that adapt to real-world contexts. In doing so, we don’t just meet legal or technical benchmarks—we build products that attract and serve more people by default. This perspective naturally builds on everything discussed so far—semantic HTML, ARIA, CSS, and the accessibility tree all work together to create a more inclusive experience: each of these tools gives us a way to communicate with clarity and empathy. Of the concepts we explored, a few stand out as especially critical to retain:
- Use semantic HTML as the foundation—it’s your most powerful out-of-the-box accessibility tool.
- Understand and inspect the Accessibility Tree to surface issues that visual inspections can miss.
- Use ARIA thoughtfully—as a supplement, not a substitute, for semantic tags.
- Prioritize focus indicators and motion preferences—small CSS details can have a big impact on usability and comfort.
These essentials lay the groundwork for accessible, inclusive experiences and should be considered non-negotiable when building for the web. Ultimately, accessibility isn't achieved through checklists alone—it flourishes when inclusive thinking becomes part of your development mindset. It becomes a habit—a lens through which you design, review, and write code. The value isn't limited to checklists or audits; it's about building software that reflects the diversity of those who use it. Ask not just “does it work?” but “does it work for everyone?” Because if it doesn’t, someone gets excluded—often silently—and that’s a bug, not a feature. Test with a keyboard: Tab, Shift+Tab, Enter, Space, Arrow keys. You’ll catch focus traps or missing indicators that automated tools miss.
Pair manual checks with browser audits and screen reader testing. Tools like Axe DevTools, Lighthouse, or NVDA can help uncover accessibility issues early and provide actionable insights. Consider feedback from real users, especially those who rely on assistive technology. Accessibility is not a static requirement—it’s an evolving discipline that invites us to build with more care, more intention, and ultimately more impact 💪.
TL;DR: Key Accessibility Practices for Developers:
- Use semantic HTML over ARIA whenever possible
- Understand how the Accessibility Tree differs from the DOM
- Ensure keyboard navigability and visible focus
- Hide only what’s necessary—don’t overuse
aria-hidden
- Respect motion preferences via CSS
- Audit using tools like Axe, Lighthouse, or NVDA
Conclusion
Making accessibility a first-class consideration in development isn't just a technical requirement—it's a design philosophy grounded in empathy and respect. Every keystroke and element can either widen or close a door. By embracing practices like semantic HTML, understanding the Accessibility Tree, using ARIA thoughtfully, and reinforcing designs with accessible CSS, we take real steps toward a more inclusive web. Start small, stay curious, and keep accessibility top-of-mind in every decision. The more you build with everyone in mind, the better your software—and the internet—becomes 🌍.
References and Resources
This article isn't an all encompassing repository about web accessibility, the intent is a Know Before You Go Guide targeted towards the intermediate or advanced web developer. There is more to ARIA that covers very specific and niche use-cases which you can find here. To continue deepening your understanding about Web Accessibility, here are some resources worth exploring:
Top comments (2)
Nice post 🔥
Thank you!