
:focus-visible: Smart Focus Indicators for Better UX
Focus indicators are crucial for accessibility, but they can look awkward when triggered by mouse clicks. The :focus-visible pseudo-class solves this by showing focus styles only when they’re actually needed.
The Problem with :focus
Traditional :focus applies to all focus states, whether from keyboard, mouse, or touch:
button:focus {
outline: 2px solid blue;
} This creates a poor user experience:
- Mouse users see outlines on every click (looks broken)
- Keyboard users need outlines to navigate (accessibility requirement)
The common anti-pattern was to remove outlines entirely:
/* ❌ NEVER do this - breaks accessibility */
button:focus {
outline: none;
} Enter :focus-visible
:focus-visible applies focus styles only when the browser determines they’re needed (typically keyboard navigation):
/* No focus style for mouse clicks */
button:focus {
outline: none;
}
/* Focus style only for keyboard navigation */
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
} The browser intelligently decides when to show focus indicators based on:
- Input method (keyboard, mouse, touch)
- Element type
- User interaction patterns
Practical Use Cases
1. Button Styling
.button {
padding: 0.75rem 1.5rem;
background: #0066cc;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
/* Remove default focus outline */
.button:focus {
outline: none;
}
/* Show focus only for keyboard users */
.button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 3px;
box-shadow: 0 0 0 5px rgba(0, 102, 204, 0.2);
}
.button:hover {
background: #0052a3;
transform: translateY(-1px);
}
/* Ensure focus style is visible even when hovering */
.button:focus-visible:hover {
outline: 3px solid #0066cc;
} 2. Form Inputs
input,
textarea,
select {
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
/* Always show focus for inputs (they expect it) */
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
/* But enhance for keyboard focus */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
border-color: #0066cc;
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
} 3. Interactive Cards
.card {
padding: 2rem;
background: white;
border-radius: 8px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
/* No outline on click */
.card:focus {
outline: none;
}
/* Clear focus indicator for keyboard */
.card:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
transform: translateY(-4px);
} 4. Navigation Links
.nav-link {
display: block;
padding: 0.75rem 1.5rem;
color: #333;
text-decoration: none;
border-radius: 6px;
transition: all 0.2s;
}
.nav-link:hover {
background: #f5f5f5;
color: #0066cc;
}
/* Remove default focus outline */
.nav-link:focus {
outline: none;
}
/* Keyboard focus gets clear indicator */
.nav-link:focus-visible {
background: #e3f2fd;
color: #0066cc;
box-shadow: inset 0 0 0 2px #0066cc;
} Advanced Techniques
Custom Focus Rings
.custom-button {
position: relative;
/* ... other styles ... */
}
.custom-button:focus {
outline: none;
}
/* Animated focus ring */
.custom-button:focus-visible::before {
content: "";
position: absolute;
inset: -4px;
border: 2px solid #0066cc;
border-radius: inherit;
animation: focusRing 0.3s ease;
}
@keyframes focusRing {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
} Different Styles for Different Contexts
/* Card in grid: subtle focus */
.grid .card:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Card as standalone: more prominent focus */
.standalone-card:focus-visible {
outline: 4px solid #0066cc;
outline-offset: 4px;
box-shadow: 0 0 0 8px rgba(0, 102, 204, 0.1);
} High Contrast Mode Support
.button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Enhance for high contrast mode */
@media (prefers-contrast: high) {
.button:focus-visible {
outline-width: 3px;
outline-offset: 3px;
}
} Real-World Example: Dashboard Interface
/* Card grid */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
padding: 2rem;
}
.dashboard-card {
background: white;
border-radius: 12px;
padding: 2rem;
border: 1px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.dashboard-card:hover {
border-color: #0066cc;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
/* No visible focus on click */
.dashboard-card:focus {
outline: none;
}
/* Clear keyboard focus indicator */
.dashboard-card:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 4px;
z-index: 10;
}
/* Buttons within cards */
.dashboard-card .action-button {
padding: 0.5rem 1rem;
background: #f5f5f5;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
margin-top: 1rem;
}
.dashboard-card .action-button:hover {
background: #0066cc;
color: white;
}
.dashboard-card .action-button:focus {
outline: none;
}
.dashboard-card .action-button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2);
}
/* Icon buttons */
.icon-button {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.icon-button:hover {
background: #f5f5f5;
}
.icon-button:focus {
outline: none;
}
.icon-button:focus-visible {
background: #e3f2fd;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.3);
} Browser Support
:focus-visible has excellent modern browser support:
- Chrome/Edge: ✅ v86+ (October 2020)
- Firefox: ✅ v85+ (January 2021)
- Safari: ✅ v15.4+ (March 2022)
Current global support is ~90%, making it production-ready with polyfills.
Progressive Enhancement
Provide fallbacks for older browsers:
/* Fallback: show focus for all */
.button:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Enhanced: only show for keyboard */
@supports selector(:focus-visible) {
.button:focus {
outline: none;
}
.button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
} Using a Polyfill
For older browsers, use the focus-visible polyfill:
<script src="https://unpkg.com/focus-visible"></script> /* Polyfill adds .focus-visible class */
.button.focus-visible {
outline: 2px solid #0066cc;
} Accessibility Considerations
:focus-visible should enhance, not replace, accessibility:
/* ✅ Good: Provides clear keyboard focus */
button:focus {
outline: none;
}
button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* ✅ Good: Maintains hover indication */
button:hover {
background: #0052a3;
}
/* ⚠️ Careful: Ensure focus is always visible somehow */
button:focus {
outline: none;
/* Make sure :focus-visible is defined! */
} WCAG Compliance
Ensure focus indicators meet WCAG 2.2 requirements:
- Minimum size: 2px outline or equivalent
- Contrast ratio: 3:1 against adjacent colors
- Visible: Don’t hide behind other elements
/* WCAG 2.2 compliant focus indicator */
.button:focus-visible {
outline: 2px solid #0066cc; /* 3:1 contrast */
outline-offset: 2px;
}
/* For high contrast mode */
@media (prefers-contrast: high) {
.button:focus-visible {
outline-width: 3px;
}
} Performance Considerations
:focus-visible is highly performant as a native browser feature:
/* ✅ Performant */
.element:focus-visible {
outline: 2px solid blue;
}
/* ✅ Also performant with transitions */
.element {
transition: box-shadow 0.2s;
}
.element:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.3);
} Common Pitfalls
1. Removing All Focus Styles
/* ❌ DANGEROUS - breaks keyboard navigation */
*:focus {
outline: none;
}
/* ✅ Better - replace with :focus-visible */
*:focus {
outline: none;
}
*:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
} 2. Forgetting :focus-visible
/* ❌ Keyboard users get nothing */
button:focus {
outline: none;
}
/* ✅ Always pair with :focus-visible */
button:focus {
outline: none;
}
button:focus-visible {
outline: 2px solid #0066cc;
} 3. Low Contrast Focus Indicators
/* ❌ Insufficient contrast */
.light-button:focus-visible {
outline: 1px solid #ccc;
}
/* ✅ Meets WCAG requirements */
.light-button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
} Combining with Other Pseudo-Classes
With :hover
.button:hover {
background: #0052a3;
}
/* Ensure focus indicator shows over hover state */
.button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 3px;
}
/* Both hover and keyboard focus */
.button:hover:focus-visible {
outline-color: white; /* Stand out against darker hover background */
} With :active
.button:active {
transform: scale(0.98);
}
/* Maintain focus indicator during active state */
.button:focus-visible:active {
outline: 2px solid #0066cc;
} With :disabled
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* No focus indicator on disabled buttons */
.button:disabled:focus-visible {
outline: none;
} Testing Focus Indicators
Manual Testing
- Keyboard only: Navigate with Tab key only
- Screen reader: Test with NVDA/JAWS/VoiceOver
- High contrast mode: Enable in OS settings
- Zoom: Test at 200% zoom
Automated Testing
// Playwright test
await page.keyboard.press("Tab");
const focusVisible = await page.locator(".button:focus-visible").count();
expect(focusVisible).toBe(1); Real-World Example: Form with Multiple Input Types
.form {
max-width: 500px;
margin: 2rem auto;
}
/* Text inputs */
.form input[type="text"],
.form input[type="email"],
.form input[type="password"],
.form textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 6px;
margin-bottom: 1rem;
transition: all 0.2s;
}
.form input:focus,
.form textarea:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
/* Checkbox and radio */
.form input[type="checkbox"],
.form input[type="radio"] {
width: 20px;
height: 20px;
margin-right: 0.5rem;
}
.form input[type="checkbox"]:focus,
.form input[type="radio"]:focus {
outline: none;
}
.form input[type="checkbox"]:focus-visible,
.form input[type="radio"]:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Buttons */
.form button {
padding: 0.75rem 2rem;
background: #0066cc;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.form button:hover {
background: #0052a3;
}
.form button:focus {
outline: none;
}
.form button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(0, 102, 204, 0.2);
}
/* Select dropdown */
.form select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 6px;
background: white;
cursor: pointer;
}
.form select:focus {
outline: none;
border-color: #0066cc;
}
.form select:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
} Conclusion
:focus-visible is a crucial tool for creating accessible, user-friendly interfaces. It allows you to:
- Provide clear keyboard navigation
- Avoid awkward mouse-click outlines
- Meet WCAG accessibility standards
- Create polished user experiences
With broad browser support and straightforward implementation, there’s no excuse not to use it. Always pair :focus { outline: none; } with a :focus-visible style to ensure keyboard users can navigate your interface effectively.
Browser support snapshot
Live support matrix for css-focus-visible from
Can I Use.
Show static fallback image

Source: caniuse.com









