Designing Visible Focus Indicators Instead of outline:none
AI generated
A11Y
WCAG
Accessibility · WCAG · Keyboard Navigation · CSS
Designing Visible Focus Indicators
instead of outline:none with no replacement

Using outline:none without a replacement makes it practically impossible for keyboard users to operate a page, because it stays invisible which element is currently active. With :focus-visible, high-contrast focus rings and clear WCAG criteria, this common mistake can be fixed precisely, without sacrificing visual calm for mouse users.

13 min. read focus-visible · WCAG 2.4.7 · WCAG 2.4.11 CSS · Hyvä Theme · Tailwind

1. Why outline:none is one of the most damaging accessibility mistakes

Hardly any single line of CSS causes as much damage in practice as outline: none or outline: 0 without a working replacement. Browsers draw a focus outline by default so that keyboard users, screen reader users with residual vision, and people with motor impairments can always recognize which element is currently active. When that outline is removed without an equivalent visual replacement, exactly these user groups lose all orientation on the page. A click on the wrong button, or a form that cannot be submitted because it is unclear which field is currently active, are direct consequences.

The mistake usually stems from purely aesthetic reasons: the browser's blue focus outline does not match the design, so it gets removed broadly via a CSS reset, often through *:focus { outline: none; }. That rule then affects every focusable element on the entire page, including ones nobody consciously styled. This is exactly why the mistake shows up so often as a critical finding in accessibility audits: it is easy to miss because it does not stand out with a mouse, yet it blocks usage for everyone who navigates exclusively by keyboard.

The good news is that the problem can be fully solved without giving up thoughtful visual design. Instead of removing the focus outline entirely, replace it with a custom, deliberately designed indicator that fits the brand while still meeting all contrast requirements. The following sections show how to achieve this with :focus-visible, clear contrast values, and practical CSS.

2. What WCAG 2.4.7 and 2.4.11 actually require

Success Criterion 2.4.7 Focus Visible in WCAG 2.1 requires that any user interface operable via keyboard also has a visible indicator showing which element currently has focus. The requirement is deliberately open and does not prescribe a specific look, but it rules out an invisible focus state. At Level AA this criterion is mandatory for most commercial websites, and it is checked first in practically every accessibility audit because it becomes obvious with a simple tab pass through the page.

WCAG 2.2 added the stricter Success Criterion 2.4.11 Focus Not Obscured (Minimum): the focus indicator must not be completely hidden by other content such as sticky headers, cookie banners, or chat widgets. A focused element that disappears behind a fixed header violates this criterion, even if the focus ring itself is styled correctly. In addition, Success Criterion 1.4.11 Non-text Contrast defines the concrete contrast requirement: the focus indicator must reach a contrast ratio of at least 3:1 against the adjacent colors, in both the focused and unfocused state.

3. focus-visible: focus rings only for keyboard users

A legitimate objection to visible focus rings is that every mouse click on a button produces a thick outline, even though it is already visually obvious which element is active. This exact problem is solved by the CSS pseudo-class :focus-visible. The browser decides heuristically whether a focus event likely originated from keyboard input, such as Tab, or from pointer input like a mouse click. On keyboard focus, :focus-visible applies, whereas on a simple mouse click on a button it typically does not, while form fields such as text inputs still show the ring on mouse click too, because input continues immediately via the keyboard there.

The safe pattern for a clean CSS setup is: never remove :focus entirely, instead style it neutrally at first or defer to :focus-visible, and for browsers without native support use the WICG focus-visible.js polyfill. It is important not to confuse :focus-visible with :focus: styling only :focus-visible while leaving :focus entirely on outline: none loses every visible indicator in older browsers without support. The safe route combines both selectors with a fallback value.


/* Safe baseline: fallback for old browsers, clean ring for keyboard */
a, button, input, select, textarea, [tabindex] {
  /* Never remove entirely, only neutralize as a base */
  outline-offset: 2px;
}

/* Fallback for browsers without :focus-visible support */
a:focus, button:focus, input:focus, select:focus, textarea:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* Precise: ring only on keyboard focus, not on mouse click */
a:focus:not(:focus-visible),
button:focus:not(:focus-visible) {
  outline: none;
}

a:focus-visible, button:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
  border-radius: 4px;
}

4. Meeting contrast requirements for focus indicators

A focus ring that is technically visible but only reaches a contrast ratio of 1.5:1 against the background formally satisfies WCAG 2.4.7 but practically fails 1.4.11 Non-text Contrast. The minimum requirement of 3:1 applies to the focus indicator itself against the immediately adjacent colors, in both states: the ring must stand out against the background and additionally against the element it frames. A dark blue ring on a dark blue button background typically fails this requirement, even if the ring has sufficient contrast against the page overall.

In practice, a two-color approach works well: a bright, thin outline directly at the element edge combined with an additional box-shadow at a larger radius in a contrasting color. This combination works reliably on both light and dark backgrounds, because at least one of the two colors always provides sufficient contrast against the respective surface. Tools such as the WebAIM Contrast Checker or the Chrome DevTools contrast check in the Accessibility panel calculate the actual contrast ratio between ring color and background color and immediately show whether 3:1 is reached.


/* Two-color focus ring: works on both light and dark surfaces */
.btn-primary:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px #18181b;
}

/* Check contrast ratio: ring color vs. background color >= 3:1 */
.card--dark {
  background-color: #18181b;
}

.card--dark a:focus-visible {
  /* Light ring on dark background: guaranteed high contrast */
  outline: 2px solid #f4f4f5;
  outline-offset: 3px;
}

.card--light a:focus-visible {
  /* Dark ring on light background */
  outline: 2px solid #18181b;
  outline-offset: 3px;
}

5. Designing your own focus ring instead of the browser default

The browser's default focus outline is functionally correct but rarely fits an existing design system visually. Instead of removing it entirely, it is worth investing in a consistent, on-brand focus style that works identically across every interactive element. CSS custom properties help here: a single variable like --focus-ring-color defined centrally lets you maintain the ring's color and strength project-wide from one place, instead of duplicating it in every component.

Also important is outline-offset: a ring sitting directly at the element's edge with no gap tends to visually merge with the element itself on rounded corners or tight layouts. An offset of 2 to 4 pixels creates visible space between element and ring and noticeably improves perceivability without claiming extra layout space, since outline, unlike border, has no effect on box-model flow. On elements with overflow: hidden on the parent, it is important to check that the ring is not clipped, since this is a commonly overlooked bug.


/* Central focus variables for the entire design system */
:root {
  --focus-ring-color: #18181b;
  --focus-ring-color-on-dark: #f4f4f5;
  --focus-ring-width: 2px;
  --focus-ring-offset: 3px;
}

/* Reusable utility class instead of duplicating per component */
.focus-ring:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset: var(--focus-ring-offset);
  border-radius: 0.375rem;
}

/* Automatically switch context on a dark background */
.bg-dark .focus-ring:focus-visible {
  outline-color: var(--focus-ring-color-on-dark);
}

/* overflow:hidden on the parent element clips rings: avoid this */
.card {
  overflow: visible; /* instead of hidden, when focusable children are present */
  padding: var(--focus-ring-offset);
}

6. Focus styles for buttons, links, forms and custom components

Native HTML elements such as <button>, <a href>, and <input> receive keyboard focus automatically because the browser includes them in the native tab order. Custom components such as a dropdown built from a <div>, or a card component with a click handler, lack this behavior entirely unless tabindex="0" is set explicitly and a matching ARIA role such as role="button" is assigned. Anyone misusing a <div> with onclick as a button must additionally retrofit tabindex, keyboard event handlers for Enter and Space, and a focus ring by hand, all of which native buttons provide for free.

Form fields deserve special attention because validation errors often use additional visual signals such as a red border. The focus ring must not collide with or obscure the error state: both states need to remain simultaneously recognizable, for example through different ring radii or a combination of border color and outline. In custom select components, which are frequently built with Alpine.js or similar libraries, the focus ring must appear on the visible trigger element, not on the often visually hidden native <select> underneath.


<!-- Custom dropdown trigger: retrofit keyboard accessibility and focus ring manually -->
<div
    x-data="{ open: false }"
    class="relative inline-block"
>
  <button
      type="button"
      class="focus-ring inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-zinc-300 bg-white text-sm font-medium"
      :aria-expanded="open"
      aria-haspopup="listbox"
      @click="open = !open"
      @keydown.escape="open = false"
  >
    Choose sorting
  </button>

  <ul
      x-show="open"
      x-cloak
      role="listbox"
      class="absolute mt-1 w-56 bg-white border border-zinc-200 rounded-lg shadow-lg py-1"
  >
    <li role="option" tabindex="0"
        class="focus-ring px-4 py-2 text-sm cursor-pointer hover:bg-zinc-50">
      Price ascending
    </li>
    <li role="option" tabindex="0"
        class="focus-ring px-4 py-2 text-sm cursor-pointer hover:bg-zinc-50">
      Price descending
    </li>
  </ul>
</div>

7. Focus management in Hyvä themes and Alpine.js

Hyvä themes replace jQuery and Knockout.js with Alpine.js, which brings both opportunities and new duties of care for focus management. Modal dialogs such as the mini-cart or the search overlay must actively move focus to the first focusable element inside the dialog when it opens, and return it to the triggering element when it closes. Without this focus trapping, keyboard focus jumps into nothing when an overlay opens, or stays on an element in the background that is visually obscured by the overlay, which is extremely confusing for keyboard users.

Alpine.js offers a direct solution with the official @alpinejs/focus plugin: the x-trap directive keeps focus trapped inside an open element as long as a condition is true, and automatically restores the previous focus on close. In standard Hyvä components such as the mini-cart slideout or the mobile menu, this pattern is often already prepared, but it should still be verified explicitly for every custom overlay component, since missing focus trapping regularly shows up as a critical finding in accessibility audits.


<!-- Hyva phtml: mini-cart overlay with focus trapping via the Alpine Focus plugin -->
<div x-data="{ open: false }">
  <button
      type="button"
      class="focus-ring"
      @click="open = !open"
      aria-haspopup="dialog"
      :aria-expanded="open"
  >
    Open cart
  </button>

  <div
      x-show="open"
      x-trap.inert.noscroll="open"
      role="dialog"
      aria-modal="true"
      aria-label="Shopping cart"
      @keydown.escape.window="open = false"
      class="fixed inset-y-0 right-0 w-full max-w-md bg-white shadow-xl"
  >
    <button type="button" class="focus-ring" @click="open = false">
      Close
    </button>
    <!-- Cart content -->
  </div>
</div>

8. Testing focus indicators, manually and automated

The fastest manual test for visible focus indicators needs no software: set the mouse aside, navigate through the entire page with the Tab key, and check at every stop whether it is clearly recognizable which element is active. Particularly important is checking transitions between components, for example from the last link in the navigation to the first element in the main content, as well as areas with a dark background, where light default rings tend to disappear quickly. A second pass with Shift+Tab backward additionally uncovers cases where the tab order does not match the visual order.

For automated checks, axe-core works well, running as a browser extension or in CI pipelines via @axe-core/playwright or jest-axe. axe-core reliably detects missing focus indicator contrast and elements without a visible focus style, but it cannot automatically verify whether focus is set correctly on dynamic overlays. For these dynamic cases, the manual keyboard test remains indispensable. Lighthouse and the Chrome DevTools accessibility check further complement the focus ring contrast measurement with a visual overview.

9. Focus patterns in direct comparison

A pattern emerges across all the previous sections: broken focus patterns almost always stem from the same few causes, usually pure aesthetics or a lack of awareness about the contrast requirement. The following overview summarizes the most common pitfalls and the respective recommended fix, so new components can orient toward the right pattern from the start instead of repeating mistakes that keep surfacing in audits anyway.

The effort required for a clean focus pattern is always low, usually just a few lines of CSS and one plugin directive, but the impact on actual usability is substantial. Once these patterns are established project-wide as a utility class or CSS custom property, there is no need to reinvent them for every new component.

Task Broken pattern Recommended focus pattern Benefit
Removing the focus outline *:focus { outline: none; } :focus-visible with a custom ring Visible for keyboard, calm for mouse
Ring contrast 1px, pale gray on white 2px, contrast ratio ≥ 3:1 against background Meets WCAG 1.4.11
Custom dropdown <div onclick> without tabindex tabindex, role, focus ring, keyboard handlers Fully operable by keyboard
Opening an overlay Focus stays in the background x-trap moves focus into the overlay No loss of orientation
Sticky header Focused element gets obscured scroll-margin-top on the focus target Meets WCAG 2.4.11

Mironsoft

Accessibility, WCAG audits and Hyvä theme customization for Magento stores

Focus indicators every user can actually see?

We audit your focus order, contrast values, and overlay components against WCAG 2.4.7, 2.4.11, and 1.4.11, and implement consistent, on-brand focus rings directly in your Hyvä theme.

Focus audit

Manual keyboard tests and axe-core analysis against WCAG 2.4.7 and 1.4.11

CSS implementation

Central focus utility classes using :focus-visible with sufficient contrast

Focus trapping

Setting up Alpine.js x-trap correctly for mini-cart, menus, and modal dialogs

10. Summary

Visible focus indicators are not an optional design detail, but a basic requirement for the usability of any website for keyboard and assistive technology users. outline: none without a replacement violates WCAG 2.4.7 and makes the page effectively unusable for keyboard users. The solution is not to give up design control, but to build a deliberately designed focus ring that uses :focus-visible to appear specifically on keyboard focus and reaches at least a 3:1 contrast ratio against the background.

For custom components like dropdowns and overlays, focus management adds a second layer: tabindex, matching ARIA roles, and in Hyvä themes the Alpine.js Focus plugin with x-trap, all ensure that focus is set correctly when a dialog opens and reliably restored when it closes. Regular keyboard tests, complemented by automated tools such as axe-core, catch regressions early, before they turn into real barriers in production.

Designing Visible Focus Indicators, The Essentials at a Glance

Never remove without replacement

outline: none without a working replacement violates WCAG 2.4.7 and blocks keyboard users completely.

Use focus-visible

Shows the ring specifically on keyboard focus, keeping the interface visually calm for mouse users.

Contrast at least 3:1

WCAG 1.4.11 requires sufficient contrast of the ring against both the background and the element.

Focus trapping in overlays

Alpine.js x-trap sets and correctly restores focus in dialogs and the mini-cart.

11. FAQ: Designing Visible Focus Indicators

1Why is outline:none without a replacement an accessibility mistake?
The focus outline is the only visual information keyboard users get about which element is active. Without a replacement, they lose all orientation and cannot reliably operate forms or navigation.
2What does WCAG 2.4.7 Focus Visible actually require?
Every keyboard-operable element needs a visible focus indicator. The look is not prescribed, but complete invisibility violates the criterion at Level AA.
3Difference between :focus and :focus-visible?
:focus applies on every focus event, including mouse clicks. :focus-visible applies heuristically only on likely keyboard input, avoiding the ring on plain clicks.
4What contrast ratio does a focus ring need to meet?
At least 3:1 against adjacent colors per WCAG 1.4.11, both against the background and against the framed element.
5What is focus trapping and when do I need it?
Keeps focus contained inside an open overlay so it does not jump into the obscured background layout. Alpine.js handles this with x-trap from the Focus plugin.
6How does focus management work for custom dropdowns?
tabindex=0, matching ARIA role such as role=listbox, manual keyboard handlers for Enter and arrow keys, and a custom focus ring, since native semantics do not apply here.
7What does WCAG 2.4.11 Focus Not Obscured additionally require?
A focused element must not be completely hidden by sticky headers or banners. scroll-margin-top on focusable elements reliably prevents this.
8How do I test focus indicators without special software?
Set the mouse aside, navigate with Tab and Shift+Tab, and check at every element whether it is clear what is active. Pay special attention to transitions and dark areas.
9Can axe-core automatically detect missing focus indicators?
axe-core reliably detects missing ring contrast and elements without a focus style. Dynamic focus management on overlays still needs manual testing.
10Does a visible focus ring negatively affect the visual design?
Not necessarily: :focus-visible shows the ring only on keyboard focus, and CSS custom properties adapt color and shape precisely to your own design system.