Accessibility
Web accessibility (also referred to as a11y) is the design and creation of websites that can be used by everyone. Accessibility support is necessary to allow assistive technology to interpret web pages.
React fully supports building accessible websites, often by using standard HTML techniques.
- Why is Web Accessibility important?Equal access for all.
- Better user experience
- Improves SEO.
- Good for Business.
- Legal Requirements – EAA
- A European Union directive – a legal framework adopted by the EU in 2019.
- It requires digital products and services, including e-commerce websites, to meet common accessibility standards across all EU Member States.
- Compliance became mandatory across all EU member states from 28th June 2025.
WCAG – Web Content Accessibility Guidelines
It is an internationally recognized set of guidelines developed by the World Wide Web Consortium (W3C) to make web content more accessible to people with disabilities.
- These guidelines exist to ensure websites and digital content are usable by everyone, including people with:
- Visual impairments (blindness, low vision, color blindness).
- Hearing impairments (deafness, hard of hearing).
- Mobility impairments (difficulty using a mouse, limited motor skills).
- Cognitive or learning disabilities.
- Provide a standardized framework so developers and organizations can build inclusive digital products.
- Help businesses meet legal and regulatory requirements in many countries.
WCAG Principles (POUR)
The guidelines are built around four principles:
- Perceivable: Content must be presented in ways users can perceive (e.g., text alternatives for images, captions for video).
- Operable: Users must be able to interact with content (e.g., full keyboard navigation, visible focus states).
- Understandable: Content and UI must be easy to understand (e.g., clear labels, predictable navigation).
- Robust: Content should be compatible with assistive technologies (e.g., screen readers).
WCAG Conformance Levels (A, AA, AAA)
There are three levels:
| Level | Meaning | Importance |
| A | Minimum requirements | Basic accessibility for many users |
| AA | Mid-level, industry standard | Addresses the biggest barriers for people with disabilities |
| AAA | Highest level | Often used by governments or for specialized accessibility needs |
WCAG 2.1 Level AA – What it means:
We should at least aim for WCAG 2.1 Level AA.
Level AA includes all Level A requirements plus additional rules for usability and inclusivity:
- Color contrast: Minimum 4.5:1 for normal text.
- Resizable text: Up to 200% without breaking layout.
- Visible focus indicators: Users can see where they are on the page.
- Headings & labels: Must be clear and descriptive.
- Keyboard operability: Every function should be usable without a mouse.
- Text alternatives: Provide alt text for images and other non-text content.
- Captions: Required for prerecorded multimedia content.
WCAG Checklist
The following WCAG checklists provide an overview:
WAI- ARIA: Web Accessibility Initiative – Accessible Rich Internet Applications
ARIA is a set of attributes defined by the W3C (World Wide Web Consortium) to make web applications and dynamic content more accessible to people who use assistive technologies like screen readers.
Why ARIA exists?
Since React apps often build custom UI components (modals, dropdowns, tabs, sliders) that don’t always map cleanly to native HTML semantics, ARIA ensures that these components are correctly understood by assistive tools.
Why ARIA matters in React?
- React frequently uses divs, spans, and custom components that may lack built-in semantics.
- Without ARIA, screen readers may not announce state changes (like “menu opened”).
- With ARIA, you can describe roles, states, and properties that match user expectations.
Note that all aria-* HTML attributes are fully supported in JSX. Whereas most DOM properties and attributes in React are camelCased, these attributes should be hyphen-cased (also known as kebab-case, lisp-case, etc.) as they are in plain HTML.
<input
type="text"
aria-label={labelText}
aria-required="true"
onChange={onchangeHandler}
value={inputValue}
name="name"
/>
ARIA best practices in React
- Number one rule of ARIA is don’t use ARIA.
- If there’s an HTML element that’ll get you the behavior you’re looking for, then use that HTML element.
- Prefer semantic HTML first → Use <button>, <nav>, <header>, etc. instead of div + ARIA role.
- Add ARIA only when needed – For custom components where semantics aren’t clear.
- Keep ARIA state in sync with React state – If a dropdown is open, aria-expanded must match.
- Test with screen readers (NVDA, VoiceOver) and accessibility tools (axe, Lighthouse).
Structural Elements: Headings & Lists
Structure is how assistive tech (and search engines) understand your page. Use semantic HTML first and keep the outline predictable. This section covers headings and lists in React, with code patterns, do/don’ts, and testing tips.
A. Headings
Goals
- Provide a clear outline of the page.
- Make sections easy to navigate via screen readers’ heading rotor and browser “jump to heading” shortcuts.
- Improve SEO and scannability.
Core rules
- Use real heading elements:
<h1>–<h6>. Avoid styling<div>s to look like headings. - One primary
<h1>per page/route (practical convention). Subsequent headings should not skip levels:h1→h2→h3… (don’t goh1→h3).
- Each section has a heading: For
<section>,<article>,<aside>,<nav>, provide a visible heading or label the region with aria-labelledby. - Keep headings short and descriptive. Don’t stuff them with keywords.
Example
export default function ProductPage() {
return (
<main>
<h1>Travel Mugs</h1>
<section aria-labelledby="filters-heading">
<h2 id="filters-heading">Filters</h2>
{/* … */}
</section>
<section aria-labelledby="results-heading">
<h2 id="results-heading">Results</h2>
{/* … */}
</section>
</main>
);
}
B. Lists
Goals
- Represent sets of related items semantically.
- Enable quick navigation for screen readers.
- Communicate order/quantity where relevant.
Choose right list type
- Unordered list:
<ul>when order doesn’t matter (e.g., feature bullets). - Ordered list:
<ol>when sequence matters (steps, rankings, timelines). - Description list:
<dl>for name–value pairs (terms and definitions, key–value properties).
Don’t
- For typical site nav, don’t use ARIA menu roles. A simple list inside
<nav>is best. role="menu"/menuitem"are for application-style menus with arrow-key behavior, overkill for standard website navigation.
- Don’t use lists solely for layout (use CSS/Grid)
- Don’t Replace
<ul>/<ol>with<div>s for groups of items. - Don’t abuse
role="list"/role="listitem"; prefer native elements.
Sectioning & Landmarks (works with headings)
- Use semantic containers to structure the page:
<main>,<header>,<footer>,<nav>,<section>,<article>,<aside>
- Label regions for navigation:
- Visible heading inside the region or aria-labelledby referencing a heading ID.
- Provide skip links to jump to
<main>content.
<a href="#main" className="sr-only focus:not-sr-only">Skip to main content</a>
<main id="main">…</main>
Links
Links (<a>) are one of the most important structural elements on the web. For React apps, ensuring links are semantic, accessible, and usable with both keyboard and assistive technologies is essential.
1) Always use real <a> elements
- Use
<a href="…">for navigation; not<div>or<span>with onClick. - React Router (and similar)
<Link>components render<a>under the hood; that’s correct. - Buttons are for actions, links are for navigation. Don’t confuse them.
// ✅ Correct
<a href="/about">About Us</a>
// ❌ Wrong (not accessible as a link)
navigate('/about')}>About Us</div>
2) Provide clear, descriptive link text
- Link text should describe the destination or action.
- Avoid vague phrases like “click here” or “read more”.
- If links repeat, add context with visually hidden text.
// ✅ Good
<a href="/pricing">View pricing plans</a>
// ❌ Bad
<a href="/pricing">Click here</a>
// For repeated links
<a href={`/articles/${id}`}>
Read more <span className="sr-only">about {title}</span>
</a>
2) Maintain a visible focus style
- Links must be focusable via Tab key and respond to Enter key activation.
a:focus-visible {
outline: 2px solid var(--focus-color);
outline-offset: 2px;
}
Buttons
- Prefer button tag instead of custom HTML elements as buttons.
- If you must use custom elements like
<div>or<span>as button, ensure they have an accessible name.
- A valid
role="button" - An accessible name via aria-label, aria-labelledby, or hidden .sr-only text
- Keyboard support
- Must be reachable with Tab.
- Must be clicked using Enter, Space keys
- Ensure visible focus styles.
- The button’s text should describe the action.
- For icon-only buttons, add aria-label or visually hidden text.
<button aria-label="Close dialog">
<CloseIcon aria-hidden="true" />
</button>
- Use aria-pressed, aria-expanded, etc. for toggle/expandable buttons.
<button
aria-expanded={open}
aria-controls="menu"
onClick={() => setOpen(!open)}
>
Menu
</button>
- Native
<button>already supports role, focus, keyboard. Add ARIA only when state/context isn’t obvious.
Form elements
- Always Associate Labels: Every HTML form control, such as
<input>and<textarea>, needs to be labeled accessibly.
<label htmlFor="email">Email</label>
<input id="email" type="email" />
- Group Related Inputs. Example: Wrap radio buttons or checkboxes in
<fieldset>+<legend>.
<fieldset>
<legend>Delivery Options</legend>
<label><input type="radio" name="delivery" value="standard" /> Standard</label>
<label><input type="radio" name="delivery" value="express" /> Express</label>
</fieldset>
- Provide Error & Help Text: Use aria-describedby to associate helper or error messages. Users should receive clear, immediate feedback when something goes wrong.
<label htmlFor="username">Username</label>
<input id="username" aria-describedby="username-help" />
<p id="username-help">Must be at least 6 characters.</p>
- All inputs must be keyboard accessible.
- Use clear focus styles.
- Ensure accessible names are provided.Users should always know the purpose of the input. Avoid relying on visual placement or icons alone.
Modals & dialogs
- Apply the correct role:
role="dialog"orrole="alertdialog".
This helps screen readers recognize the modal as a dialog and announce its contents appropriately. - Trap keyboard focus inside the modal while it’s open.
When the modal opens, focus should be placed inside the modal. Users should not be able to Tab out of the modal and into the page behind it. This preserves the context and prevents confusion. - Return focus to the trigger element when the modal closes.
This ensures a smooth navigation experience, particularly for keyboard and screen reader users. - Enable the modal to close via the Esc key.
Keyboard users expect modals to be dismissible without needing to tab through all content. - Prevent interaction with elements outside the modal.
Usearia-hidden="true"or the inert attribute on background content so it’s not announced or focusable while the modal is active.
Carousels (Slide Show)
- When the user tabs into the carousel, this should be the tabbing order:
- left arrow (
<) navigation button - bullets
- right arrow (
>) navigation button. - active slide’s first interactive element, followed by all visible slides and then it focus should exit out of the carousel.
- left arrow (
- Other elements of the slides like heading, paragraph, image can be accessed via VO lock (Ctr + Opt + : , then arrow key to navigate on Mac Voiceover).
- Users should be able to change slides using left and right arrow keys on the keyboard.
- The reason carousel navigation controls should be first in tab order, so users can choose to navigate slides or skip.
- Screen reader announcement (example using ARIA live regions or offscreen text), when user tabs into the arrows:
“Slide: 1 of 4 - Title of the slide”. - As the user moves to the next slide, the announcement should mention the slide count. Example
“Slide: 2 of 4: Title”
- Screen reader announcement (example using ARIA live regions or offscreen text), when user tabs into the arrows:
- Keep Non-Visible Slides Inaccessible: Offscreen slides must have:
aria-hidden="true",tabindex="-1" - The left arrow should have
aria-label="Previous slide"and right arrow should havearia-label="Next slide". - Use an ARIA live region (outside carousel) to announce:
<div aria-live="polite" class="sr-only" role="status">
Slide 4 of 9. Offer for Abacos, Bahamas now visible.
</div>
- Use aria-roledescription and aria-label for each slide container. If there are multiple elements inside the slide, use role=”group” . It groups related elements together (e.g., heading, paragraph, image, link) . It helps screen reader users understand that this collection of content is one logical unit.
<div role="group" aria-roledescription="slide" aria-label="Offer for British Virgin Islands" tabindex="-1">
...Slide content....
</div>
- Do not force focus onto the role=”group” slide container unless there’s a real reason to do so.
- Do not move keyboard focus automatically to new slide content.
- Visual Focus Indicators: Ensure all interactive elements show clear visible focus.
Images
Accessible images make your UI usable for people who rely on assistive tech (screen readers, braille displays), and they improve SEO and performance. This guide focuses on what to show and how to code it in React.
Always choose the right alternative text
Based on the correct choice of alt text, images can be divided into four categories:
A. Informative images (convey meaning)
- Images (such as charts, photos, product previews) should describe the content or purpose. The alt attribute helps screen readers describe the image to non-visual users.
- Provide a concise alt that communicates the same info as the image.
<img src="/team/alex.jpg" alt="Alex Nguyen, Customer Success Manager" />
B. Functional images (links/buttons/icons that perform actions)
For functional images, the accessible name must describe the action or destination, not the picture.
<a href="/cart">
<img src="/icons/cart.svg" alt="View cart" />
</a>
<button aria-label="Close dialog">
<img src="/icons/close.svg" alt="" aria-hidden="true" />
</button>
C. Decorative images (purely visual / redundant)
Use an empty alt or role=”presentation” so screen readers skip it.
<img src="/decor/gradient.png" alt="" />
or
<img src="/decor/gradient.png" role="presentation" />
Inline SVG icons used as decoration
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">…</svg>
D. Complex images (charts, diagrams, infographics)
Provide a short alt + a longer description nearby. Use <figure> and <figcaption> or reference text with aria-describedby.
<figure>
<img src="/charts/sales-q4.png" alt="Q4 sales by region bar chart" aria-describedby="chart-desc" />
<figcaption id="chart-desc">
North America leads with $2.1M, followed by EMEA $1.6M, APAC $1.1M. YoY growth 12%.
</figcaption>
</figure>
Avoid images of text
- Use real text styled with CSS. If you must show text in an image (branding, charts), ensure the alt/figcaption conveys the same words and that color contrast is sufficient.
- Don’t rely on color alone to convey meaning in images (e.g., “errors are red”); add symbols or labels.
Testing accessible images
- Keyboard: Ensure image-based controls (buttons/links) are focusable and operable.
- Screen readers: Try VoiceOver/NVDA to confirm expected announcements.
- Automated checks: Run axe/Lighthouse; verify “Image elements have alt attributes” and no redundant/meaningless alts (e.g.,
alt="image"). - Visual checks: Confirm no layout shifts and that images don’t obscure focus outlines.
In-Page tabbed component – tabs pattern
There are two most common tab patterns:
- Tabs With Automatic Activation: A tabs widget where tabs are automatically activated and their panel is displayed when they receive focus.
- Tabs With Manual Activation: A tabs widget where users activate a tab and display its panel by pressing
Behaviour
- User should be able to navigate between tab controls
When to use automatic vs manual activation?
- Automatic: It is recommended that authors consider implementing automatic activation of tabs only in circumstances where panels can be displayed instantly.
- Manual: It is recommended to use manual activation when there is loading or large layout shifts on tab change. For example the tab is content heaving containing multiple images, videos, api calls etc.
Roles, states and properties
- Correct roles/relationships: tablist → tab ↔ tabpanel with aria-controls/aria-labelledby.
- State management: Only one
aria-selected="true"; others should be false; inactive panels hidden. - Keyboard support: Arrow keys + Home/End navigation; Enter/Space for activation (manual).
- Focus styles: Strong, visible, and logical focus order.
- Focusable panels:
tabindex="0"so keyboard users can easily enter content.
Tables
- Use semantic HTML – Always use
<table>,<thead>,<tbody>,<tr>,<th>, and<td>appropriately. Avoid creating tables visually using<div>or<span>alone. - Define headers properly.
- Use
<th>for headers. - When both row and column headers are present, use
scope="col"for column headers andscope="row"for row headers. - Use the
<caption>element to briefly describe the table’s purpose. This helps screen reader users understand the context. - Ensure logical and consistent reading order – The order of rows and cells in HTML should always match the visual order.
- Avoid layout tables – Layout tables are a legacy practice. Use CSS-based grid or flex layouts instead.
Color contrast
- Ensure sufficient color contrast for text and UI components
For normal text (under 18pt or 14pt bold), maintain a minimum contrast ratio of 4.5:1 against the background.
For large text (≥18pt regular or ≥14pt bold), the minimum is 3:1.
Example:
- Good: Dark gray #333 on white #fff, contrast ratio 15:1
- Bad: Light gray #999 on white, contrast ratio too low (2.5:1)
Tools and packages
- Development assistance:
- The eslint-plugin-jsx-a11y plugin for ESLint provides AST linting feedback regarding accessibility issues in your JSX
- Axe Dev Tools
- WAVE Evaluation Tool
- Pa11y CLI Tool
POUR principles ref
Perceivable
- Text Alternatives – Provide text alternatives for any non-text content so that it can be changed into other forms people need, such as large print, braille, speech, symbols, or simpler language.
- Time-based Media – Provide alternatives for time-based media.
- Adaptable – Create content that can be presented in different ways (for example simpler layout) without losing information or structure.
- Distinguishable – Make it easier for users to see and hear content, including separating foreground from background.
Operable
- Keyboard accessible – Make all functionality available from a keyboard.
- Enough time – Provide users enough time to read and use content.
- Seizures – Do not design content in a way that is known to cause seizures.
- Navigable – Provide ways to help users navigate, find content, and determine where they are.
Understandable
- Readable – Make text content readable and understandable.
- Predictable – Make Web pages appear and operate in predictable ways.
- Input assistance – Help users avoid and correct mistakes.
Robust
- Maximize compatibility with current and future user agents, including assistive technologies.
How to make accessible UI
Start with Radix/Shadcn components
WAI-ARIA compliance – Correct ARIA roles, states, and properties (aria-expanded, aria-controls, role="dialog", etc.) are managed for you.
Full keyboard navigation – All components are operable with a keyboard (e.g., arrow keys for menus, Tab for focus, Enter/Space for activation, Esc for dismissal).
Focus management – It intelligently handles focus, trapping it within modals and returning it to the trigger element when the modal is closed. This is a huge win for accessibility and is difficult to implement correctly from scratch.
Manage color and contrast
- WCAG contrast ratios:
- AA (Minimum) – Text should have a contrast ratio of at least 4.5:1 against its background.
- AA (Large Text) – Large text (18.66px bold or 24px regular) needs a ratio of at least 3:1.
- AAA (Enhanced) – For a higher level of accessibility, aim for 7:1 for normal text and 4.5:1 for large text.
- Tools:
- Browser DevTools – The inspector has a built-in contrast checker.
- Online checkers – Use tools like WebAIM Contrast Checker or Coolors.
- Figma plugins – Use plugins like “Stark” or “A11y – Color Contrast Checker” during the design phase.
Provide accessible labels
- Always use a
<label>element associated with its input. Shadcn’sLabelandInputcomponents make this easy. - An icon alone is meaningless to a screen reader.
- Provide a textual alternative.
- Include a
<span>with text and hide it visually. Shadcn uses this pattern. - You can also use an
aria-labeldirectly on the button.
Testing for accessibility
- Keyboard test – Can you use your entire website with only the
Tab,Shift+Tab,Enter,SpaceandEsckeys – This is the single most important test you can run. - Automated check – Use the Axe DevTools browser extension. It will scan your page and identify common violations like missing alt text, color contrast issues, and incorrect ARIA usage.
- Plugins – Plugins like jest-axe or vite-axe, which are custom Jest matchers for axe for testing accessibility, help to run automated tests at the unit level.
Do read our article on All about Web Accessibility, WCAG and making WordPress website more compliant.







