Accessibility Tips Most Developers Forget
#accessibility
#a11y
#webdev
#frontend
#ux
Accessibility is not a checklist you bolt on at the end—it’s a quality attribute of your product. The good news: many high-impact improvements are small, cheap, and easy to standardize. Here are the tips even experienced developers often miss, with quick examples you can copy into your codebase today.
1) Don’t remove focus outlines—style them
Keyboard users rely on a visible focus. If you remove outlines, you hide where they are on the page.
/* Keep a visible focus; customize instead of removing */
:focus {
outline: none; /* only if you provide a replacement */
}
:focus-visible {
outline: 2px solid #0a7cff;
outline-offset: 2px;
border-radius: 4px;
}
Use :focus-visible to avoid noisy outlines during mouse interaction while preserving keyboard visibility.
2) Add a skip link and use landmarks
Skip links let keyboard users jump past repetitive navigation.
<a class="skip-link" href="#main">Skip to content</a>
<header>...</header>
<nav aria-label="Primary">...</nav>
<main id="main">...</main>
<footer>...</footer>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
background: white;
color: #111;
padding: 0.5rem 1rem;
z-index: 1000;
}
Use semantic landmarks (header, nav, main, footer) or roles (banner, navigation, main, contentinfo).
3) Keep heading levels meaningful and in order
Headings are the outline of your page. Avoid using headings for styling only, and don’t skip levels arbitrarily.
- Exactly one h1 per page
- Nest h2 under h1, h3 under h2, etc.
- Don’t create headings with div + role=“heading” unless necessary
4) Use the right element: link vs. button
- Anchors (a) navigate; Buttons (button) perform actions.
- In forms, set type=“button” to avoid accidental submits.
<!-- Correct: navigation -->
<a href="/pricing">View pricing</a>
<!-- Correct: action -->
<button type="button">Open modal</button>
Give buttons an accessible name via text or aria-label if the label is not visible.
5) Always pair inputs with labels, help text, and errors
Screen readers need explicit relationships.
<label for="email">Email</label>
<input id="email" name="email" type="email" aria-describedby="email-hint email-err">
<div id="email-hint">We’ll never share your email.</div>
<div id="email-err" role="alert">Please enter a valid email.</div>
- Use aria-invalid=“true” on error.
- Use role=“alert” (or aria-live=“assertive”) for immediate error announcements.
6) Write alt text for purpose, not pixels
- Informative images: concise, contextual alt text.
- Decorative images: alt="" (empty) and optionally role=“presentation”.
- Icons-only controls need labels:
<button aria-label="Search">
<svg aria-hidden="true" ...></svg>
</button>
Avoid duplicating nearby text in alt. If an image is a link, alt should describe the target or action.
7) Don’t rely on color alone
Color-only cues exclude many users.
- Links should have visible affordances (underline) and sufficient contrast.
- Minimum contrast: 4.5:1 for normal text, 3:1 for large text (≥18pt regular or 14pt bold), and 3:1 for UI components and focus indicators.
a {
text-decoration: underline;
text-underline-offset: 0.15em;
}
8) Manage focus in modals, drawers, and menus
Dynamic UI must not lose or trap focus.
- Move focus into the modal when opened
- Trap focus while open
- Close with Escape and click outside
- Return focus to the trigger on close
Prefer the dialog element where possible.
<button id="open">Open dialog</button>
<dialog id="modal" aria-labelledby="modal-title">
<h2 id="modal-title">Subscribe</h2>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
<script>
const open = document.getElementById('open');
const modal = document.getElementById('modal');
open.addEventListener('click', () => modal.showModal());
modal.addEventListener('close', () => open.focus());
</script>
For custom modals, implement focus trap and aria-modal=“true”.
9) Announce dynamic updates with live regions
If the page updates without navigation (e.g., cart count, async status), announce it.
<span id="cart-count" aria-live="polite">0</span>
For urgent errors, use role=“alert” or aria-live=“assertive”.
10) Respect user preferences
- Reduced motion:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
- High contrast/forced colors: test with Windows High Contrast or forced-colors modes
- Dark/light modes: ensure contrast in both
11) Don’t disable zoom; set the language
- Avoid meta viewport values that prevent zoom (no user-scalable=no or max-scale=1).
- Declare the page language:
<html lang="en">
Unique, descriptive page titles also help screen reader navigation and SEO.
12) Ensure adequate touch targets
Make interactive targets at least 44×44 CSS pixels with generous spacing. Avoid tiny checkboxes or closely packed links.
button,
a[role="button"] {
min-width: 44px;
min-height: 44px;
}
13) Use real tables for data, with headers
<table>
<caption>Quarterly revenue</caption>
<thead>
<tr>
<th scope="col">Quarter</th>
<th scope="col">Revenue</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Q1</th>
<td>$120k</td>
</tr>
</tbody>
</table>
Avoid using tables for layout. For complex tables, use headers/id or aria-describedby.
14) Keep a logical focus order; avoid tabindex > 0
The DOM order is the tab order. Reordering visually with CSS is fine, but keep interactive elements in a meaningful sequence.
- Use tabindex=“0” sparingly (only to include custom widgets in tab order).
- Avoid positive tabindex values; they create confusing jumps.
- Hide off-screen or disabled items from tab order with inert or tabindex=“-1” as appropriate.
15) Don’t overuse ARIA
Native semantics beat ARIA. If you must use ARIA, follow the rules: no aria-* on a role that forbids it, ensure required child roles, and keep states in sync.
- Use button, summary/details, dialog, form controls, lists, and headings before building with divs.
- Reference the WAI-ARIA Authoring Practices when creating custom widgets.
A 10-minute accessibility audit
- Can I fully use the site with only a keyboard?
- Is focus visible at all times?
- Is there a skip link to main content?
- Does each page have one h1 and a logical heading structure?
- Do forms have programmatic labels, hints, and error messages?
- Are interactive elements the correct HTML elements?
- Do text and UI elements pass color contrast?
- Are modals/menus focus-managed and dismissible with Escape?
- Are dynamic updates announced via live regions?
- Is zoom enabled and the page language set?
Helpful tools
- Browser DevTools Accessibility panel
- Automated scanners: axe DevTools, Lighthouse, WAVE
- Screen readers: NVDA (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
- linters: eslint-plugin-jsx-a11y (React), stylelint-a11y
- Color contrast checkers (e.g., APCA/WCAG contrast tools)
Bake these practices into your components and CI to prevent regressions. Small, consistent habits deliver outsized impact for real people using your product every day. error saving Accessibility Tips Most Developers Forget