AniUI Academy

Accessibility Fundamentals

Tying together semantic HTML, alt text and labels with new ground — WCAG contrast ratios, visible focus states, tabindex, skip links, and why no ARIA is better than bad ARIA.

10 min read

The previous nine lessons each touched accessibility as they went — alt text, label association, landmark roles, table scope. This lesson pulls those threads together and adds the remaining pieces you need to actually build a page that works for people using a keyboard, a screen reader, or both, instead of a mouse and full-colour vision.

What you've already covered — quickly, in one place

  • Semantic HTML and landmarks (from the Semantic HTML lesson): <nav>, <main>, a top-level <header>/<footer>, and <aside> expose real ARIA landmark roles automatically, letting screen reader users jump directly between regions of a page.
  • Meaningful alt text (from Images and Media): describes what an image actually communicates, or is left empty (alt="") for purely decorative images so screen readers skip them silently.
  • Real label association (from Forms and Inputs): <label for> matched to an id, or wrapping the control — never placeholder alone.

If any of those feel shaky, it's worth a quick revisit — this lesson builds on top of them rather than repeating them in full.

Colour contrast: the real WCAG numbers

Text that's technically visible but low-contrast is unreadable for a meaningful share of visitors — not just people with diagnosed vision impairments, but anyone reading on a dim screen outdoors, or simply as eyes age. WCAG 2.1 Success Criterion 1.4.3 (level AA) sets concrete, measurable minimum contrast ratios between text and its background:

  • 4.5:1 minimum for normal-sized text.
  • 3:1 minimum for large text — roughly 18pt (24px) and up, or 14pt (about 18.5px) and up if bold.

These ratios are calculated from the relative luminance of the two colours, not just "does it look okay" — and they're checkable with a calculator rather than a judgement call. Light grey text on a white background is the classic failure: it often looks clean and modern, and often fails 4.5:1 outright.

Visible keyboard focus

Every browser ships a default visual indicator — commonly a blue outline — that appears around whatever element currently has keyboard focus. A keyboard-only user (someone who can't use, or chooses not to use, a mouse) relies on this indicator as their only way of knowing where they are on the page and what they're about to activate if they press Enter.

/* Do not do this without replacing it with something */
:focus {
  outline: none;
}

Removing the default outline with nothing in its place is a real, well-documented accessibility failure, not a nitpick — it leaves a keyboard user with no visible indication of their position on the page at all. If the default outline genuinely clashes with your design, replace it with your own visible alternative rather than deleting it:

:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

:focus-visible (rather than plain :focus) is worth knowing about here: it lets browsers show the indicator specifically for keyboard navigation while skipping it for a mouse click, which is closer to what most designs actually want — visible for the people who need it, without a visible ring around every button someone clicks with a mouse.

tabindex

By default, interactive elements (<a>, <button>, <input>, and similar) are focusable and reachable via the Tab key in their natural document order. tabindex changes that behaviour for other elements, and has three distinct uses:

  • tabindex="0" — makes a normally non-interactive element (a <div> acting as a custom control, say) focusable and reachable by Tab, in its natural position in the document order.
  • tabindex="-1" — makes an element focusable programmatically (via .focus() from JavaScript, commonly after some action like opening a dialog) but removes it from the Tab key sequence entirely — a visitor tabbing through the page will never land on it by pressing Tab.
  • A positive value (tabindex="1", "2", ...) — forces the element to a specific position in the tab order, ahead of everything with a default or zero tabindex. This is a real anti-pattern. It's extremely easy to lose track of which elements have which positive value as a page grows, and the result is a Tab order that jumps around in an order that matches nobody's mental model of the page — worse than the natural order it replaced. Avoid positive tabindex values entirely; if the natural order is wrong, that usually means the underlying HTML order needs to change, not that tabindex needs to patch around it.
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- ... header, nav ... -->
<main id="main-content">
  ...
</main>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 0.5rem 1rem;
  z-index: 100;
}
 
.skip-link:focus {
  top: 0;
}

A skip link is a link, placed as the very first focusable element on the page, that jumps straight past repeated navigation to the main content — "Skip to main content." It's hidden by default (moved off-screen with positioning, not display: none, which would remove it from focus order entirely) and becomes visible the moment it receives keyboard focus. Without one, a keyboard user has to tab through every single navigation link on every single page load before reaching the actual content — genuinely tedious on a page with a long navigation menu, and it's a near-zero-cost addition to fix.

"No ARIA is better than bad ARIA"

WAI-ARIA (Accessible Rich Internet Applications) is a set of HTML attributes — role, aria-label, aria-expanded, and many more — for describing custom widgets to assistive technology when no native HTML element already does the job. The official WAI-ARIA authoring guidance states the priority plainly: prefer a native semantic element first; only reach for ARIA when there genuinely is no native element that provides the needed behaviour.

<!-- Worse: a div pretending to be a button -->
<div role="button" onclick="submit()">Submit</div>
 
<!-- Better: it already IS a button -->
<button type="submit">Submit</button>

A real <button> already comes with the correct role, keyboard support (Enter and Space activate it, for free), and focusability built in. The <div role="button"> version only gets the role right — it still needs manual JavaScript to be focusable and to respond to keyboard activation, and it's extremely easy to get one of those pieces wrong or forget it entirely. An incorrect or incomplete ARIA attribute can actively mislead assistive technology — telling it something is a certain kind of control when it doesn't actually behave like one — which is worse than having said nothing and let the browser's own default (a plain, unstyled <div> with no implied behaviour) come through. Reach for ARIA to describe genuinely custom widgets with no native equivalent — a tab panel, a custom dropdown — not as a substitute for elements that already exist.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • Semantic landmarks, real alt text, and real label association (covered earlier in this track) are the foundation — this lesson adds the rest.
  • WCAG 2.1 AA requires 4.5:1 contrast for normal text, 3:1 for large text — measurable, not a judgement call.
  • Never remove the focus outline without replacing it with a visible alternative; keyboard users rely on it entirely.
  • tabindex="0" adds natural-order focusability, tabindex="-1" allows only programmatic focus, and positive values are an anti-pattern to avoid.
  • A skip link, hidden until focused, saves keyboard users from tabbing through navigation on every page.
  • Prefer native semantic HTML over ARIA; only add ARIA when no native element covers the behaviour you need.

Check yourself

4 questions · pass 3/4 to unlock Introducing CSS

up to 50
  1. 1.Under WCAG 2.1 AA, what is the minimum contrast ratio required for normal-sized body text?

  2. 2.Why is removing the default focus outline with outline: none and not replacing it a real accessibility failure?

  3. 3.What is tabindex="-1" for?

  4. 4.What does the WAI-ARIA guideline "no ARIA is better than bad ARIA" mean in practice?

4 left to answer