A Practical Guide to Web Accessibility in React and Vue (a11y)

I've been writing code for frontend applications for more than 7 years already, and during all this time I've personally been caught into the same trap many times: we focus on a pixel perfect implementation of the design, state management, re-renders, API calls and slick animations, to only then remember about accessibility.
Or, let's be honest: the reality of our industry is that when we're near deadlines and the backlog is overflowing, accessibility is often the first thing pushed to 'Phase 2', especially on internal tools where the immediate pressure is just to make it functional.
We get caught up in optimizing effects and performance, completely forgetting that a significant portion of our users might be navigating our app with a keyboard or a screen reader.
Modern frameworks like React and Vue don't inherently make applications inaccessible, but their component-driven nature makes it incredibly easy to accidentally build a web of meaningless <div> tags.
Let's look at how we can build inclusive modern applications using the most important accessibility best practices, with particular focus on Vue and React.
What is Web Accessibility?
The World Health Organization estimates that 15% of the world's population has some form of disability, 2-4% of them severely so. That is an estimated 1 billion people worldwide; making people with disabilities the largest minority group in the world.
At its core, web accessibility (often abbreviated as a11y) is the practice of designing and developing applications so that everyone, including people with disabilities, can use them without barriers.
It ensures that users with disabilities, a slow connection, outdated hardware or relying on assistive technologies — like screen readers, keyboard-only navigation, or voice commands — can perceive, understand, navigate, and interact with our interfaces just as seamlessly as those using a standard mouse and monitor in perfect conditions+.
In order to make a web page truly accessible, we must build upon a foundation of semantic HTML, intentionally manage user focus during dynamic state changes, and clearly communicate the UI's context through text alternatives and ARIA attributes.
WCAG (2.1) lists a series of recommendations and requirements we should rely on in order to build accessible web pages. The four main guiding principles are, abbreviated as POUR, should serve as our ideal guidelines:
Perceivable: Users must be able to perceive the information being presented
Operable: Interface forms, controls, and navigation are operable
Understandable: Information and the operation of user interface must be understandable to all users
Robust: Users must be able to access the content as technologies advance
Mastering Layout and Content Structure
Landmarks and Structural Semantics
Good semantics start with layout landmarks.
You may ask, why they are so important? Well, screen reader users don't read a page top-to-bottom on first load: they jump between major regions using keyboard shortcuts. If your entire layout is built out of <div> tags, assistive technologies cannot map the page.
We should always use explicit HTML5 landmark elements to define the layout structure:
HTML | ARIA Role | Landmark Purpose |
|
| Main heading / title of the page |
|
| Collection of links suitable for use when navigating the document or related documents |
|
| The main or central content of the document |
|
| Information about the parent document: footnotes/copyrights/links to privacy statement |
|
| Supports the main content, yet is separated and meaningful on its own content |
|
| This section contains the search functionality for the application |
|
| Collection of form-associated elements |
|
| Content that is relevant and that users will likely want to navigate to. Label must be provided for this element |
The first rule of ARIA:
Worth to briefly mention some very important concepts regarding ARIA attributes:
If you can use a native HTML element instead of an ARIA-enhanced custom component, do so.
Prefer<button>over a clickable<div>,<details>over a custom accordion, and native form controls whenever possible.
Generally speaking, ARIA should enhance semantics, not replace them.
Skip Navigation Links
Imagine navigating a large application using only a keyboard.
Every time a new page loads, you would need to repeatedly press the Tab key to move through the logo, primary navigation, search controls, user menu, and other persistent interface elements before finally reaching the main content.
For keyboard users, this quickly becomes frustrating.
A skip navigation link (often called a skip link) provides a shortcut that allows users to bypass repetitive content and jump directly to the primary content area of the page.
The pattern is surprisingly simple:
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>
<!-- Site header -->
</header>
<nav>
<!-- Navigation links -->
</nav>
<main id="main-content">
<!-- Page content -->
</main>The link is typically placed as the first focusable element in the document. While it can remain visually hidden during normal browsing, it should become visible when it receives keyboard focus:
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
z-index: 9999;
}When a keyboard user presses Tab immediately after the page loads, the skip link appears and allows them to jump directly to the main content area without navigating through every menu item first.
Combined with semantic landmarks such as <header>, <nav>, and <main>, skip links significantly improve navigation efficiency for keyboard and screen reader users with very little implementation effort.
Heading Hierarchy and Content Flow
Headings (<h1> through <h6>) act as the dynamic table of contents for a screen reader.
In practice, most teams benefit from having a single primary <h1> that describes the page, followed by a logical hierarchy of <h2> and <h3> elements. Skipping levels (e.g. jumping from <h2> to <h5>) breaks the logical document outline and disorients keyboard-reliant users.
A common senior anti-pattern is using heading tags purely for visual font sizing. If you need a small subtitle, do not arbitrarily use an <h4> directly after an <h1>. In a similar fashion, don't use a <p> or <div> styled as a <h2> just to represent a heading element.
<main role="main" aria-labelledby="main-title">
<h1 id="main-title">Main title</h1>
<section aria-labelledby="section-title-1">
<h2 id="section-title-1">Section Title</h2>
<h3>Section Subtitle</h3>
<!-- Content -->
</section>
<section aria-labelledby="section-title-2">
<h2 id="section-title-2">Section Title</h2>
<h3>Section Subtitle</h3>
<!-- Content -->
<h3>Section Subtitle</h3>
<!-- Content -->
</section>
</main>Grouping Media with Figures and Figcaptions
When including structural illustrations, diagrams, code snippets, or photos that require an explanatory caption, do not just pair an <img> tag with a standard paragraph. Wrap them in a <figure> element and use a <figcaption>.
This explicitly registers the relationship in the browser's accessibility tree, letting assistive tools know that the text caption directly describes the accompanying visual asset.
<figure>
<img
src="https://example.com/charts/quarterly-growth.svg"
alt="Line chart showing a steady 15% increase in user acquisition from Q1 to Q4."
/>
<figcaption>
Figure 1: Active user acquisition growth metrics throughout the 2026 fiscal year.
</figcaption>
</figure>Designing Accessible Forms and Interactive Elements
Semantic Forms and Explicit Labels
Forms are the most high-friction components on the web, something we both love and hate (ehm mostly hate ehm).
To keep them accessible, every input field must have a programmatically linked <label>. Relying on visual proximity is not enough:
<label for="user-email">Email Address</label>
<input id="user-email" type="email" />Ensure you link labels explicitly using the for attribute (or htmlFor in React) mapped directly to the input’s id. This expands the clickable target area for users with motor control difficulties and ensures screen readers announce the exact input requirements when the field receives focus.
Historically, developers have used two valid approaches:
Explicit association using
forandidImplicit association by nesting the input inside the label
While both approaches are valid, explicit associations are generally preferred because they scale better in complex component hierarchies and make relationships easier to reason about during development.
<label>
Email Address
<input id="user-email" type="email" />
</label>When adding instructions for your input fields, make sure to link it correctly to the input. You can provide additional instructions and bind multiple ids inside an aria-labelledby. This allows for more flexible design.
<fieldset>
<legend>Using aria-labelledby</legend>
<label id="date-label" for="date">Current Date: </label>
<input
type="date"
name="date"
id="date"
aria-labelledby="date-label date-instructions"
/>
<p id="date-instructions">MM/DD/YYYY</p>
</fieldset>Alternatively, you can attach the instructions to the input with aria-describedby:
<fieldset>
<legend>Using aria-describedby</legend>
<label id="dob" for="dob">Date of Birth: </label>
<input type="date" name="dob" id="dob" aria-describedby="dob-instructions" />
<p id="dob-instructions">MM/DD/YYYY</p>
</fieldset>The Danger of Relying on Placeholders
This is subtle but important: placeholder attributes are not a substitute for <label> tags.
They present severe accessibility hazards, as they typically violate default color contrast requirements, usually disappear the moment a user starts typing (taxing short-term memory), and many screen readers ignore them entirely.
As rule of thumb, use placeholders strictly for formatting hints (e.g., DD/MM/YYYY), never for critical context or instructions.
Buttons vs. Links: Know the Difference
Mixing up buttons and links is one of the most widespread interactive defects on modern web apps.
The distinction is simple:
Links (
<a>): Navigates the user to a new page or a different anchor point on the current page. They are triggered by theEnterkey.Buttons (
<button>): Triggers an action, mutates data, alters state, or opens a modal on the current page. They are triggered by both theEnterkey and theSpacebar.
Using an <a> tag with an onClick handler that executes JavaScript state changes breaks native keyboard behavior and leaves keyboard users trapped.
Focus Visibility and Keyboard Navigation
Many users navigate applications entirely with a keyboard, using keys like Tab, Shift + Tab, Enter, and Space to move between and interact with controls.
For these users, the focus indicator acts as a cursor. It communicates which element is currently active and where the next interaction will occur. Without a visible focus state, navigating an application becomes little more than guesswork.
Unfortunately, one of the most common accessibility regressions in modern applications is removing the browser's default focus outline purely for aesthetic reasons:
button:focus {
outline: none;
}While this may create a cleaner visual appearance, it effectively removes the user's ability to track their position within the interface.
If you choose to customize focus styles, always replace the default indicator with an equally visible alternative:
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}The :focus-visible pseudo-class is particularly useful because it only displays the focus indicator when the browser determines it is needed, such as during keyboard navigation. Mouse users won't see unnecessary outlines after clicking on elements, while keyboard users retain a clear visual indicator.
Focus styling should also satisfy the same principles as the rest of your design system:
Maintain sufficient color contrast against the surrounding interface.
Ensure the focus indicator is clearly visible at different zoom levels.
Avoid relying solely on subtle color changes that may be difficult for users with low vision to detect.
A simple way to identify focus-related issues is to disconnect your mouse and navigate the entire application using only the keyboard. If you ever lose track of where you are, your users probably will too.
Visible focus indicators are not merely a compliance requirement; they are a fundamental usability feature for anyone who relies on keyboard navigation.
Safely Hiding Content
Sometimes you need to hide elements visually while keeping them available to screen readers, or vice-versa.
To hide from everyone: Use
display: noneor thehiddenattribute. This removes the element from both the visual screen and the accessibility tree.To hide visually but keep for screen readers: Use a utility class like
.visually-hiddenor.sr-only. This keeps off-screen descriptive text (like an extra instruction or context tag) fully readable by assistive tools.
/* Standard utility to hide content safely */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}Optimizing Media, Visuals, and Copy
Text Alternatives (Alt Text) for Functional Images
Every image needs an alt attribute, but how you fill it depends on the context:
Informative Images: The
alttext should describe the information conveyed by the image, not a literal description of the graphic itself.Decorative Images: If an image is purely atmospheric or redundant to nearby text, provide an empty
alt=""attribute. This signals to screen readers to silently skip past it.Functional Images: If an image acts as the entire content of an interactive control (like a gear icon inside a settings button), the
alttext must state the action of the control (e.g.,alt="Open settings"), not describe the graphic (alt="Gear icon").
Example of a functional image:
<form role="search">
<label for="search" class="hidden-visually">Search: </label>
<input type="text" name="search" id="search" v-model="search" />
<!-- functional image -->
<input
type="image"
class="btnImg"
src="https://img.icons8.com/search"
alt="Search"
/>
</form>Accessible Icon Management
Icons (often rendered via inline SVGs or icon fonts) should be treated carefully.
If an icon is purely visual and sits next to a text label, hide it from screen readers entirely by using aria-hidden="true".
If the icon stands completely alone and functions as an interactive button, ensure you give it a clear text fallback via an aria-label or an underlying .sr-only span.
<button type="button">
<svg aria-hidden="true" ...>
...
</svg>
Save Changes
</button>
<button type="button" aria-label="Close settings">
<svg ...>
...
</svg>
</button>
<button type="button">
<svg aria-hidden="true" ...>
...
</svg>
<span class="sr-only">Close settings</span>
</button>Colors, Fonts, and Contrast
Visual design heavily dictates accessibility.
According to WCAG AA guidelines, standard text must maintain a contrast ratio of at least 4.5:1 against its background (and 3:1 for large text).
While ensuring a mathematical contrast ratio isn't very practical, what we need to ensure is that all readable text on our websites have sufficient color contrast to remain maximally readable by users with low vision.
Beyond contrast, never rely on color as the sole mechanism for conveying information. If an input field changes to red to signal an error, you must also provide a textual indicator or an icon to ensure colorblind users can immediately identify the issue. For typography, always use relative font sizing (rem or em) rather than fixed pixels (px) so that interfaces scale gracefully when users adjust their default browser zoom text sizes.
Practical Hints for React and Vue
1. Beware the Wrapper Div
The biggest accessibility trap in component-based architectures is what in Internet people started calling "div soup":

Historically, framework used to enforce us writing components inside a single root element, causing developers to instinctively wrap everything inside a <div>. When screen readers parse a page where every structural element is a <div>, the page loses its navigational meaning, and scores low on tools like Google PageSpeed and Lighthouse.
React: Embrace Fragments
React is one of the frameworks that particularly suffered the most this issue.
Consider the following example:
import { Fragment } from 'react';
function ListItem({ item }: ListItemProps) {
return (
<Fragment>
<li>{item.label}: {item.value}</li>
</Fragment>
);
}
function List(props: ListProps) {
return (
<ul>
{props.items.map(item => (
<ListItem item={item} key={item.id} />
))}
</ul>
);
}I've seen too many times using <div> instead of <Fragment> in use cases like this, resulting in a broken HTML hierarchy or, if we may name it, a “div soup”.
Whenever you need a wrapper purely for React's rendering engine, use a Fragment (abbreviated with <>...</>).
Vue: Leverage Multi-Root Components
Vue 3 natively supports Fragments. You no longer need to wrap your component's template in a single root <div>. Let the semantic tags breathe.
<template>
<button>Save</button>
<button>Cancel</button>
</template>If you are building a navigation menu, use <nav>. If you are building an article card, use <article>. Don't let your component architecture dictate your HTML semantics.
2. Managing Focus
Single Page Applications (SPAs) are notorious for breaking standard keyboard navigation.
In a traditional site, navigating loads a new page and resets focus to the top. In React and Vue, the DOM updates dynamically. If a user closes a modal, the focus might be left dangling on a button that no longer exists, dropping the keyboard user back to the top of the document.
When a modal opens, you must programmatically shift focus inside it. When it closes, focus should return to the trigger element.
Skip links and proper focus management solve a similar problem: ensuring keyboard users always know where they are and can quickly reach where they need to go.
React: useRef and useEffect
import { useRef, useEffect } from 'react';
export default function Modal({ isOpen, onClose }: ModalProps) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
modalRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" tabIndex="-1" ref={modalRef}>
<h2>Settings</h2>
<button onClick={onClose}>Close Settings</button>
</div>
);
}Vue: Template Refs and watch
<script setup lang="typescript">
import { ref, watch, nextTick } from 'vue';
const props = defineProps({
isOpen: Boolean
});
const emit = defineEmits(['close']);
const modalRef = ref(null);
watch(() => props.isOpen, async (newVal) => {
if (newVal) {
await nextTick();
modalRef.value?.focus();
}
});
</script>
<template>
<div
v-if="isOpen"
role="dialog"
aria-modal="true"
tabindex="-1"
ref="modalRef"
>
<h2>Settings</h2>
<button @click="emit('close')">Close Settings</button>
</div>
</template>3. Dynamic ARIA Attributes: Tying State to Semantics
ARIA (Accessible Rich Internet Applications) attributes bridge the gap between complex dynamic UIs and assistive technologies. The beauty of reactive frameworks is that keeping ARIA attributes in sync is effortless because they just map to your component's state.
For example, a screen reader needs to know if a custom accordion is currently expanded or collapsed.
React: Mapping State
import { useState, useId } from 'react';
export default function Accordion({ title, children }) {
const [isExpanded, setIsExpanded] = useState(false);
const contentId = useId();
return (
<>
<button
aria-expanded={isExpanded}
aria-controls={contentId}
onClick={() => setIsExpanded(!isExpanded)}
>
{title}
</button>
<div id="accordion-content" hidden={!isExpanded}>
{children}
</div>
</>
);
}Vue: Data Binding
<script setup>
import { ref } from 'vue';
defineProps({ title: String });
const isExpanded = ref(false);
</script>
<template>
<button
:aria-expanded="isExpanded"
aria-controls="accordion-content"
@click="isExpanded = !isExpanded"
>
{{ title }}
</button>
<div id="accordion-content" :hidden="!isExpanded">
<slot></slot>
</div>
</template>By mapping aria-expanded directly to your reactive variables (isExpanded), the UI and the accessibility tree will never fall out of sync.
Testing and Automating Our A11y Checks
In 2026, we're lucky to have plenty of tools and automation to do the heavy lifting for us in our CI/CD pipeline and our IDE. And this is key: we shouldn't rely solely on human memory to catch missing alt tags or invalid ARIA roles.
ESLint Plugins: Install eslint-plugin-jsx-a11y for React, or eslint-plugin-vuejs-accessibility for Vue. These check your templates in real-time and will throw errors if you put an
@clickhandler on a<div>without giving it a keyboard listener and arole.Axe DevTools: This browser extension is fantastic for running audits on your rendered application to catch contrast issues and DOM structural problems that static analysis misses.
Regarding testing, there are a number tools out there like the Axe Platform, Google Lighthouse and browser extensions like WAVE and ARC Toolkit.
Before shipping, always remember (or automate):
Navigate the entire application using only a keyboard (
Tab,Shift + Tab,Enter,andSpace keys)Zoom the page to 200%.
Verify focus indicators remain visible.
Test with a screen reader such as NVDA (Windows) or VoiceOver (macOS).
Accessibility As Product Requirement
From my experience and in my opinion, accessibility should not be treated purely as a final QA checklist.
The most successful teams actually incorporate accessibility during design, development, code review, and testing rather than attempting to retrofit it before release.
A semantic button, a visible focus state, or a properly associated form label often costs nothing when implemented from the beginning, yet becomes significantly more expensive once a feature is already in production.
In short, accessibility should not bee seen as satisfying a compliance checklist but about ensuring every user can successfully complete the tasks your application was built to support.