How Do You Create Accessible Focus Indicators for WCAG 2.4.7?
WCAG 2.4.7 Focus Visible requires that any keyboard-operable user interface has a visible focus indicator showing which element currently has focus. This seemingly simple requirement is one of the most frequently failed WCAG criteria: WebAIM's 2024 analysis of one million home pages found that 78% had detectable focus indicator issues. For the estimated 8 million Americans who rely on keyboard navigation due to motor impairments, missing focus indicators make websites completely unusable. This guide covers everything you need to know about implementing accessible focus indicators.
Key Takeaways
Focus visibility is fundamental to keyboard accessibility. Here are the critical points:
- Every interactive element must have a visible focus indicator when navigated via keyboard
- The default browser focus outline should never be removed without providing an equivalent or better replacement
- WCAG 2.4.11 (Focus Appearance, Level AAA) specifies minimum size and contrast for focus indicators
- Focus indicators must work on all backgrounds (light, dark, images) your component may appear on
- The `:focus-visible` CSS pseudo-class helps show focus only for keyboard users, improving both accessibility and aesthetics
Understanding WCAG 2.4.7 Focus Visible
The Requirement Explained
The official WCAG 2.4.7 requirement states:
> "Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible."
This means:
- Every focusable element must show a visible indicator when focused
- The indicator must be visible to sighted keyboard users
- Focus must be trackable as users Tab through the interface
The criterion is Level AA, making it a baseline requirement for most accessibility conformance targets.
Why Focus Visibility Matters
Users who navigate with keyboards include:
- People with motor impairments who cannot use a mouse
- Screen reader users who navigate via keyboard commands
- Power users who prefer keyboard efficiency
- Users with temporary limitations (broken arm, RSI, holding a baby)
- People using alternative input devices that emulate keyboard input
Without visible focus, these users cannot tell:
- Which element is currently selected
- Where they are on the page
- What will happen if they press Enter or Space
- How far they've progressed through a form
The Problem: Removed Outlines
The most common focus visibility failure is developers removing the default browser outline:
/* NEVER DO THIS without providing a replacement */
*:focus {
outline: none;
}
/* Or these variations */
button:focus,
a:focus,
input:focus {
outline: 0;
}This pattern became widespread because:
- Default browser outlines don't match design systems
- Mouse users find outlines on clicked elements jarring
- CSS resets sometimes include `outline: none`
- Designers request outline removal without understanding the impact
Creating Effective Focus Indicators
CSS Focus Techniques
Basic outline replacement:
/* Replace default with custom outline */
:focus {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* Remove only when replacing */
button:focus {
outline: none;
box-shadow: 0 0 0 3px #0066cc;
}High-contrast double ring (works on any background):
.focus-universal:focus {
outline: 2px solid #ffffff;
outline-offset: 2px;
box-shadow: 0 0 0 4px #000000;
}
/* Inverted for dark backgrounds */
.focus-dark:focus {
outline: 2px solid #000000;
outline-offset: 2px;
box-shadow: 0 0 0 4px #ffffff;
}Using :focus-visible for keyboard-only indicators:
/* Show focus only for keyboard navigation */
button:focus {
outline: none;
}
button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* Fallback for browsers without :focus-visible support */
@supports not selector(:focus-visible) {
button:focus {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
}Focus Indicator Design Principles
1. Sufficient Contrast
The focus indicator should have at least 3:1 contrast against:
- The unfocused state of the component
- Adjacent background colors
/* Good: High contrast indicator */
.button:focus-visible {
outline: 3px solid #005fcc; /* 4.5:1 against white bg */
}
/* Bad: Low contrast indicator */
.button:focus-visible {
outline: 3px solid #a0c4ff; /* 2.1:1 against white bg */
}2. Sufficient Size
WCAG 2.4.11 (Level AAA) recommends focus indicators with:
- Minimum thickness of 2 CSS pixels
- Minimum area of the perimeter times 2 pixels
/* Meets size recommendations */
.input:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Larger for better visibility */
.button-primary:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 3px;
}3. Distinct from Other States
Focus should be distinguishable from hover, active, and selected states:
.interactive-element {
background: #ffffff;
border: 2px solid #0066cc;
}
.interactive-element:hover {
background: #e6f0ff;
}
.interactive-element:active {
background: #cce0ff;
}
.interactive-element:focus-visible {
outline: 3px solid #ff6600; /* Different color from border */
outline-offset: 2px;
}
.interactive-element.selected {
background: #0066cc;
color: #ffffff;
}Component-Specific Focus Styles
Buttons:
.button {
padding: 0.75rem 1.5rem;
background: #0066cc;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 3px;
}
/* Ghost button needs different approach */
.button-ghost {
background: transparent;
border: 2px solid #0066cc;
color: #0066cc;
}
.button-ghost:focus-visible {
outline: 3px solid #ff6600;
outline-offset: 2px;
}Form Inputs:
.text-input {
padding: 0.5rem;
border: 1px solid #767676;
border-radius: 4px;
}
.text-input:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.25);
}
/* Checkbox with custom focus */
.checkbox-custom:focus-visible + .checkbox-label::before {
outline: 3px solid #0066cc;
outline-offset: 2px;
}Links in Text:
a {
color: #0066cc;
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
border-radius: 2px;
}
/* Alternative: Background highlight */
a:focus-visible {
outline: none;
background-color: #ffff00;
box-shadow: 0 0 0 2px #ffff00;
}Cards and Larger Interactive Areas:
.card-link {
display: block;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
text-decoration: none;
color: inherit;
}
.card-link:focus-visible {
outline: 3px solid #0066cc;
outline-offset: -3px; /* Inside the card */
}
/* Alternative: Inset shadow */
.card-link:focus-visible {
outline: none;
box-shadow: inset 0 0 0 3px #0066cc;
}Focus Management with JavaScript
Programmatic Focus Control
When dynamically updating the page, manage focus appropriately:
// After opening a modal
function openModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.add('open');
// Move focus to first focusable element or modal itself
const focusable = modal.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable) {
focusable.focus();
} else {
modal.setAttribute('tabindex', '-1');
modal.focus();
}
}
// After closing a modal, return focus to trigger
function closeModal(modalId, triggerId) {
const modal = document.getElementById(modalId);
const trigger = document.getElementById(triggerId);
modal.classList.remove('open');
trigger.focus();
}Skip Links
Provide a way to bypass navigation for keyboard users:
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav><!-- Navigation --></nav>
<main id="main-content" tabindex="-1">
<!-- Main content -->
</main>
</body>.skip-link {
position: absolute;
top: -100px;
left: 0;
padding: 1rem;
background: #0066cc;
color: #ffffff;
z-index: 1000;
}
.skip-link:focus {
top: 0;
}Focus Trap for Modals
Keep focus within modal dialogs:
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', function(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
});
}How to Test for WCAG 2.4.7 Focus Visible
Manual Keyboard Testing
The most reliable test method:
- Start at the browser address bar
- Press Tab repeatedly to move through the page
- For each Tab press, verify:
- Focus is visible on an element - The focused element is the one that should be focused - Focus order follows a logical sequence
- Press Shift+Tab to verify reverse navigation
- Test all interactive elements: buttons, links, form fields, menus
Testing Checklist
## Focus Visibility Audit
### Header/Navigation
- [ ] Skip link visible when focused
- [ ] Logo link has focus indicator
- [ ] Navigation links have focus indicators
- [ ] Dropdown menus show focus on items
- [ ] Search input shows focus state
### Main Content
- [ ] All links show focus
- [ ] Buttons show focus
- [ ] Form fields show focus
- [ ] Custom components show focus
### Footer
- [ ] Footer links show focus
### Modals/Dialogs
- [ ] Focus moves to modal when opened
- [ ] Focus trapped within modal
- [ ] Focus returns to trigger when closed
### Complex Components
- [ ] Tab panels show focus correctly
- [ ] Accordions show focus
- [ ] Carousels navigable with focus visibleAutomated Testing
// Using axe-core
axe.run({
runOnly: {
type: 'rule',
values: ['focus-visible']
}
}).then(results => {
console.log('Focus visibility issues:', results.violations);
});# Using pa11y CLI
pa11y https://example.com --include-warnings | grep -i focus
# Using Lighthouse
lighthouse https://example.com --only-categories=accessibilityBrowser Developer Tools
Chrome DevTools:
- Open DevTools > Rendering tab
- Enable "Emulate a focused page"
- Use Elements panel to inspect `:focus` styles
Firefox:
- Open DevTools > Accessibility tab
- Use Tab key while watching the accessibility tree
- Check for focus indicators in the tree view
How to Fix WCAG 2.4.7 Focus Visible Issues
Finding Hidden Focus Removals
Search your codebase for problematic patterns:
# Find outline:none declarations
grep -r "outline:\s*none" --include="*.css" --include="*.scss"
grep -r "outline:\s*0" --include="*.css" --include="*.scss"
# Find in JavaScript (inline styles)
grep -r "outline.*none" --include="*.js" --include="*.jsx"Systematic Remediation
Step 1: Create focus utility classes
/* Focus utility system */
.focus-ring:focus-visible {
outline: 3px solid var(--focus-color, #0066cc);
outline-offset: 2px;
}
.focus-ring-inset:focus-visible {
outline: 3px solid var(--focus-color, #0066cc);
outline-offset: -3px;
}
.focus-ring-dark:focus-visible {
--focus-color: #ff9500;
}Step 2: Replace outline:none with proper styles
/* Before */
.button:focus {
outline: none;
}
/* After */
.button:focus {
outline: none;
}
.button:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}Step 3: Handle complex backgrounds
/* For components on variable backgrounds */
.button-on-image:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
box-shadow:
0 0 0 2px #ffffff,
0 0 0 5px #000000;
}Step 4: Test each component
// Focus testing utility
function testFocusVisibility() {
const focusables = document.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusables.forEach((el, i) => {
setTimeout(() => {
el.focus();
const styles = window.getComputedStyle(el);
const outline = styles.outline;
const boxShadow = styles.boxShadow;
if (outline === 'none' && boxShadow === 'none') {
console.warn('No focus indicator:', el);
}
}, i * 500);
});
}Frequently Asked Questions
Can I use color alone as a focus indicator?
No. Color alone is insufficient because color-blind users may not perceive the change. Focus indicators should include a visible outline, border, or shadow that creates a clear visual boundary around the focused element.
What about :focus vs :focus-visible?
`:focus` activates for all focus events, including mouse clicks. `:focus-visible` only activates when the browser determines focus should be shown (typically keyboard navigation). Use `:focus-visible` for aesthetic reasons, but ensure `:focus` fallback exists for older browsers.
Do decorative images need focus indicators?
If an image is decorative and not interactive, it shouldn't be focusable at all (no `tabindex`, not wrapped in a link). Only interactive elements need focus indicators. Non-interactive elements should not receive keyboard focus.
How do I handle focus on touch devices?
Touch devices typically don't show focus indicators because touch interaction is fundamentally different from keyboard navigation. However, switch access users on touch devices still use focus, so don't completely disable focus styling on mobile.
What if our brand colors don't have enough contrast for focus?
Create a focus-specific color that meets contrast requirements. This doesn't need to match brand colors exactly. Many design systems use high-contrast colors like black, white, or bright accent colors specifically for focus indicators.
Should focus indicators be the same across all browsers?
Consistency across browsers improves user experience. Default browser focus styles vary significantly, so custom focus styles create a more predictable experience. Reset all focus styles and apply your own system-wide.
Related Resources
- WCAG 2.2 Compliance Guide: Everything You Need to Know
- Complete Accessibility Testing Guide for Web Developers
- Best Shopify Accessibility Tool 2025: A Complete Review
This article was crafted using a cyborg approach—human expertise enhanced by AI to deliver comprehensive, accurate, and actionable accessibility guidance.
Stay informed
Accessibility insights delivered
straight to your inbox.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo