Accessibility Fundamentals

Learn the foundations of web accessibility: semantic structure, heading hierarchy, labels and alt text, keyboard navigation and focus, accessible links and buttons, an overview of ARIA, and the first rule of when NOT to use ARIA at all.

Accessibility (often abbreviated a11y) means building pages that people with disabilities — visual, motor, auditory, or cognitive — can actually use: someone navigating by keyboard alone, someone using a screen reader, someone with low vision zoomed in at 400%. The encouraging news is that most of accessibility isn't a separate skill bolted onto HTML — it's simply using HTML correctly. This lesson builds directly on Semantic Elements and Global Attributes to show how markup choices translate into real usability.

Semantic HTML is the foundation

Every accessibility feature you get 'for free' — landmark navigation, correct roles, built-in keyboard behavior — comes from using the right native element for the job. A <button> is focusable, triggerable with Enter and Space, and announced as 'button' by screen readers automatically. A <div onclick="..."> gets none of that unless you rebuild it by hand. This is the single most important idea in accessibility: before reaching for extra attributes or scripting, ask whether a more semantically correct element already solves the problem.

Heading structure

Screen reader users frequently navigate a page by jumping between headings, similar to skimming a table of contents. This only works if headings (h1 through h6) are used to reflect the actual logical structure of the content — one h1 per page describing its main topic, with h2s and h3s nesting sensibly beneath it — rather than being chosen for their default font size. Skipping levels (jumping from h1 straight to h4) or using headings purely because 'it looks bold' breaks that navigation experience.

Labels and alt text

Every form input needs an associated, programmatically-linked label — either by wrapping the input in a <label>, or connecting a <label for="id"> to an <input id="id">. A placeholder is not a substitute for a label: it disappears the moment the user types, and many screen readers don't reliably announce it as a label.

Every meaningful <img> needs an alt attribute describing what the image conveys, so a screen reader user gets equivalent information. A purely decorative image (a background flourish with no informational content) should use alt="" (empty, but present) so screen readers skip it entirely instead of announcing an unhelpful filename.

Keyboard navigation and focus

Many users — not only screen reader users, but people with motor impairments, or simply power users — navigate entirely by keyboard, using Tab, Shift+Tab, Enter, and Space instead of a mouse. Every interactive element must be reachable and operable this way.

The browser draws a visible focus indicator (commonly an outline) around whatever element currently has keyboard focus, so a keyboard user can see where they are on the page. Removing it with CSS like *:focus { outline: none; } without providing a replacement is one of the most damaging and common accessibility mistakes on the web — it leaves keyboard users completely unable to tell where they are.

Accessible links and buttons

Link text should describe its destination on its own, without relying on surrounding context — 'Read the full accessibility guide' is accessible; 'click here' or 'read more' is not, because screen reader users often pull up a list of all links on a page out of context, and a list full of 'click here, click here, click here' is useless.

Use <button> for anything that performs an action on the current page (submitting a form, opening a modal, toggling a menu), and <a href="..."> only for actual navigation to a new URL or page section. A clickable <div> styled to look like a button provides none of the native keyboard support, focusability, or semantic role a real <button> gives you automatically — you'd have to manually reimplement all of it with tabindex, role, and key event handlers, and it's easy to miss something.

ARIA: roles, states, and properties

ARIA (Accessible Rich Internet Applications) is a set of extra HTML attributes — role, and aria-* attributes — that describe custom widgets to assistive technology when native HTML has no element for what you're building. role tells assistive tech what kind of widget something is (role="tablist", role="dialog"). States and properties like aria-expanded, aria-checked, aria-hidden, and aria-label communicate dynamic information a native element can't otherwise express.

ARIA is powerful for genuinely custom components (a tab panel, a combobox, a custom slider) that HTML has no native equivalent for — but it comes with a critical caveat covered next.

The first rule of ARIA: don't use ARIA

The Web Accessibility Initiative's own guidance states this bluntly: if a native HTML element or attribute already has the semantics and behavior you need, use it, rather than repurposing another element and adding ARIA to make it accessible. ARIA only changes what assistive technology is *told* about an element — it does nothing to change actual browser behavior like keyboard operability, focus management, or built-in form validation. Adding role="button" to a <div> tells a screen reader 'this is a button,' but the div still won't respond to the Enter key, still won't be in the tab order, and still won't trigger on Space — unless you also manually implement every one of those behaviors yourself. Misused or unnecessary ARIA doesn't just fail to help; incorrect ARIA (a wrong role, a state that never gets updated) can actively make a page less accessible than using no ARIA at all, because it tells assistive tech something false about the page.

Accessible form field pattern

The for attribute's value must exactly match the input's id. Both patterns create a programmatic label-to-input association that assistive tech relies on.

<label for="email">Email address</label>
<input type="email" id="email" name="email" required>

<!-- or, wrapping form (no id/for needed): -->
<label>
  Email address
  <input type="email" name="email" required>
</label>

Code Examples

The <div> version is invisible to keyboard users entirely — it can't even receive focus without extra tabindex, role, and manual key handling. The <button> gets all of that for free from the browser.

HTML has no native <tab> element, so ARIA roles are appropriate here — note the tabs are still built on real <button> elements underneath, keeping native keyboard/focus behavior, with ARIA layered on top only to describe the tablist relationship.

This is exactly what the first rule of ARIA warns against: role="button" makes a screen reader announce 'button,' creating a false promise, but the <span> still isn't focusable and still won't fire on Enter or Space without extra manual JavaScript. A real <button>Delete</button> accomplishes the same visual result with zero extra work and zero risk of missing a behavior.

Common mistake: Adding an ARIA role to a non-interactive element without also manually replicating every native behavior that role implies.

<!-- Inaccessible: no keyboard support, no focus, no role -->
<div class="btn" onclick="submitForm()">Submit</div>

<!-- Accessible: free keyboard support, focus, and role -->
<button type="button" onclick="submitForm()">Submit</button>
<div role="tablist" aria-label="Settings sections">
  <button role="tab" aria-selected="true" aria-controls="panel-general" id="tab-general">General</button>
  <button role="tab" aria-selected="false" aria-controls="panel-privacy" id="tab-privacy">Privacy</button>
</div>
<div role="tabpanel" id="panel-general" aria-labelledby="tab-general">...</div>
<div role="tabpanel" id="panel-privacy" aria-labelledby="tab-privacy" hidden>...</div>
<span role="button" onclick="deleteItem()">Delete</span>

Important Rules

Prefer a native HTML element over an ARIA-patched generic element whenever one exists — this is literally called the first rule of ARIA.

Never remove the visible focus indicator without providing a clear, sufficiently visible custom replacement.

Every <img> needs an alt attribute — descriptive text for meaningful images, alt="" for purely decorative ones.

Every form input needs a programmatically associated <label>, not just a placeholder.

Link text must make sense read out of context — avoid 'click here' and 'read more' as the entire link text.

Use one logical h1-h6 heading hierarchy per page; don't skip levels or choose headings by visual size alone.

In practice

Automated accessibility audits (like Lighthouse or axe) can catch missing alt text, missing labels, and low-contrast text automatically, but they cannot catch misused ARIA that lies about a widget's true behavior, or a heading structure that's technically present but logically scrambled — that requires actual keyboard testing (tab through the whole page) and, ideally, testing with a real screen reader like VoiceOver or NVDA. Accessibility lawsuits under laws like the ADA in the US have specifically targeted sites with keyboard traps, missing form labels, and unlabeled buttons — this isn't just an ethical best practice, it's frequently a legal requirement for many organizations.

<input type="text" placeholder="Full name"> <label for="fullname">Full name</label> <input type="text" id="fullname" placeholder="e.g. Jane Doe">

A placeholder is not an accessible label — it vanishes once text is entered and isn't reliably announced by all assistive technology. It's fine as supplementary hint text alongside a real label, never as a replacement for one.

a, a:focus { outline: none; } a:focus-visible { outline: 2px solid #1e3a8a; outline-offset: 2px; }

Blanket-removing outline: none strips the focus indicator for every keyboard user with no replacement. :focus-visible lets you style focus specifically for keyboard interaction while still keeping a visible indicator.

[Concept Matching] A developer builds a custom dropdown menu entirely from styled <div> elements and adds role="button", aria-expanded="false", and tabindex="0" to the trigger div, but writes no additional JavaScript to handle Enter, Space, or Escape key presses. What does the 'first rule of ARIA' say about this situation? A. The ARIA attributes alone are sufficient — browsers automatically add keyboard behavior to any element with role="button". B. This setup is fine because aria-expanded and tabindex together already provide full keyboard operability. C. ARIA attributes only describe an element to assistive technology; they don't add real keyboard behavior, so this div will be announced as a button but still won't respond to Enter, Space, or Escape — a native <button> would have provided that behavior automatically, which is exactly why the first rule of ARIA favors native elements when one fits. D. The first rule of ARIA states that role="button" should never be used under any circumstances, even for genuinely custom widgets with no native equivalent.

C. ARIA attributes only describe an element to assistive technology; they don't add real keyboard behavior, so this div will be announced as a button but still won't respond to Enter, Space, or Escape — a native <button> would have provided that behavior automatically, which is exactly why the first rule of ARIA favors native elements when one fits. — ARIA changes only what assistive technology is told about an element (its announced role/state), never the browser's actual behavior. Keyboard operability, focus handling, and activation on Enter/Space must still be built manually for a non-native element — which is precisely why the first rule of ARIA is to use a native element like <button> whenever one already provides the needed behavior for free.

Take a 'toggle switch' built as <div class="toggle" onclick="toggle()"></div> and make it fully accessible using ARIA — but first ask whether ARIA is even the right tool, or whether a native element could do the job with less risk.

Option A (native, preferred): <input type="checkbox" id="notif"> styled with CSS to look like a switch, with <label for="notif">Notifications</label>. Option B (custom widget, if a checkbox truly can't be styled to fit): <div role="switch" aria-checked="false" tabindex="0" onclick="toggle()" onkeydown="handleKey(event)">Notifications</div>, manually handling Enter/Space and updating aria-checked in JavaScript.

A checkbox styled to look like a toggle switch (<input type="checkbox">, hidden visually, styled via CSS) gets full keyboard and screen reader support natively, with zero ARIA needed — that's usually the better answer than a div with role="switch".