
Accessibility is easiest to maintain when it is part of implementation rather than a final audit. A meaningful page structure, a usable keyboard path, a correctly labelled form, and a visible focus indicator help people use a site before you add a testing tool.
This guide covers practical techniques for developers building websites and web applications. It focuses on semantic HTML, keyboard interaction, forms, focus, ARIA, color, and responsive behavior. It is an implementation guide, not a legal compliance guarantee. Use the W3C Web Content Accessibility Guidelines (WCAG) overview to understand the current standards and choose the conformance target that fits your project.
The techniques below also make a site easier to test. Once you have applied them, use the manual and automated checks in our developer's guide to testing website accessibility to find problems that implementation alone cannot reveal.
Start with a useful document structure
HTML elements communicate relationships to browsers and assistive technology. Start with the element that describes the behavior instead of starting with a generic <div> and adding attributes later.
Use one <main> element for the primary page content, headings in a logical order, <nav> for navigation, <header> and <footer> for page or section boundaries, and lists for groups of related items. A heading level should describe the section hierarchy, not the visual size you want. CSS can make an <h2> look larger or smaller without changing its meaning.
For example, a page shell can begin like this:
<a class="skip-link" href="#main-content">Skip to main content</a>
<header>
<a href="/" aria-label="Super Dev Resources home">Super Dev Resources</a>
<nav aria-label="Primary navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="#main-content">Main content</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<h1>Choose an accessible form pattern</h1>
<section aria-labelledby="validation-heading">
<h2 id="validation-heading">Validation</h2>
<p>Explain an error next to the field that needs attention.</p>
</section>
</main>
The link's aria-label is useful here only because the visible link contains a logo or site name whose accessible name needs to be explicit. Do not add ARIA labels to every element by habit. Visible text is usually the best accessible name.
The skip link gives keyboard users a way to bypass repeated navigation. Keep it visually hidden until it receives focus:
.skip-link {
position: absolute;
left: 1rem;
top: 0;
transform: translateY(-150%);
padding: 0.75rem 1rem;
background: #ffffff;
color: #111111;
z-index: 1000;
}
.skip-link:focus {
transform: translateY(0);
}
Avoid using CSS order or heading sizes to create a structure that the HTML does not express. A sighted visitor may infer the intended layout, but someone navigating by headings or landmarks relies on the document structure itself.
Use native controls before custom widgets
Native HTML controls already provide keyboard behavior, focus handling, form integration, and semantics. Use a <button> for an action and an <a> for navigation. Do not use a clickable <div> or <span> merely because it is easier to style.
<button type="button" id="save-button">Save settings</button>
<a href="#account">View account</a>
This is safer than:
<div class="button" onclick="saveSettings()">Save settings</div>
The custom version does not automatically behave like a button when a user presses Enter or Space, does not receive the same semantics, and may not be reachable by keyboard. Adding role="button" does not recreate all of the native behavior; you would still need to implement focusability, key handling, disabled state, and other details.
Use <details> and <summary> for a simple disclosure, <dialog> when the browser behavior and support requirements fit your project, and native form controls wherever they meet the requirement. A custom menu, combobox, tab interface, or modal needs a complete interaction model. The WAI-ARIA Authoring Practices Guide documents patterns when a custom widget is genuinely necessary.
Make every task possible with a keyboard
WCAG includes keyboard-access requirements because a pointer is not the only way to operate a web page. Test the complete task without a mouse:
- Press Tab and Shift+Tab through the page.
- Check that the focus order follows the task's visual and logical order.
- Activate links, buttons, menus, dialogs, and form controls with the expected keys.
- Confirm that a user can reach and leave every component.
- Look for content that opens on hover but cannot be opened with a keyboard.
Do not use positive tabindex values to force an order. They create a second order that is difficult to maintain as the page changes. Let native controls enter the tab sequence naturally. Use tabindex="-1" only when you need to move focus programmatically to an element that should not be reached during ordinary tabbing, such as a newly opened dialog heading or an error summary.
If a button opens a dialog, move focus into the dialog and return it to the triggering button when the dialog closes. If a menu or listbox is custom, implement the keyboard behavior described by its chosen ARIA pattern rather than adding a role and stopping there.
Keep focus visible
Never remove the browser's focus indicator without replacing it with an equally clear or clearer style. :focus-visible lets you provide a strong indicator when keyboard focus needs to be shown:
:where(a, button, input, select, textarea, summary):focus-visible {
outline: 3px solid #005fcc;
outline-offset: 3px;
}
Check the indicator against the surrounding background, including on cards, images, dark themes, and sticky headers. Do not rely on color alone. A thicker outline, offset, underline, or contrasting background can make the focused control easier to identify.
When a component moves focus after an action, explain why in the interface. For example, move focus to a results heading after a search submission if the page does not otherwise make the new results location clear. Avoid moving focus on every click; unexpected focus changes can make ordinary navigation confusing.
Build forms with labels and useful errors
An accessible form needs more than a temporary hint. Give every control a visible label, associate it with the control, provide instructions before the user needs them, and describe errors in text.
<form>
<div>
<label for="email">Email address</label>
<input
id="email"
name="email"
type="email"
autocomplete="email"
aria-describedby="email-help email-error"
required
>
<p id="email-help">We will use this address for the receipt.</p>
<p id="email-error" hidden>Please enter a valid email address.</p>
</div>
<button type="submit">Send receipt</button>
</form>
When validation fails, make the error visible, set aria-invalid="true" on the invalid control, and ensure that the message explains how to fix the problem. If several errors can occur, provide an error summary with links to the affected controls and move focus to the summary or the first invalid control according to the flow your application uses.
The browser's required and type validation can help, but it is not a replacement for server-side validation. A client can bypass HTML constraints, and a server must validate and authorize submitted data independently. Keep error messages specific without echoing sensitive input or revealing whether a private account exists.
Do not use a temporary field hint as the only label. Hint text disappears while the user types, can have weak contrast, and does not reliably describe the field after an error. Use autocomplete tokens where the expected value has a standard token so browsers and password managers can help users complete the form.
For grouped choices, use <fieldset> and <legend>:
<fieldset>
<legend>Preferred contact method</legend>
<label><input type="radio" name="contact" value="email"> Email</label>
<label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>
The group label prevents users from having to infer what the individual radio buttons refer to. Use the same principle for related checkboxes and other groups of controls.
Treat ARIA as a precise tool
ARIA can expose a widget's role, name, state, or relationship to assistive technology. It cannot repair incorrect interaction behavior. The first rule in the WAI-ARIA Authoring Practices is to use a native HTML element when one provides the required semantics and behavior.
Before adding ARIA, ask:
- Does a native element already express this control?
- Is the accessible name visible and meaningful?
- Does the state change in the DOM when the UI changes?
- Can a keyboard user perform the same actions as a pointer user?
- Have I implemented the focus behavior and keyboard commands for this pattern?
For example, aria-expanded="false" on a button is useful when the button controls a collapsible region. The value must change to "true" when the region opens, and aria-controls should identify the controlled element:
<button
type="button"
aria-expanded="false"
aria-controls="filters"
>
Filters
</button>
<div id="filters" hidden>
<!-- Filter controls -->
</div>
JavaScript must update both the hidden state and aria-expanded state. A static attribute on a permanently visible or permanently hidden element gives assistive technology incorrect information.
Do not add role="presentation" or aria-hidden="true" to content that a user needs. Also avoid placing focusable elements inside an aria-hidden subtree. Hiding content from a screen reader does not remove it from every interaction model; manage the DOM and focus state together.
Do not communicate meaning with color alone
Color can support a message, but it should not be the only signal for success, errors, required fields, links, or status. Pair it with text, an icon with an accessible name, a pattern, or another programmatically available cue.
<p class="status status-error">
<strong>Error:</strong> Your payment method could not be saved.
</p>
.status-error {
border-inline-start: 0.25rem solid #b42318;
padding-inline-start: 0.75rem;
}
The text remains meaningful if the user cannot distinguish the colors. The border is a visual reinforcement, not the only explanation.
For WCAG 2.2, the contrast requirement for normal-sized text is at least 4.5:1, and large text has a 3:1 minimum under Success Criterion 1.4.3. User-interface components and meaningful graphical objects have a separate 3:1 non-text contrast requirement under Success Criterion 1.4.11. Check text, controls, temporary hints, focus indicators, icons, and chart elements against the actual backgrounds where they appear. The WCAG 2.2 Understanding documents explain the exceptions and measurement details.
Contrast is not a complete accessibility review. A page can meet a ratio and still be hard to use because of small text, dense layout, missing labels, motion, or poor focus order.
Support zoom, reflow, and reduced motion
Do not disable browser zoom with a restrictive viewport setting. Let users enlarge content and test the layout at increased text size and narrow widths. Avoid fixed-height containers that clip text when it wraps, and make sure dialogs, tables, navigation, and code blocks have a usable strategy on small screens.
For motion, respect the user's preference when an animation is not essential:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
This is a baseline, not permission to hide important state changes. A reduced-motion mode should still make it clear that content opened, closed, loaded, or moved. Avoid autoplaying video and large, unexpected movement, especially near interactive controls.
Common implementation mistakes
These mistakes often appear in otherwise polished interfaces:
- A clickable card with no real link: Use a real
<a>for navigation and make the link text describe the destination. - An icon-only button with no name: Add visible text when possible; otherwise provide an accessible name such as
aria-label="Close dialog". - A custom select that traps focus: Start with a native
<select>unless the product requirement genuinely needs a richer widget. - An error shown only in red: Add a text message, associate it with the invalid control, and make the error location discoverable.
- A modal that leaves focus behind: move focus into the dialog, keep focus inside while it is modal, and return focus when it closes.
- A heading chosen for appearance: Keep the document hierarchy correct and style it with CSS.
- A permanently hidden focus outline: Restore a visible
:focus-visibleindicator. - A test that checks only automated violations: Include keyboard, zoom, screen-reader, content-order, and task-completion checks.
Automation is valuable, but it cannot decide whether a heading is useful, whether an error is understandable, or whether a user can complete a task efficiently. Apply implementation practices first, then combine automated checks with manual review.
A practical pre-release checklist
Before shipping a page or component, ask:
- Can a user understand the page from its headings and landmarks?
- Can every action and form control be reached and operated with a keyboard?
- Is focus visible and does it move only when there is a clear reason?
- Does every control have a useful accessible name and state?
- Are labels, instructions, grouped choices, and errors connected to the right controls?
- Does the interface remain usable at increased text size and narrow widths?
- Do color, icons, and motion reinforce meaning without being the only way to perceive it?
- Have you tested the actual task with keyboard navigation and at least one assistive technology?
- Have you run automated checks and investigated their findings instead of treating the score as a pass?
Accessibility is not a separate version of the interface. Semantic markup, predictable focus, clear errors, and resilient layout improve the experience for many users and give the rest of the team a stronger foundation to test. Build those behaviors into reusable components, document the exceptions, and review them whenever the component changes.