← back to section

Imagine that all navigation on your site is built from <div onClick>. For someone who can see the screen and holds a mouse, everything works. But a person who controls the computer with a keyboard or listens to the page through a screen reader hits a wall: buttons do not respond to Enter, elements have no names, the focus order is chaotic.

Accessibility means the site works for everyone: people with visual, hearing, or motor impairments, as well as those who use only a keyboard or bump the font size up. On top of that, accessible markup is generally indexed better by search engines — the browser and the search crawler both love correct HTML.

The problem: div instead of proper elements

When developers attach click handlers to div or span, the browser has no idea these are interactive elements. The browser will not include them in the Tab order, will not inform the screen reader that an element is a button, and will not handle the Enter key.

<!-- bad: div tells the browser nothing -->
<div onclick="save()">Save</div>

<!-- good: the browser already knows everything about this button -->
<button onclick="save()">Save</button>

<button> receives focus via Tab, activates on Enter and Space, and the screen reader announces its role — "button." All of this is free, without a single line of JavaScript.

Semantic markup

Ninety percent of accessibility is simply using the right HTML elements. The browser knows their meaning and provides the correct behavior automatically:

  • <nav> — navigation: a screen reader lets users jump directly to it.
  • <main> — the main content of the page.
  • <button> — an interactive button, focusable and operable from the keyboard.
  • <a href> — a link, opens on Enter.
  • <label> — a caption for an input field; clicking it moves focus to the field.
  • <h1><h6> — headings in hierarchy; skipping levels (h1 → h3 without h2) should be avoided.
  • <img alt="..."> — every image must have an alt attribute; if the image is decorative, use alt="".
<label for="email">Email address</label>
<input id="email" type="email" />

The rule is simple: look for the right HTML element first. Only if one does not exist should you add ARIA.

Keyboard and focus

Everything that works with a mouse must work with a keyboard. The check is easy: put the mouse aside and navigate through the page using Tab.

  • Focus moves through elements in a logical order via Tab.
  • Buttons and links activate on Enter.
  • Buttons (not links) also activate on Space.
  • It is visible which element currently has focus — that is the :focus-visible style.

The last point is frequently broken "for aesthetics": a developer removes the outline: none border, and keyboard users can no longer tell where they are. This is one of the most common and easily fixable defects.

Semantic elements provide all of the above for free. Custom widgets — dropdown menus, modal windows, carousels — must be implemented manually:

  • in an open modal, focus must stay inside (focus trap);
  • when the modal closes, focus returns to the element that opened it;
  • pressing Escape closes the modal.

ARIA — only when there is no HTML equivalent

ARIA (Accessible Rich Internet Applications) is a set of attributes that add meaning where HTML falls short. But there is a first rule of ARIA: do not use ARIA if a native HTML element exists.

<button> is better than <div role="button" tabindex="0"> with a manual key handler. The latter requires manual maintenance and it is easy to make a mistake.

Wrong ARIA is worse than no ARIA: it misleads the screen reader and creates confusion. ARIA is appropriate where HTML has no equivalent:

<!-- A notification that appears dynamically -->
<div aria-live="polite" aria-atomic="true">
  File uploaded successfully
</div>

<!-- An icon-only button with no visible text -->
<button aria-label="Close">✕</button>

<!-- Accordion -->
<button aria-expanded="false" aria-controls="panel1">Section 1</button>
<div id="panel1" hidden>Content</div>

aria-live — for regions that update without a page reload; the screen reader will announce the change aloud. aria-label — when a button has no text, only an icon. aria-expanded — to communicate the open/collapsed state of an element.

Color and contrast

Information must not be conveyed through color alone — some users cannot distinguish hues. If an error in a form is highlighted only in red, add an icon or text as well.

Text must have sufficient contrast against the background. WCAG defines the minimum threshold: 4.5:1 for regular text and 3:1 for large text (18px and above). You can check contrast in the browser's DevTools or with dedicated tools such as Colour Contrast Analyser.

How to check accessibility

Automatically. The axe tool (browser extension or test plugin) catches common errors: missing alt, poor contrast, incorrect ARIA, broken heading hierarchy. Automation covers around 30–40% of issues.

In tests. Testing Library finds elements the same way a screen reader does — by role and accessible name. If a test is written using getByRole, it also verifies accessibility as a side effect:

// The test won't find the button if it doesn't have a meaningful name
const button = screen.getByRole("button", { name: "Save" });

If an element cannot be found by role, it is most likely inaccessible to assistive technologies.

By hand. Put the mouse aside and navigate through the page with your keyboard. Turn on a screen reader (VoiceOver on macOS, NVDA or JAWS on Windows, TalkBack on Android) and listen to what it reads. This quickly surfaces problems that automation misses.

In short

  • Proper HTML elements (<button>, <nav>, <label>) give you accessibility for free — do not replace them with <div>.
  • Headings follow a proper order, images have alt, fields have <label>.
  • Everything that works with a mouse also works with a keyboard: Tab, Enter, Space, Escape.
  • Visible focus (:focus-visible) is never removed — without it keyboard users get lost.
  • ARIA is added only where HTML has no suitable element; wrong ARIA is worse than none.
  • aria-live — for dynamic notifications; aria-label — for icon-only elements; aria-expanded — for expandable elements.
  • Information must not be conveyed by color alone; text contrast must be at least 4.5:1.
  • axe catches ~30–40% of issues automatically; the rest requires keyboard and screen reader testing.
  • Testing Library with getByRole verifies accessibility as a side effect of ordinary tests.
  • Testing: Testing Library and Vitest — how to write tests by role.
  • Rendering performance — optimizing React components.