WCAG-compliant focus indicators with Tailwind CSS
A focus ring is to keyboard users what the mouse cursor is to everyone else: the only way to see where they currently are. With the ring-* utilities, focus-visible and ring-offset, Tailwind CSS builds focus indicators that stay visible instead of disappearing behind outline-none.
Table of Contents
- 1. Why a visible focus ring determines accessibility
- 2. WCAG 2.4.7, 2.4.11 and 1.4.11 in detail
- 3. The outline-none antipattern and its consequences
- 4. Designing focus rings with the ring-* utilities
- 5. focus: versus focus-visible: the right selector
- 6. ring-offset for focus rings on colored backgrounds
- 7. Focus rings for custom components and interactive divs
- 8. Testing focus rings: keyboard, contrast, automation
- 9. Focus ring strategies compared
- 10. Summary
- 11. FAQ
1. Why a visible focus ring determines accessibility
Anyone who operates a website exclusively with the keyboard, whether due to motor impairments, a visual impairment, or simply efficiency, relies entirely on the focus ring. Without this visible indicator there is no feedback about which element is currently active. A mouse click provides instant visual feedback through hover states and the cursor, the keyboard has only the focus ring as an equivalent. Without it, every form input, every navigation step, every button click becomes a guessing game.
In practice, the focus ring is therefore not a cosmetic detail but a functional necessity. Design teams often remove it for purely aesthetic reasons, because the browser default focus ring clashes with the rest of the design. This is exactly where Tailwind CSS comes in: with the ring-* utilities, a custom, designed focus ring can be built that fits the brand while still meeting WCAG requirements. This article shows how a well-considered focus ring design comes together, which pitfalls exist, and how to test it.
2. WCAG 2.4.7, 2.4.11 and 1.4.11 in detail
The success criterion WCAG 2.4.7 "Focus Visible" requires that every focusable element on the page receives a visible focus ring as soon as it is reached via the keyboard. It is not enough for a focus ring to exist in theory, it must be genuinely perceivable. WCAG 2.2 added more concrete requirements with 2.4.11 "Focus Appearance": the focus indicator must meet a minimum area and a minimum contrast against the adjacent background, so it does not become practically invisible through a design that is too thin or too pale.
In addition, WCAG 1.4.11 "Non-text Contrast" applies: the focus ring itself counts as a non-text element and must achieve a contrast of at least 3:1 against the adjacent surface. A light blue focus ring on a white background can easily fall short of this value even though it looks visible at first glance. These three criteria together set the bar for every focus ring design: visible, sufficiently large, sufficiently high in contrast, and on every background the element might appear on.
3. The outline-none antipattern and its consequences
The most common accessibility mistake on the web is the global rule * { outline: none; }, or the Tailwind class outline-none without any replacement. It is usually applied to get rid of the browser's default blue focus ring, which visually clashes with the design. The result: no focus ring left, for anyone, on any element. Mouse users never notice, but keyboard users lose the page entirely because all orientation is gone.
The solution is not to remove the focus ring entirely, but to replace it with a deliberately designed equivalent. Tailwind CSS provides the ring-* utilities as a direct replacement for outline. Important: outline-none must never appear without a matching ring-* or outline-* alternative in the same class list. Every code review should specifically check for this pairing, since the antipattern slips in easily and unnoticed, for example through button components copied from older projects.
<!-- WRONG: outline-none without any replacement removes the focus ring entirely -->
<button class="outline-none bg-sky-600 text-white px-4 py-2 rounded-lg">
Submit
</button>
<!-- RIGHT: outline-none paired with a custom ring as the visible focus indicator -->
<button class="outline-none bg-sky-600 text-white px-4 py-2 rounded-lg
focus-visible:ring-2 focus-visible:ring-sky-400
focus-visible:ring-offset-2 focus-visible:ring-offset-white">
Submit
</button>
<!-- ALSO VALID: keep the native outline, just restyle it -->
<button class="bg-sky-600 text-white px-4 py-2 rounded-lg
focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-sky-400">
Submit
</button>
4. Designing focus rings with the ring-* utilities
The ring-* utilities in Tailwind CSS create a focus ring via box-shadow, not via the native outline property. This has a decisive advantage: a box-shadow-based focus ring follows the element's border-radius exactly, whereas the native outline stays rectangular in some browsers even when the button has rounded corners. ring-2 creates a two pixel wide ring, ring-sky-400 sets the color, ring-offset-2 pushes the ring two pixels away from the element so it does not merge visually with the element's own background.
In Tailwind CSS v4, default values for ring width and ring color can be defined centrally via the @theme directive, so not every component has to repeat ring-2 ring-sky-400 individually. A central focus ring token prevents inconsistencies between buttons, links and form fields and makes later adjustments possible in a single place instead of dozens of component files.
/* Tailwind v4: central focus ring tokens via @theme */
@import "tailwindcss";
@theme {
--ring-color-default: var(--color-sky-400);
--ring-offset-color-default: var(--color-white);
}
/* Reusable focus ring utility for all interactive elements */
@layer utilities {
.focus-ring {
@apply outline-none focus-visible:ring-2 focus-visible:ring-sky-400
focus-visible:ring-offset-2 focus-visible:ring-offset-white;
}
}
5. focus: versus focus-visible: the right selector
A common design mistake is using focus: instead of focus-visible: for the focus ring. The focus: modifier reacts to any focus, even when an element was focused by a mouse click. This leads to distracting focus rings on every mouse click on a button, which often tempts design teams to remove the ring entirely instead of choosing the right selector.
focus-visible: uses the native CSS pseudo-class :focus-visible, which the browser itself decides on: it fires on keyboard focus, and generally not on pure mouse focus. The result is a focus ring that appears exactly when it is needed, namely during keyboard navigation, without disturbing the visual calm for mouse users. For form fields, focus-within: is also relevant when a wrapping element such as a label wrapper should visually react to a child element's focus.
6. ring-offset for focus rings on colored backgrounds
A focus ring placed directly at the edge of a button merges visually when the ring color and the button background are too similar. ring-offset-2 solves this by creating a small gap between the element and the ring, filled with the color defined via ring-offset-color. On a white background, ring-offset-white is enough, on dark sections ring-offset-slate-900 or a matching dark variant must be set, otherwise the gap looks like a bright patch that creates its own contrast problems.
In multi-colored layouts, such as cards with different background colors in a grid, it becomes necessary to adjust ring-offset-color per section instead of using a single global value. A systematic approach defines a matching offset color per background color as a Tailwind variant, so the focus ring remains equally readable on every section of the page, regardless of whether the card is styled light or dark.
7. Focus rings for custom components and interactive divs
Native elements like <button> and <a> are focusable by default and automatically receive a focus ring from the browser. As soon as a custom component is built on a <div> with role="button", for example for a custom dropdown or a clickable card, tabindex="0" must be set so the element even enters the tab order at all. Without tabindex, even the most beautifully styled focus ring is useless, because the element cannot be reached by keyboard in the first place.
In addition, such a custom component needs its own keyboard event handlers for Enter and Space, since native buttons provide this interaction automatically while a <div> does not. The focus ring itself is applied identically via focus-visible:ring-2, regardless of whether the element is a native button or a custom component. Wherever possible, a native element remains the more robust choice, because keyboard semantics, ARIA role and focus ring then interact correctly by default.
<!-- Custom component built on a div needs tabindex plus keyboard handlers -->
<div
role="button"
tabindex="0"
class="cursor-pointer bg-white border border-slate-200 rounded-xl p-4
outline-none focus-visible:ring-2 focus-visible:ring-sky-400
focus-visible:ring-offset-2 focus-visible:ring-offset-white"
x-on:keydown.enter="open()"
x-on:keydown.space.prevent="open()"
x-on:click="open()"
>
<p class="font-semibold text-slate-800">Open product card</p>
</div>
8. Testing focus rings: keyboard, contrast, automation
Manual testing remains the most reliable way to verify a focus ring design: put the mouse aside completely and operate the entire page using only Tab, Shift+Tab, Enter and the arrow keys. Every interactive element must receive a clearly visible focus ring in a logical, comprehensible order. If focus becomes invisible anywhere or jumps unexpectedly, that is a direct sign of a missing or incorrectly configured focus ring style.
For contrast, a contrast checker such as the WebAIM Contrast Tool is worth using, applied to the ring color against the adjacent background. Automated tools like axe-core or Lighthouse detect missing tabindex values and some ARIA problems, but only check the visual contrast of a focus ring in a limited way. A Playwright test can at least programmatically verify that an element has a visible box-shadow or outline after keyboard focus, which at least secures the technical presence of the focus ring.
// Playwright: verify a visible focus ring exists after keyboard focus
import { test, expect } from '@playwright/test';
test('button shows a visible focus ring on keyboard focus', async ({ page }) => {
await page.goto('/checkout');
await page.keyboard.press('Tab');
const button = page.locator('button[type="submit"]');
await button.focus();
const boxShadow = await button.evaluate(
(el) => getComputedStyle(el).boxShadow
);
// A real Tailwind ring produces a non-empty box-shadow value
expect(boxShadow).not.toBe('none');
});
9. Focus ring strategies compared
Not every focus ring strategy is equally robust. The following overview shows which approaches work in practice and which typical problems arise when teams take shortcuts in focus ring design.
| Approach | Problem | Recommended focus ring pattern | Benefit |
|---|---|---|---|
| outline-none with no replacement | No focus ring for anyone | outline-none focus-visible:ring-2 |
Visible ring is preserved |
| focus: instead of focus-visible: | Ring appears on mouse click too | focus-visible:ring-2 |
Ring only on keyboard use |
| Ring without offset | Ring merges with the element | ring-offset-2 ring-offset-white |
Ring stays clearly separated |
| Custom div without tabindex | Element unreachable by keyboard | tabindex="0" plus keydown handlers |
Element becomes focusable |
| Pale ring color | Contrast below 3:1 (WCAG 1.4.11) | Strong accent color, checked with a contrast tool | Ring visible on every background |
This table makes clear that a good focus ring design rarely depends on a single class, but on the combination of selector, color, offset and keyboard reachability. Anyone who implements all five points consistently gets a focus ring that works on every element and every background of the application, without needing exceptions for individual components.
Mironsoft
Tailwind CSS, accessibility and WCAG-compliant frontend development
Focus ring design that works for every user?
We audit existing components for missing or incorrectly configured focus rings and build a consistent, WCAG-compliant focus system with Tailwind CSS that fits the brand and never excludes keyboard users.
Focus ring audit
Systematic review of every interactive element for a visible focus ring
Design system integration
Central focus ring tokens instead of repeated classes in every component
WCAG evidence
Documented compliance with WCAG 2.4.7, 2.4.11 and 1.4.11
10. Summary
A well-considered focus ring design is the precondition for a Tailwind CSS application to remain usable for keyboard users at all. outline-none without a replacement removes all orientation, ring-* utilities combined with focus-visible instead build a designed but guaranteed visible focus ring. ring-offset ensures the ring stays recognizable on every background instead of merging with the element.
Custom components built on <div> additionally need tabindex="0" and their own keyboard handlers, so the most beautiful focus ring can even be reached in the first place. Manual testing with the keyboard remains indispensable, complemented by contrast checkers and automated tests. Anyone who implements these points consistently meets WCAG 2.4.7, 2.4.11 and 1.4.11 without making compromises on the visual design.
Focus Ring Design with Tailwind CSS - the essentials at a glance
Never remove without a replacement
Use outline-none only together with focus-visible:ring-2 or focus-visible:outline.
focus-visible instead of focus
Shows the focus ring only on keyboard navigation, not on every mouse click.
ring-offset against merging
Set ring-offset-2 with a matching ring-offset-color per background color.
tabindex on custom components
A focus ring is useless if the element cannot be focused at all without tabindex="0".