Focus Indicators — Designing Visible Focus States in Design Systems
In this tutorial, you will learn about Focus Indicators. We cover key concepts, practical examples, and best practices to help you master this topic.
Focus indicators in design systems must be visible, high-contrast, consistent across all components, use focus-visible for smart appearance, and be defined as design tokens for system-wide consistency.
What You'll Learn
You will learn how to design accessible focus indicators, how to use CSS focus-visible for the right behavior, how to define focus tokens, and how to ensure every interactive component has a visible focus state.
Why It Matters
Users who navigate with a keyboard rely on focus indicators to know where they are on the page. Without visible focus indicators, keyboard users cannot operate the interface effectively.
Real-World Use
DodaKit defines focus indicators as design tokens. Every component uses the same focus ring style. The focus ring uses outline not box-shadow to respect platform conventions and avoid clipping.
flowchart TD A[Focus Indicators] --> B[Visible] A --> C[High Contrast] A --> D[Consistent] A --> E[focus-visible] A --> F[Design Token] B --> G[2-3px outline] C --> H[3:1 minimum on all backgrounds] D --> I[Same style everywhere] E --> J[Shows only on keyboard] F --> K[Token for reuse]
The focus-visible Pseudo-class
Use :focus-visible instead of :focus. The :focus-visible pseudo-class applies focus styles only when the browser determines the focus should be visible, typically keyboard navigation. It does not show on mouse clicks.
/* Focus indicator system */
:root {
--ds-focus-ring-width: 3px;
--ds-focus-ring-style: solid;
--ds-focus-ring-color: #005fcc;
--ds-focus-ring-offset: 2px;
--ds-focus-ring: var(--ds-focus-ring-width) var(--ds-focus-ring-style) var(--ds-focus-ring-color);
}
/* Apply to all interactive elements */
*:focus-visible {
outline: var(--ds-focus-ring);
outline-offset: var(--ds-focus-ring-offset);
}
/* High contrast mode override */
@media (prefers-contrast: more) {
*:focus-visible {
outline-color: #000000;
outline-width: 4px;
}
}
/* Reduced motion: no transition on focus */
@media (prefers-reduced-motion: reduce) {
*:focus-visible {
transition: none;
}
}
Focus Indicator Design
Size
A focus ring should be 2 to 3 pixels wide. Thinner rings may not be visible. Thicker rings obscure the element.
Color
Use a high-contrast color. Blue (#005fcc) works on most backgrounds. Ensure 3:1 contrast against all adjacent colors.
Offset
A 2 to 4 pixel offset prevents the focus ring from blending into the component border.
Shape
Use outline for focus rings. Box-shadow can clip on overflow hidden containers. Outline respects platform conventions.
// Focus indicator validator
function validateFocusIndicator(styles) {
const checks = [];
// Check if focus-visible is used
if (styles.focusSelector === ':focus-visible') {
checks.push({ check: 'Uses :focus-visible', pass: true });
} else if (styles.focusSelector === ':focus') {
checks.push({ check: 'Uses :focus instead of :focus-visible', pass: false,
note: ':focus shows on mouse click too' });
} else {
checks.push({ check: 'Has focus selector', pass: false,
note: 'No focus selector found' });
}
// Check outline width
const outlineWidth = parseFloat(styles.outlineWidth);
if (outlineWidth >= 2 && outlineWidth <= 4) {
checks.push({ check: 'Outline width 2-4px', pass: true });
} else {
checks.push({ check: 'Outline width 2-4px', pass: false,
note: `Width is ${outlineWidth}px` });
}
// Check for outline removal without replacement
if (styles.outline === 'none' && !styles.customIndicator) {
checks.push({ check: 'Has replacement for removed outline', pass: false,
note: 'Outline removed without replacement' });
}
// Check box-shadow not used as only indicator
if (styles.usesBoxShadow && !styles.usesOutline) {
checks.push({ check: 'Not using only box-shadow', pass: false,
note: 'Box-shadow clips on overflow hidden' });
}
const passed = checks.filter(c => c.pass).length;
return {
total: checks.length,
passed: passed,
failed: checks.length - passed,
details: checks,
overall: checks.length === passed ? 'Pass' : 'Issues found'
};
}
const buttonStyles = {
focusSelector: ':focus-visible',
outlineWidth: '3px',
outline: '3px solid #005fcc',
customIndicator: true,
usesBoxShadow: false,
usesOutline: true
};
console.log(validateFocusIndicator(buttonStyles));
Expected output:
{
total: 4,
passed: 4,
failed: 0,
details: [
{ check: 'Uses :focus-visible', pass: true },
{ check: 'Outline width 2-4px', pass: true },
{ check: 'Has replacement for removed outline', pass: true },
{ check: 'Not using only box-shadow', pass: true }
],
overall: 'Pass'
}
Focus Within and Focus Ring
For Composite components, use :focus-within to show focus on the container when any child element has focus. This helps users identify which component group is focused.
<!-- Focus indicator examples -->
<button class="ds-button ds-button--primary" type="button">
Scan (focus-visible)
</button>
<div class="ds-input-group" aria-label="Search">
<label for="search-input" class="visually-hidden">Search</label>
<input id="search-input" type="search" placeholder="Search...">
<button type="submit" aria-label="Submit search">Go</button>
</div>
<style>
/* Component focus states */
.ds-button:focus-visible {
outline: var(--ds-focus-ring);
outline-offset: var(--ds-focus-ring-offset);
}
/* Input group focus-within */
.ds-input-group:focus-within {
outline: var(--ds-focus-ring);
outline-offset: 2px;
border-radius: 4px;
}
/* Never remove focus without replacement */
*:focus {
outline: none; /* Only if :focus-visible provides replacement */
}
</style>
Common Mistakes
1. Removing Focus Outlines
The most common Accessibility mistake. outline: none without a replacement makes the site unusable for keyboard users.
2. Using :focus Instead of :focus-visible
Using :focus applies focus styles on mouse clicks too. This confuses users who see a focus ring after every click.
3. Focus Ring Not Visible on All Backgrounds
A blue focus ring works on white but may be invisible on a blue background. Test on all component variants.
4. Box-Shadow Focus That Clips
Components with overflow: hidden clip box-shadow. Use outline instead for reliable focus indicators.
5. No Focus on Interactive Containers
Interactive containers like cards with onclick need focus indicators. Use tabindex="0" and a focus style.
6. Inconsistent Focus Styles
Different focus styles on different components confuse users. Use a single focus ring token everywhere.
7. Focus Ring Too Thin
A 1px focus ring is not visible enough. Use at least 2px, preferably 3px.
Practice Questions
1. What CSS pseudo-class should be used for focus indicators?
:focus-visible. It applies focus styles only when the browser determines focus should be visible, typically during keyboard navigation.
2. Why should outline be used instead of box-shadow for focus?
Outline does not clip on overflow: hidden containers. Box-shadow can be clipped.
3. What is the recommended focus ring width?
2 to 3 pixels. Thinner rings are not visible enough. Thicker rings obscure the element.
4. What is :focus-within used for?
To show a focus indicator on a parent container when any child element has focus. Useful for composite components.
5. Challenge: Design a focus indicator system with 3 variants (default, dark background, high contrast). Test each on 5 different background colors.
FAQ
Mini Project
Create a focus indicator system for a design system with 8 interactive components. Define focus tokens, apply focus-visible, test on light and dark backgrounds, and document the system.
What's Next
Learn about Accessible Forms System patterns for accessible form components. Then explore Accessible Navigation System.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro