Accessibility for engineers
Many accessibility issues arise during implementation, but the good news is that most disappear when we use the web platform as intended.
Your default mindset should be:
Start with semantic HTML. Add ARIA only when necessary. Never remove built-in accessibility.
Small decisions in markup have a huge impact: keyboard navigation, screen reader output, SEO, machine readability and maintainability all depend on correct semantics.
Table of Contents
1.Semantic HTML First
Native HTML elements come with accessibility built in: roles, keyboard behavior, focus handling, and meaningful semantics. Using them correctly is the easiest and most reliable way to build accessible interfaces.
Use the right element for the job
<button>for actions<a>for navigation<form>and real form controls (<input>,<select>,<textarea>) for data entry<header>,<main>,<nav>,<section>,<article>,<footer>for structure<ul>/<ol>/<li>for lists
This immediately gives you:
correct semantics for screen readers
predictable keyboard behavior (Enter, Space, Tab, Escape)
proper focus management without extra JS
better SEO and machine readability
Don't replace native controls with divs
Avoid patterns like:
They're not focusable, not operable by keyboard and not announced properly by screen readers.
If you must build a custom component, you'll need to manually add:
correct
roletabIndex="0"full keyboard support (
Enter,Space, arrow keys when needed)proper focus states
ARIA attributes where appropriate
This is significantly more work and easier to get wrong.
Keep your HTML predictable
Respect the natural heading hierarchy (
h1 → h2 → h3…).Avoid skipping levels (don't jump from
h1toh4).Use one
<main>landmark per page.Wrap related controls in a
<fieldset>with a<legend>when appropriate.
Good structure helps not only assistive tech but also users scanning the page visually.
Bonus: Accessible = Machine-readable
Accessibility isn't just about people; it's about making content understandable to machines too.
Clear semantic HTML is easier for:
search engines (better SEO)
voice assistants (Siri, Alexa, Google Assistant)
browser accessibility APIs
AI-powered tools and summarizers
automated testing and maintenance
Good markup benefits every layer of the ecosystem, not just screen readers. When you build accessible interfaces, you're also building machine-readable, maintainable code that works better for everyone.
2.Labels & Forms
Forms must be identifiable, operable, and understandable, both visually and with assistive technologies. Good markup solves most accessibility issues automatically.
Forms must be real <form> elements
<form> elementsA search bar is still a form and it should use <form> and a submit button.
This improves semantics, keyboard support, and allows assistive tools to trigger the action correctly.
Always provide a real label
Every form control needs an associated
<label>that must be programmatically associated with the field:
Labels should be visible and placed consistently (top or left) when possible. If the UI doesn't allow labels (e.g., search bars or very short forms), use a visually hidden label:
Note:
.sr-only(screen-reader-only) is a utility class that visually hides content while keeping it accessible to assistive technologies. Most CSS frameworks include this class. If you need to implement it yourself:
Hidden labels are acceptable only in very short forms. For login, registration, or any multi-field flow, visible labels are required.
Never rely on placeholders as labels. Placeholders disappear on typing, offer poor contrast, and are not consistently announced by screen readers. Use them only for hints or examples (e.g., "[email protected]"), never as the main label.
Required fields
Mark required fields using the native
requiredattribute; this automatically exposes the information to assistive tech.When necessary, reinforce with
aria-required="true"(e.g., in custom components).Provide a clear visual indicator like "(required)" or "Required". Avoid using a lone asterisk
*without context. Many users don't understand what it means.
Autocomplete
Autocomplete helps users with cognitive disabilities, dyslexia, ADHD, or memory difficulties by reducing the amount of information they must recall and type.
Always add
autocompletewhen the field has a known purpose.Use specific values such as
email,name,address-line1,tel,current-password,new-password, etc.This improves speed, accuracy, and reduces form abandonment.
Disabled fields
Disabled native inputs are skipped by keyboard navigation and screen readers.
Avoid disabling fields without explanation; many users won't know why they can't interact.
When a field is intentionally unavailable, provide context:
For custom components, you have two options:
Match native behavior: remove from tab order (
tabindex="-1") and mark as disabled (aria-disabled="true") so it behaves like a disabled native input.Keep it discoverable: use
aria-disabled="true"when users need to understand why it's unavailable (especially with explanatory text viaaria-describedby).
Group related fields
For radios, checkboxes or grouped selections, use <fieldset> and <legend>:
Helpful hints & instructions
Provide short, actionable instructions near the field ("Must be 8–20 characters").
Use
aria-describedbyfor hints that should be announced:
Accessible errors
Errors should describe the actual problem ("Password must be at least 8 characters", not "Invalid input") and be announced to screen readers.
They should appear near the field, not at the bottom of the form, and be visually distinct with both color + text (don't rely on red alone).
Use
aria-invalidandrole="alert"when errors are present, and associate the message viaaria-describedby.
Success messages
Users should be informed, not only visually, when an action succeeded.
For non-critical updates (e.g., "Saved"), use
role="status", which announces the message politely without interrupting screen reader flow.For important confirmations (e.g., "Payment complete"), use
role="alert"to announce it immediately.
These roles ensure the message is spoken automatically without requiring the user to focus it.
Predictable keyboard flow
Users must be able to complete the form with Tab, Shift+Tab, and Enter.
Maintain logical field order in the DOM. Do not rearrange form fields visually via CSS only.
Avoid trapping focus inside custom widgets unless necessary (e.g., date pickers), and provide a clear escape path.
Provide focus styles
Don't remove focus outlines: they're essential for keyboard and low-vision users.
If you customize focus styles, ensure they're clearly visible (sufficient contrast, adequate size).
Prefer
:focus-visibleto style only real keyboard focus, leaving mouse focus clean:
3.Keyboard Navigation
Keyboard accessibility is essential for users who cannot use a mouse (motor disabilities, repetitive strain injuries, temporary injuries) and for power-users who simply prefer keyboard interaction. If a UI can't be operated with a keyboard, it is not accessible.
Ensure all interactive elements are reachable
Links, buttons, inputs, and controls must be focusable with Tab.
Use semantic elements first:
<button>,<a>,<input>,<select>,<textarea>.If you create a custom interactive component (e.g.,
<div>acting as a button), you must add:tabindex="0"Keyboard handlers for Enter and Space
A visible focus state
But: prefer a real <button> whenever possible.
Logical tab order
The tab order must follow the visual reading order.
Avoid large jumps caused by:
Absolutely positioned elements
Portalled modals without proper focus handling
Moving elements into a new DOM position on focus or hover
Hidden elements (
display:none,visibility:hidden) should not be focusable.
Visible focus styles
Users must always see where they are on the screen.
Never remove the outline without providing an accessible replacement.
Prefer
:focus-visiblefor modern browsers:
This avoids showing focus on mouse click, but keeps it for keyboard users.
Focus behavior in complex UI
Some components require explicit focus management:
Modals:
Trap focus inside the modal.
Return focus to the trigger when the modal closes.
Menus and dropdowns:
Move focus to the first menu item when opened.
Close on Escape.
Tabs:
Arrow keys should navigate between tabs.
Tab should move into/out of the tab panel content.
A minimal focus trap example:
For more complex scenarios, consider using libraries like focus-trap or focus-trap-react for React applications.
Managing focus responsibly
Don't move elements into a different DOM position on focus: it breaks tab order.
If you manually call
.focus(), make sure it's predictable and not surprising.Returning focus to the trigger element after closing overlays improves usability.
Keep focus out of hidden or collapsed content (
display:noneelements should not be focusable).
Don't hijack keyboard behavior
Don't override arrow keys unless you're building a component that traditionally uses them (menus, sliders, carousels).
Don't trap the user inside components unintentionally (e.g., in carousels or chat windows).
Avoid global
keydownlisteners that swallow Escape or Tab.
Quick test
A 10-second test to catch most issues:
Put your mouse aside.
Press Tab.
Can you see where you are? (focus indicator)
Try to reach all interactive elements.
Try to operate the entire flow: open menus, submit forms, close dialogs.
If something can't be done with the keyboard, it's a red flag.
4.Images & Alt Text
Images need meaningful text alternatives so assistive technologies can convey their purpose or content. The goal is not to describe the pixels, but to communicate the intent.
When an image conveys information
Provide a short, specific description that reflects what the user needs to understand.
Avoid vague alt text such as "image", "photo", or the filename.
Keep it concise; screen readers read it inline with the rest of the content.
When an image is decorative
If an image adds visual flavour but no essential meaning, use an empty alt attribute so screen readers skip it.
Never leave out the
altattribute completely;<img>without alt is announced as "unlabeled graphic".
Icons inside buttons or links
If the text already communicates the action, the icon is decorative.
If the icon is the only content, give it a meaningful label:
Complex images & visual content (charts, diagrams, prototypes)
Some visuals contain more than a simple picture: they convey data, relationships or meaning.
Avoid placing essential explanations only in tooltips, background images, or hover states, as they are not consistently accessible. Use a short alt (if applicable) plus a longer explanation, ideally using <figure> and <figcaption>.
alt→ concise summaryfigcaption→ extended description, context or insightsDo not repeat the same text in both.
If the image needs no extended explanation, use only
alt.If the caption already fully describes the visual, provide an empty alt so screen readers don't read everything twice.
Using <figure> with images
Screen readers treat the figcaption as the semantic description of the figure, but only the non-duplicate parts should appear there, not a copy of the alt.
<figure> is not limited to <img>
You can use it with any standalone visual: <canvas>, <svg>, <video>, or a rendered component.
In these cases, there is no alt attribute, so the description must be provided entirely in the caption.
aria-hidden="true"ensures the canvas (which exposes no semantic info) is skipped by assistive tech.The caption becomes the only accessible description, which is what users need.
If the design doesn't require a visible caption (e.g., the insight is already in the UI), hide it visually using the
.sr-onlyutility class (see Labels & Forms for implementation details). This keeps the content accessible while preserving the intended layout.
Background images
Background images (CSS) cannot have alt. Ensure the essential content is in HTML, not CSS.
Do not encode text inside images
Text embedded inside an image is invisible to screen readers, translators, and search engines.
Prefer real HTML text on top of a background.
If unavoidable, duplicate the text as
altor nearby content.
Emojis and accessibility
Screen readers read emoji names, which can be awkward or confusing. If an emoji conveys important meaning, use aria-label to control what gets announced:
For decorative emojis, you can use aria-hidden="true" to hide them from screen readers, similar to decorative images.
5.ARIA
ARIA (Accessible Rich Internet Applications) provides attributes that help assistive technologies understand the structure, state and behavior of custom UI components. It is not a replacement for semantic HTML; it does not fix inaccessible markup, and it does not add keyboard behavior automatically. Moreover, it often adds complexity and can break accessibility if misused. Use it sparingly, and only when native elements cannot express the needed behavior.
General rule
"Use native HTML whenever possible. If you can use a
<button>,<a>,<label>,<fieldset>,<dialog>, don't recreate them withdivs and ARIA."
Appropriate ARIA use
Use ARIA only to add missing semantics to custom components:
role="dialog"for custom modalsaria-expandedfor disclosure widgetsaria-controlsto indicate what element a control affectsrole="alert"orrole="status"to announce dynamic messagesaria-liveregions for dynamic contentaria-current="page"for navigationaria-selectedfor tabs, listboxes, custom selects
When not to use ARIA
Avoid adding ARIA roles that duplicate or override native behaviors:
Do not use
role="button"on a<button>Do not use
role="link"on an<a>Do not use
role="heading"instead of using<h1>…<h6>Do not add interactive roles to
divorspanelements unless you fully implement keyboard interaction, focus handling and states.
ARIA attributes alone do not provide keyboard support or focus behavior. If you add role="button" to a div, you also need to handle:
tabindex="0"onKeyDownfor Space and Enterfocus styling
preventing unexpected behavior
Which is why native elements are almost always better.
Attributes that require caution
Use these only when you have a specific reason:
aria-hidden="true"hides content from assistive tech. Only use when elements are truly decorative or duplicated visually.aria-labelcan provide an accessible name for icon-only buttons, but prefer visible text whenever possible.aria-labelledbyis often better thanaria-labelbecause it references existing visible text.
ARIA is not a substitute for correct structure
Avoid using ARIA to compensate for:
missing labels
incorrect heading hierarchy
broken tab order
inaccessible custom components
poor contrast or unreadable text
missing focus management
images without alt text
All of these should be fixed with HTML, CSS and proper component design.
Quick mental model
First: Use the correct native element
Then: Fix semantics with structure (labels, headings, lists)
Finally: Add ARIA only where necessary to fill a real gap
For more detailed guidance, refer to WAI-ARIA (W3C Web Accessibility Initiative): https://www.w3.org/WAI/standards-guidelines/aria/
6.Custom UI Components
Custom UI components need to offer the same accessibility guarantees as their native HTML equivalents.
Buttons, links, checkboxes, dialogs and selects already come with built-in semantics, keyboard behavior, and assistive-technology support. When we replace them with div-based widgets, we must recreate all of that manually.
Whenever possible, prefer native elements or accessibility-focused headless libraries like React Aria, Radix UI or Headless UI. When selecting any UI library, make sure its components have documented accessibility patterns and ARIA support.
Use a custom component only when the native option truly doesn't meet the product requirements.
What every custom component must support
If a component is custom, it must:
Be reachable via keyboard (
Tab,Shift + Tab).Support expected interaction keys for its pattern (e.g.
Enter/Spaceto activate, arrows to navigate lists,Escto close).Expose a clear accessible name (using text,
aria-label, oraria-labelledby).Announce its role and state (
aria-expanded,aria-selected,aria-checked, etc.).Manage focus consistently (no unexpected jumps, no "lost" focus).
Provide visible focus styles.
Respect user settings such as reduced motion and high-contrast modes.
Work reliably with assistive technologies.
If any of these are missing, the component isn't complete.
Follow established patterns
For complex widgets (selects, tabs, dialogs, disclosures, listboxes, comboboxes), use the official patterns documented in the WAI-ARIA Authoring Practices
These patterns define how the component should behave (roles, expected keyboard interactions, states, relationships). They're not optional: assistive technologies rely on them.
Common mistakes
Using a
divas a button without adding semantics or keyboard support.Creating dropdowns that don't announce whether they're open or that can't be opened with the keyboard.
Closing dropdowns on click but not on Escape.
Building "fake inputs" that don't expose a real value to assistive tech or autocomplete.
Tabs without
role="tablist"and correct arrow-key navigation.Modals that don't trap focus or don't return focus when closed.
Carousels or sliders that auto-rotate without user control or reduced-motion support.
Example (simplified)
Incorrect
Issues: no semantics, no states, no keyboard behavior, no accessible name.
Correct (simplified structure + minimal keyboard behavior)
This is still simplified, but shows the minimum expected behavior: keyboard support, state management, and predictable focus handling.
7.Reduced Motion
Some users experience motion sensitivity, vertigo or cognitive overload when exposed to large, fast or unexpected animations. CSS gives us tools to respect user preferences automatically, and we should use them whenever we animate UI elements.
Respect prefers-reduced-motion
prefers-reduced-motionAlways wrap non-essential animations in a media query that checks for reduced-motion preferences:
This should not remove all transitions everywhere by default, but it shows the pattern. Apply it selectively to components with significant movement.
Avoid motion-heavy patterns
Big parallax effects
Auto-scrolling or scroll-jacking
Continuous background animations
Large zooms, bounces or slides
Animations that move content unexpectedly
Prefer small opacity or color transitions that feel stable.
Provide stable alternatives
If your component uses motion to communicate state, ensure the same information is available without animation:
A menu should not rely solely on slide-in movement to indicate it opened.
A snackbar should not require motion to be noticed. Also use
role="status"or clear styling.A tooltip should not animate from far away; keep movement minimal.
Animation libraries
If you use animation libraries (Motion (formerly Framer Motion), GSAP, React Spring):
Check if they provide reduced-motion helpers
Prefer opacity/fade transitions over positional movement
Avoid timeline-driven continuous loops unless essential
Motion example:
When motion is essential
If motion is part of the interaction (carousel, slider, drag & drop):
Make movement short, predictable and slow
Allow pause/stop if it auto-advances
Provide visible focus outlines for keyboard users
Respecting reduced-motion settings is not only an accessibility requirement, it also makes interfaces feel calmer, more stable and more professional.
8.Basic Accessibility Testing
Accessibility doesn't require a full audit to catch the biggest issues. A few quick checks during development can prevent most blockers before they ship.
Keyboard testing (the fastest and most important check)
Try navigating your page with only:
Tab → move forward
Shift+Tab → move backward
Enter / Space → activate
Esc → close modals or menus
Arrow keys → navigate lists, tabs, menus if applicable
If you get stuck, lose focus, or can't activate something, it's inaccessible.
Screen reader smoke test
You don't need to be an expert. Just test the basics:
Turn on VoiceOver (macOS: Cmd+F5) or NVDA (Windows).
Navigate headings (VoiceOver: VO+Cmd+H, NVDA: H).
Navigate links (VoiceOver: VO+Cmd+L, NVDA: K).
Navigate form controls (VoiceOver: VO+Cmd+J, NVDA: F).
Open your UI menus and dialogs.
Check that elements:
Have a meaningful accessible name
Are announced with the correct role
Have states ("expanded", "selected", etc.)
A short 3-minute test can reveal missing labels, broken structure or incorrect roles.
Built-in browser tools
Use Chrome DevTools → Accessibility pane:
Check the Accessibility Tree (is the element exposed correctly?)
Look for missing labels or incorrect roles
Verify contrast directly in DevTools
Inspect focus order with "Tab" focus highlighting
Automated tools (first pass)
Automated tools won't catch everything, but they're excellent for fast feedback:
Lighthouse Accessibility (Chrome DevTools → Lighthouse tab → Accessibility audit)
eslint-plugin-jsx-a11y (during development)
Run these early; treat errors as code smells.
Test with reduced motion & zoom
Enable Reduce Motion in OS settings
Ensure the UI still works and doesn't flicker or jump
Zoom to 200% in the browser
Check that layout still holds and nothing becomes unreachable
When working on components
Test your component in isolation:
Can I reach it with keyboard?
Does focus go where I expect when it opens/closes?
Does it expose the right role and state?
Does it still work with reduced motion?
Does it behave consistently across devices?
These checks take seconds and prevent major downstream issues.
When QA time is limited
At minimum, test:
Keyboard navigation
Labels on forms
Focus visibility
Alt text on images
Color contrast
Modals opening/closing correctly
Error messages being announced
Small habits → huge accessibility wins for the whole product.
9.Engineering Accessibility Checklist
A fast, practical checklist to review implementations before shipping.
Use semantic HTML first (
button,nav,header,form,fieldset).Every input has a label (visible or visually hidden in short forms).
Correct form semantics: required fields, autocomplete, error messages.
Keyboard navigation works everywhere (Tab, Enter, Space, Esc).
No
outline: none, focus is always visible.Focus management: modals trap focus, restore on close.
Meaningful alt text; decorative images use
alt="".ARIA is used only when needed and according to authoring practices.
Custom components support roles, states, keyboard, and focus.
Respect prefers-reduced-motion for animations and transitions.
Run automated checks (Lighthouse, eslint-plugin-jsx-a11y).
Test with a screen reader (basic navigation).
Test at 200% zoom to ensure layout resilience.
Before shipping
Can I navigate the UI without a mouse?
Can a screen reader user understand structure and purpose?
Are errors and confirmations announced properly?
Are forms usable and predictable?
Are animations optional and non-invasive?
Does the app behave correctly in high zoom or reduced motion mode?
If most answers are "yes", the UI is in good shape.
Last updated
