/ Accessibility Fixes / Focus Indicators

How to Fix Focus Indicators

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.

1. Find the offending CSS

Step 1
Search stylesheets for outline removal
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.
⚠️ Never use blanket * { outline: none } or :focus { outline: none } without replacing with a custom indicator. This is the #1 accessibility regression in modern CSS.

2. Replace with focus-visible

: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.

3. Hit WCAG 2.4.7 visibility requirements

Size and offset

:focus-visible {
  outline: 2px solid currentColor;  /* at least 2px */
  outline-offset: 2px;               /* gap between control and outline */
}

Contrast

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.

Visibility against all backgrounds

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 */
}

4. Style by element type

Buttons and links

button:focus-visible,
a:focus-visible {
  outline: 2px solid #7c3aed;
  outline-offset: 2px;
  border-radius: 4px;
}

Form inputs

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid #7c3aed;
  outline-offset: 1px;
  border-color: #7c3aed; /* also change border colour */
}

Custom interactive elements (with role)

[role="button"]:focus-visible,
[role="link"]:focus-visible,
[tabindex="0"]:focus-visible {
  outline: 2px solid #7c3aed;
  outline-offset: 2px;
}

5. Skip-link pattern

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>

6. Test by keyboard

Step 1
Tab through every page
Put your mouse away. Use Tab/Shift+Tab/Enter only. Try to use every feature of the site:
  • Open menus, navigate dropdowns
  • Fill forms, submit them
  • Open modals, close them
  • Add items to cart, checkout
At every step, you should see a clear focus indicator. If you ever lose track of where focus is, the indicator failed there.
Step 2
Tab order check
While you're tabbing, also check tab ORDER matches visual order. Skipping around (down then back up) signals layout/DOM disconnect that hurts comprehension.

7. Automated checks

Step 1
axe-core / Playwright accessibility tests
// 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.
💡 If you maintain a design system, define the focus style once at the system level. Every consumer of the system gets accessible focus for free. Defining per-component is how you end up with 12 different focus styles, half of them failing WCAG.

♿ Re-run the Accessibility audit

Verify focus-indicator findings are cleared after the CSS update.

Run Accessibility Audit →
Related Guides: Accessibility Fixes  ·  Fix Keyboard Nav  ·  Fix Colour Contrast  ·  Accessibility Guide
💬 Got a problem?