Lesson 10 of 32
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.
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
alttext (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 anid, or wrapping the control — neverplaceholderalone.
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 positivetabindexvalues 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.
Skip links
<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
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
1.Under WCAG 2.1 AA, what is the minimum contrast ratio required for normal-sized body text?
2.Why is removing the default focus outline with
outline: noneand not replacing it a real accessibility failure?3.What is
tabindex="-1"for?4.What does the WAI-ARIA guideline "no ARIA is better than bad ARIA" mean in practice?
4 left to answer