Eqii
Design Beginner 12 min read

Web Accessibility Fundamentals: Building for Everyone

Semantic HTML, ARIA done right, keyboard navigation, focus management, color contrast, screen reader testing, and prefers-reduced-motion. The accessibility habits that should be in every developer's default toolkit.

Why accessibility is not optional

The World Health Organization estimates that 1 in 6 people on earth — about 1.3 billion people — live with some form of disability. That covers visual impairments, hearing loss, motor limitations, cognitive differences, and temporary conditions like a broken arm or a screen in bright sunlight. If your website is not usable by these people, you are excluding a meaningful fraction of your potential audience, and you are almost certainly breaking the law.

The legal landscape has shifted decisively. The Americans with Disabilities Act (ADA) has been interpreted by U.S. courts to cover websites, and the Department of Justice published final rules in 2024 explicitly requiring state and local government web content to meet WCAG 2.1 Level AA. The European Accessibility Act takes effect in June 2025 and applies to a wide range of private-sector digital products sold in the EU. Accessibility lawsuits filed in U.S. federal court have grown from a few hundred per year in the early 2010s to over four thousand in 2023. The legal risk is no longer hypothetical.

Beyond ethics and law, accessibility overlaps heavily with good engineering. Semantic HTML is more robust, more maintainable, and easier to test. Keyboard-friendly interfaces work better on mobile and on screen readers. Sufficient color contrast improves readability for everyone. The work you do for accessibility almost always improves the quality of your product for the rest of your users.

If you take one thing from this guide, take this: accessibility is not a feature you add at the end. It is a property of the code you write every day, and the cheapest time to build it in is when you are writing the code the first time. Retrofitting accessibility onto a finished product is ten times the work and ten times the cost.

Semantic HTML: the foundation

The single most impactful thing you can do for accessibility is use the right HTML element for the right purpose. A `<button>` is keyboard-accessible, has a focus ring, and announces itself as button to screen readers. A `<div>` with an `onclick` handler is none of those things. A `<nav>` element tells assistive technology that this is a navigation region. A `<div>` with a class of `nav` tells it nothing.

Semantic HTML is the foundation because every assistive technology — every screen reader, every voice control system, every switch device — is built on the assumption that the underlying HTML uses the correct elements. JAWS, NVDA, VoiceOver, and TalkBack all expose semantics to their users. When you use a `<button>`, the user can ask their screen reader to list all buttons on the page and jump to the one they want. When you use a `<div>` styled to look like a button, that feature is broken.

Use the heading hierarchy correctly. A page should have exactly one `<h1>`, and the rest of the headings should descend in order: `<h2>` for major sections, `<h3>` for subsections, and so on. Screen reader users navigate by headings the way sighted users navigate by scanning — it is how they understand the structure of a page. Skipping heading levels (an `<h4>` after an `<h2>`) breaks that navigation.

Use HTML5 landmarks: `<header>`, `<nav>`, `<main>`, `<aside>`, `<footer>`, `<section>`, `<article>`. These give screen reader users a map of the page. A user can jump directly to `<main>` to skip your header and navigation, which is essential when they visit multiple pages on your site.

A short list of elements developers underuse: `<dialog>` for modals (it handles focus trapping and ESC for you in evergreen browsers), `<details>` and `<summary>` for collapsible sections (no JavaScript required), `<input type="search">` for search fields (announces correctly and may render a clear button), `<output>` for live calculation results, and `<figure>` with `<figcaption>` for images with captions. Every one of these replaces a JavaScript implementation that was probably less accessible.

ARIA: powerful but dangerous

ARIA (Accessible Rich Internet Applications) is a set of attributes that let you extend HTML semantics for things the native elements cannot express — custom dropdowns, tabs, modals, live regions that announce updates. The first rule of ARIA, written by the W3C, is: do not use ARIA if a native HTML element or attribute will do the job.

The reason is that ARIA does not add behavior. A `<div role="button">` is announced to screen readers as a button, but it is still a `div`. It is not focusable by default, it does not respond to space and enter, and it has no disabled state. You have to add all of that yourself with `tabindex`, keyboard handlers, and ARIA states. Native `<button>` gives you all of it for free, and it works correctly in every browser and every screen reader. The vast majority of ARIA on the web is unnecessary, and a meaningful fraction of it is wrong in ways that make the page less accessible than if the ARIA were removed.

When you do need ARIA, use it correctly. The patterns are well-documented in the ARIA Authoring Practices Guide (APG), which is maintained by the W3C. Read the APG before building a custom widget. The patterns for tabs, menus, comboboxes, and dialogs are non-trivial, and getting them wrong is worse than not trying.

One ARIA attribute worth knowing is `aria-label`, which lets you provide an accessible name for an element that has no visible text. An icon-only button needs an `aria-label` so screen reader users know what it does. The companion attributes `aria-labelledby` and `aria-describedby` let you reference the ID of another element on the page that provides the name or description. Prefer `aria-labelledby` over `aria-label` when the visible text on the page can serve as the label — it keeps the accessible name in sync with the visible text automatically.

A common ARIA mistake: using `aria-hidden="true"` to hide an element visually without realizing it also hides it from screen readers. If you want to hide something visually but keep it available to assistive technology, use a CSS class that moves it offscreen rather than `aria-hidden`. If you want to hide it from everyone, use `display: none`, which removes it from the accessibility tree as well as the visual layout.

Keyboard navigation and focus management

Many users do not use a mouse. They use a keyboard, a switch device, voice control, or a screen reader that simulates keyboard input. Every interactive element on your page must be reachable and operable from the keyboard.

The Tab key moves focus between interactive elements in DOM order. The default focus order is the order elements appear in the HTML, which is almost always the right order. Do not change it with `tabindex` values greater than zero — positive `tabindex` values create a custom order that is hard to maintain and confusing for users. Use `tabindex="0"` to make a non-interactive element focusable (rarely necessary), and `tabindex="-1"` to make an element focusable programmatically but not via Tab (useful for moving focus into a modal or a dynamically inserted region).

Focus must be visible. The default browser focus ring is the correct starting point; removing it without replacing it is one of the most common accessibility mistakes. If you must style focus, use `:focus-visible`, which only shows the focus indicator for keyboard users, not for mouse clicks. The CSS `outline: none` without a replacement is an accessibility failure.

Focus management becomes critical in single-page applications. When you navigate from one route to another without a full page reload, screen readers do not announce the change. You need to move focus to the new content — typically to the `<h1>` of the new page, with `tabindex="-1"` so it can receive focus — so the screen reader reads it. When you open a modal, focus must move into the modal and stay there (focus trapping) until the modal closes. When the modal closes, focus must return to the element that opened it. Get this wrong and your SPA is unusable for keyboard and screen reader users.

A useful test: unplug your mouse and try to complete every primary task on your site. If you cannot complete a purchase, fill in a form, or close a modal without the mouse, you have an accessibility bug. This test catches more issues than any automated tool.

Color, contrast, and visual design

Color contrast is the most measurable accessibility requirement and one of the most commonly failed. WCAG 2.1 Level AA requires a contrast ratio of at least 4.5:1 for normal-sized text and 3:1 for large text (18 point or 14 point bold). Level AAA requires 7:1 and 4.5:1 respectively. The ratio is calculated from the relative luminance of the foreground and background colors; pure black on pure white is 21:1, mid-gray on white is around 4.5:1, and light gray on white is well below the threshold.

The most common contrast failures are low-contrast placeholder text, secondary text in muted colors, and text on top of images without a sufficient overlay. Test your colors with a contrast checker — there are dozens of free ones online, and browser DevTools now include one. The fix is usually to darken the text color slightly; the design rarely suffers.

Color must not be the only way information is conveyed. A form field with a red border to indicate an error is fine, but the error must also be conveyed in text. A link distinguished only by color from surrounding body text fails this test for users with color blindness, who may not see the difference. Add an underline, or use a sufficiently different luminance, or both.

Text resizing is the other visual requirement. WCAG requires that text can be resized up to 200 percent without loss of content or functionality. Use relative units (`rem`, `em`, `%`) rather than pixels for font sizes, and test your layout at 200 percent zoom. Most layouts break somewhere; the fix is usually to allow text to reflow rather than fixing it at a specific size.

A note on dark mode: dark themes do not automatically satisfy contrast requirements. A common failure is dark gray text on a slightly darker background, which scores below 4.5:1 even though it looks fine to a designer with healthy vision. Run the contrast check on both themes. If you support both, your color tokens need to be tuned twice.

Forms, images, and the details that matter

Forms are where accessibility gets practical. Every input must have a label, and the label must be associated with the input via the `for` attribute (or by wrapping the input inside the label). Placeholder text is not a label — it disappears when the user starts typing, and screen readers may not announce it. Use `<label>` for every input, even if the design calls for a floating label effect; the visual trick can be implemented on top of a real label.

Error messages must be programmatically associated with the input they describe. The pattern is to use `aria-describedby` to point to the error message element, and `aria-invalid="true"` on the input. When an error appears, move focus to the first invalid field, or announce the errors via a live region. The goal is that the screen reader user knows an error occurred and what to do about it.

Images need alt text. The `alt` attribute should describe the image's purpose, not its appearance. A product photo on an e-commerce site needs alt text that conveys what the product is. A decorative image that adds nothing to the content needs `alt=""` (empty alt), which tells screen readers to skip it. An image that is the only content of a link needs alt text that describes the link's destination. The mistake to avoid is leaving the `alt` attribute off entirely — screen readers will then read the image's filename, which is rarely useful.

For complex images — charts, diagrams, infographics — a short alt text is not enough. Provide a longer description in the surrounding text, or use the `longdesc` attribute (or, more commonly, a link to a description). Data visualizations should have an accessible table alternative that contains the underlying data.

A few small things that matter more than they look: mark up abbreviations with `<abbr title="...">` so screen readers pronounce them correctly; use `<time datetime="...">` for dates so they are parsed consistently; mark up tables with `<th scope="col">` and `<th scope="row">` so screen reader users can navigate them; avoid CAPTCHAs that rely on sight or hearing, and prefer alternatives like hCaptcha or a hidden honeypot field where possible.

Motion, testing, and shipping accessible code

The `prefers-reduced-motion` media query lets you respect users who have asked their operating system to minimize motion. Some users experience motion sickness, vestibular disorders, or seizures triggered by animation. Honor the setting:

``` @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ```

This does not eliminate animation; it makes it effectively instant. Users who need reduced motion still see the start and end states, just without the transition. Parallax effects, autoplaying carousels, and full-page transitions should all be reconsidered — they cause real harm to real users.

Also honor `prefers-contrast: more` and `prefers-color-scheme` where they apply. A user who has asked their OS for high contrast is telling you something important about how your page renders on their screen. Respecting these preferences is a small change that costs you almost nothing and pays back in usability.

Testing accessibility is a continuous activity, not a final audit. Automated tools (axe DevTools, Lighthouse, WAVE) catch about thirty to forty percent of accessibility issues — mostly the mechanical ones like missing alt text, low contrast, and invalid ARIA. The rest requires manual testing. Test with a keyboard: unplug your mouse and try to use your site. Test with a screen reader: NVDA is free on Windows, VoiceOver is built into macOS and iOS, and both have a learning curve worth investing in. Test with real users who have disabilities when you can; nothing surfaces issues faster.

A useful audit checklist for every pull request:

  • Does every interactive element have a keyboard equivalent?
  • Is focus visible everywhere it can land?
  • Does every image have appropriate alt text (or empty alt if decorative)?
  • Are form fields labeled and are errors announced?
  • Does the page have a logical heading order?
  • Is color contrast at least 4.5:1 for body text?
  • Does the page respect prefers-reduced-motion?

Accessibility is a habit, not a feature. Build it into your component library, your design tokens, your code review checklist, and your definition of done. The work is not extra; it is the work, done properly.