:focus-visible, :focus-within and Keyboard Navigation
Good focus management separates web development from accessible web development. :focus-visible shows the focus ring only during keyboard use, :focus-within enables container-level reactions, and together with correct outline styles that follow WCAG they form the foundation for keyboard navigation without compromising visual design.
Table of Contents
- 1. The focus dilemma: visibility versus aesthetics
- 2. :focus vs. :focus-visible: the crucial difference
- 3. Styling outline correctly under WCAG standards
- 4. :focus-within for container states
- 5. Focus traps in modals and dialogs
- 6. Skip links and keyboard navigation
- 7. :has() for focus-dependent layouts
- 8. Focus strategies compared
- 9. Testing and accessibility audits
- 10. Summary
- 11. FAQ
1. The focus dilemma: visibility versus aesthetics
For years, web developers faced an apparent contradiction: the browser's native focus ring, a blue or black outline around focused elements, is indispensable for keyboard users, yet it looks visually disruptive on many designs, especially when users trigger a blue ring by clicking a button or link with the mouse. The common "solution" was outline: none in global CSS resets, which effectively destroyed keyboard navigation for users who rely on visible focus indicators.
The dilemma is not one you need to solve through compromise, there is a clean technical solution. The CSS pseudo-class :focus-visible activates focus styling selectively: the focus ring appears on keyboard input but not on mouse click. That matches the behavior users intuitively expect: someone clicking with the mouse does not need a visible focus indicator, while someone navigating with the keyboard depends on it. :focus-visible makes exactly this distinction possible, without JavaScript and without heuristic browser detection.
2. :focus vs. :focus-visible: the crucial difference
The pseudo-class :focus activates whenever an element receives focus, whether through keyboard, mouse click, touch, or a programmatic element.focus() call. :focus-visible, on the other hand, is selective: the browser decides based on an internal heuristic algorithm whether the focus "should be made visible". During keyboard navigation that is always the case. On mouse click for most interactive elements it is not, except for elements where users typically enter text (inputs, textareas), where the focus ring is always shown.
The difference is practically significant: with CSS :focus-visible you can effectively set outline: none for mouse users without sacrificing keyboard accessibility. The pattern is simple: first neutralize the default focus style, then give :focus-visible a nice, WCAG-compliant outline style. Browsers that do not support :focus-visible ignore the second rule and keep the default focus ring, which is a valid progressive-enhancement fallback.
/* Step 1: Remove default focus ring (it will be replaced) */
:focus:not(:focus-visible) {
outline: none;
}
/* Step 2: Style focus-visible with WCAG-compliant indicator */
:focus-visible {
outline: 3px solid rgb(124 58 237); /* Violet, meets contrast requirements */
outline-offset: 3px;
border-radius: 4px;
}
/* Custom per-component focus styles */
.btn:focus-visible {
outline: 3px solid rgb(196 181 253);
outline-offset: 4px;
box-shadow: 0 0 0 6px rgb(124 58 237 / 0.25);
}
.card:focus-visible {
outline: 2px solid rgb(124 58 237);
outline-offset: 2px;
}
/* Dark background: invert focus ring for contrast */
.dark-section :focus-visible {
outline-color: rgb(196 181 253); /* Light purple on dark background */
}
An important note: setting outline: none globally on all elements and resetting it only for :focus-visible is the recommended approach. The alternative :focus:not(:focus-visible) { outline: none } is equally correct and more explicit. Both approaches require :focus-visible styles to be defined for all interactive elements; a single global rule is often enough, but component-specific adjustments improve visual consistency.
3. Styling outline correctly under WCAG standards
WCAG 2.1 (Web Content Accessibility Guidelines) defines concrete requirements for focus indicators in Success Criterion 2.4.11 (Focus Appearance, Level AA in WCAG 2.2). The focus indicator must cover a minimum area equal to the outline width times the perimeter of the element, and the contrast ratio between the indicator and the adjacent area must be at least 3:1. In practice this means an outline with a width of at least 2 pixels and sufficient contrast against both the background of the focused element and its own unfocused color.
The CSS property outline-offset is particularly important here. It pushes the outline ring outward and prevents it from colliding with the element's border. An outline-offset: 3px creates visually appealing, clearly recognizable focus rings that stand out from the focused element. Combining outline with box-shadow enables more elaborate focus styles: the box-shadow can create a double ring that stays equally visible on light and dark backgrounds.
/* WCAG 2.2-compliant focus indicators */
/* Universal focus base: high visibility */
*:focus-visible {
outline: 3px solid rgb(124 58 237);
outline-offset: 3px;
/* Rounded corners follow element's border-radius */
border-radius: inherit;
}
/* Double-ring pattern: visible on any background */
.interactive:focus-visible {
outline: 3px solid rgb(124 58 237);
outline-offset: 2px;
/* White gap between element and outline */
box-shadow:
0 0 0 2px white,
0 0 0 5px rgb(124 58 237);
}
/* High-contrast mode support */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid ButtonText;
outline-offset: 3px;
}
}
/* Reduced-motion: no animated focus transitions */
@media (prefers-reduced-motion: reduce) {
:focus-visible {
transition: none;
}
}
/* Focus indicator for links within text */
a:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
text-decoration-color: transparent;
}
4. :focus-within for container states
The pseudo-class :focus-within becomes active on an element when that element itself or one of its descendants has focus. That enables container-level styling reactions that were previously impossible without JavaScript: a form container can be highlighted when any input field inside it is active. A navigation group can open when a submenu item receives focus. A search field wrapper can change its label style when the input inside it is focused.
A typical use case for CSS :focus-within: a form label that moves upward when its associated input receives focus (the floating label pattern). With :focus-within on the wrapper element this effect can be implemented purely in CSS. Other applications: dropdown menus that open on focus and close on focus loss, search bars that expand, tooltips that appear on focus. :focus-within is the CSS equivalent of JavaScript's focusin event at the container level.
5. Focus traps in modals and dialogs
A focus trap keeps keyboard focus inside a specific element, typically a modal or dialog, and prevents the user from tabbing into content behind it. This is essential for accessibility: while a modal is open, focus must stay inside it until the user explicitly closes it. Otherwise keyboard users could accidentally activate elements behind the modal without realizing their focus has left it.
The HTML <dialog> element implements a focus trap natively when opened with showModal(). For custom modals that do not use the native <dialog> element, JavaScript is needed for a complete focus trap. CSS alone cannot create a true focus trap, but it can help by removing all focusable elements outside the modal from the tab order using inert or tabindex="-1". Modern CSS with visibility: hidden on the backdrop overlay or pointer-events: none on the background restricts interactions, but tab navigation is independent of that.
/* Focus management for native <dialog> element */
dialog {
/* Native dialog: focus trap is automatic with showModal() */
border-radius: 1rem;
padding: 2rem;
border: 1px solid rgb(226 232 240);
box-shadow: 0 20px 60px rgb(0 0 0 / 0.3);
max-width: min(90vw, 40rem);
max-height: min(85dvh, 50rem);
overflow-y: auto;
}
/* Backdrop styling */
dialog::backdrop {
background: rgb(0 0 0 / 0.6);
backdrop-filter: blur(4px);
}
/* First focusable element gets focus on open */
dialog h2:first-child {
/* Not focusable by default, add tabindex="-1" in HTML */
/* Then use JS: dialog.querySelector('h2').focus() */
}
/* Skip link, visible only on keyboard focus */
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 9999;
padding: 1rem 2rem;
background: rgb(124 58 237);
color: white;
font-weight: bold;
border-radius: 0 0 0.5rem 0;
}
.skip-link:focus-visible {
left: 0;
outline: 3px solid white;
outline-offset: 2px;
}
6. Skip links and keyboard navigation
Skip links are anchor links at the top of a page that let keyboard users bypass repeated navigation elements and jump straight to the main content. They are one of the most important accessibility features for keyboard users and correspond to WCAG Success Criterion 2.4.1 (Bypass Blocks). On the CSS side, skip links are usually positioned outside the visible area and made visible only on focus, using :focus-visible rather than :focus for a correct, modern implementation.
The skip link pattern with CSS :focus-visible: the element is hidden with position: absolute; left: -9999px or clip: rect(0,0,0,0) and returns to its visible position on :focus-visible. Important: the target element (e.g. #main-content) needs either a native focus attribute or tabindex="-1" so that :target scrolling and programmatic focus management work correctly. The easiest way to test skip links is to press Tab on a freshly loaded page.
7. :has() for focus-dependent layouts
The CSS pseudo-class :has() combined with :focus-visible or :focus-within opens up powerful new possibilities for focus-dependent layouts. With :has(:focus-visible) a container can react to the focus of any descendant, similar to :focus-within, but with the flexibility of the :has() selector, which can also target sibling elements. The pattern .form-group:has(input:focus-visible) label changes the label style when the associated input is focused, even though the label is not a direct parent of the input.
Another example: a navigation bar that changes appearance when any link inside it receives focus. With nav:has(a:focus-visible) { background: rgb(245 243 255) } the nav gets a light violet background as soon as a link is focused, a subtle but effective visual cue for keyboard users about which area of the page they are in. This kind of contextual focus management improves orientation without being intrusive.
8. Focus strategies compared
Different approaches to focus management come with different trade-offs in accessibility, visual quality, and implementation effort.
| Approach | Keyboard users | Mouse users | WCAG compliance |
|---|---|---|---|
| :focus-visible (modern) | Clearly visible | No ring | AA compliant |
| outline: none (global) | No focus indicator | No ring | WCAG violation |
| :focus (style everything) | Visible | Ring on click | Compliant, but intrusive |
| JS-based focus management | Flexible | Controllable | Depends on implementation |
| Browser default (no CSS) | Functional | Platform dependent | Usually compliant |
The recommended strategy is clear: :focus-visible for all interactive elements, complemented by careful outline styling that follows WCAG guidance. :focus-within for container states. The native <dialog> element whenever possible, since it implements a focus trap natively. Use JavaScript only where CSS does not offer a sufficient solution.
9. Testing and accessibility audits
Focus management can be tested with simple means: press Tab on the page and check whether every interactive element has a visible focus indicator, whether the tab order is logical, and whether no "focus traps" exist outside modals. Browser DevTools show the computed focus state and ARIA attributes under Accessibility. The axe DevTools plugin for Chrome reports automated WCAG violations for focus management.
Specific tests for :focus-visible: focus every interactive element by mouse click and verify that no focus ring appears. Then navigate with Tab and verify the ring is visible on every element and has sufficient contrast and size. For modals: open the modal, press Tab and verify that focus stays inside the modal and does not move to the background content. Press Escape and verify that focus returns to the triggering element.
Mironsoft
Accessibility audits, WCAG compliance and accessible frontend design
Need WCAG-compliant keyboard navigation for your website?
We run complete accessibility audits, identify focus management issues, and implement :focus-visible, skip links, and correct focus traps to the WCAG 2.2 AA standard.
Accessibility audit
Full WCAG 2.2 review with a focus on keyboard navigation and focus management
CSS implementation
Setting up :focus-visible, :focus-within and outline styles to WCAG standards
Modal accessibility
Focus traps, ARIA attributes and keyboard handlers for accessible dialogs
10. Summary
Correct CSS focus management is not an optional feature, it is the foundation of an accessible website. The key building blocks: :focus-visible shows the focus ring selectively, only during keyboard navigation, not on mouse click. An outline with a width of at least 3 pixels and sufficient contrast meets WCAG 2.2 AA. :focus-within enables container reactions to focus events without JavaScript. The native <dialog> element provides a native focus trap for modals. Skip links using :focus-visible substantially improve keyboard navigation.
Combining these building blocks produces a robust focus system: users navigating with a mouse never encounter a distracting focus ring. Keyboard users get clear, consistent focus indicators on every interactive element. Screen reader users get correct focus order and focus traps inside modals. :has() combined with :focus-visible additionally enables contextual styling that improves orientation. All of these techniques work without JavaScript and are fully supported in modern browsers.
CSS Focus Management: the essentials at a glance
:focus-visible
Focus ring only during keyboard navigation. :focus:not(:focus-visible) { outline: none } for mouse users, then :focus-visible with a nice outline style.
WCAG-compliant outline
At least 3px width, 3:1 contrast ratio. outline-offset for visual spacing. Double ring with box-shadow for universal visibility.
:focus-within
Container reaction to a descendant's focus. Floating labels, dropdown triggers, search field expansion, all in pure CSS.
Focus trap
Native <dialog> with showModal() for an automatic focus trap. Custom modals need JavaScript for a complete implementation.