Keyboard users navigate by tabbing through interactive elements. The focus indicator tells them where they are. Remove or weaken the indicator and the site becomes invisible to keyboard users — and to anyone using assistive tech that relies on focus state. WCAG 2.4.7 makes visible focus indicators a Level AA requirement. The fix is small CSS but the design implications need thought. This guide covers the fix and the modern :focus-visible pattern that satisfies both designers and accessibility.
grep -rE "outline:\s*(none|0)" your-css/Common offenders: legacy CSS resets (Eric Meyer's reset, normalize.css versions before 8.0), button styles that "clean up" the default outline, frameworks that override focus per component.
* { outline: none } or :focus { outline: none } without replacing with a custom indicator. This is the #1 accessibility regression in modern CSS.:focus-visible applies only when the browser thinks focus should be visible — keyboard navigation typically yes, mouse clicks typically no. Modern browsers all support it.
/* GOOD modern pattern */
:focus {
outline: none; /* hide default for everyone */
}
:focus-visible {
outline: 2px solid #1e3a8a;
outline-offset: 2px;
}
Result: mouse clicks don't show an outline (matches most designer intent), keyboard tabs do show one (matches accessibility requirement). Both groups happy.
:focus-visible {
outline: 2px solid currentColor; /* at least 2px */
outline-offset: 2px; /* gap between control and outline */
}
Outline colour needs 3:1 contrast against adjacent colours. For a button on white, outline must be sufficiently dark. For a button on a dark navigation bar, outline must be sufficiently light. currentColor often works — it inherits from the text colour, which usually has good contrast already.
If a button can appear on different background colours, single-colour outline may fail somewhere. Use a doubled outline or shadow for guaranteed visibility:
:focus-visible {
outline: 2px solid #fff;
box-shadow: 0 0 0 4px #000; /* outer ring contrasts against light backgrounds */
}
button:focus-visible,
a:focus-visible {
outline: 2px solid #7c3aed;
outline-offset: 2px;
border-radius: 4px;
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid #7c3aed;
outline-offset: 1px;
border-color: #7c3aed; /* also change border colour */
}
[role="button"]:focus-visible,
[role="link"]:focus-visible,
[tabindex="0"]:focus-visible {
outline: 2px solid #7c3aed;
outline-offset: 2px;
}
Skip links let keyboard users jump past navigation. Visually hidden until focused — a great accessibility pattern.
<a href="#main" class="skip-link">Skip to main content</a>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #1e3a8a;
color: white;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
// Playwright test
import { test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('no a11y violations on homepage', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Runs in CI, catches regressions before merge.
Verify focus-indicator findings are cleared after the CSS update.
Run Accessibility Audit →