Scalable Focus Indicators Meet AA/AAA Criteria
Focus visibility is WCAG 2.2 AA (focus-visible) and AAA (focus-appearance, new in 2.2). AA requires keyboard interfaces to show focus indicators like browser outlines. AAA demands distinguishable indicators: at minimum, 2px thick outlines contrasting non-focused states.
Default browser outlines often fail AAA (too thin or low-contrast). Eckles fixes this with custom properties and max() for responsive sizing:
:root {
--outline-w: max(2px, 0.15em);
--outline-o: max(2px, 0.15em);
}
*:focus-visible {
outline: var(--outline-w) solid currentColor;
outline-offset: var(--outline-o);
}
max(2px, 0.15em) ensures ≥2px width (scales with font-size but never below 2px). currentColor inherits text color, passing contrast if text meets AA (4.5:1). For buttons where currentColor (white) fails against backgrounds, override to button's background color—outline-offset creates a gap for distinction.
This scales across components without per-element styles. Tradeoff: Test per-component contrast; currentColor assumes text contrast is solid.
Related AA/AAA criteria prevent author styles from hiding focus (AA: not entirely hidden; AAA: no part hidden). Browsers may obscure focus (e.g., Chrome clips outlines). Fix with scroll-margin-block: 10dvb on :focus—adds proportional viewport space (dynamic viewport units) without layout shifts. Only affects scroll position.
"The key take-away from the guidance for focus appearance is that any outline that is at least 2 pixels thick and contrasts with the non-focused appearance would pass this criterion." – Eckles on AAA minimums, enabling simple passes over complex tweaks (see Sara Soueidan for nuances).
Logical Focus Order Without DOM Rewrites
WCAG A (focus-order) requires tabbing to match logical/visual flow for screen readers/keyboard users. CSS like Grid/Flexbox order, position: sticky/absolute/fixed, anchor positioning, or masonry disrupts this—focus jumps randomly.
Best fix: Author DOM in visual order. If impossible, upcoming reading-flow/reading-order (Chrome-only; no Mozilla/WebKit support yet) linearizes focus. E.g., Grid rows: reading-flow: grid-rows tabs row-by-row without DOM/CSS changes.
Tradeoff: Unreliable cross-browser; prioritize source order. Eckles stresses: "When you are placing interactive elements in a layout, it's important to consider the actual order in the DOM as compared to what the final layout position."
Reflow at 400% Zoom Without Dual Scrolling
WCAG AA (reflow) demands single-column reflow at 400% desktop zoom (1280x1024px → 320x256px viewport) without content loss, functionality breaks, or horizontal scroll. This ~landscape iPhone 14 aspect ratio (narrower/wider than portrait mobile) trips mobile-first designs.
Common breaks: Sticky navs cover viewport; contained scrolls cutoff; fluid typography overflows; fixed px margins dominate (128px sections → 50%+ viewport at 400%).
Key insight: Viewport size ≠ device/user. Small viewports mean zoom, not mobile/touch—rearrange, don't remove features.
Fix spacing with min(128px, 25vh)—uses px normally, switches to proportional vh at zoom where px exceeds (as early as 200%). Viewport units scale computed values with zoom; px don't.
"Viewport size is not a proxy for device or user capabilities... a small viewport doesn't necessarily mean mobile. Doesn't necessarily mean touch. It may just mean a high zoom level is in use." – Eckles reframes responsive assumptions, preventing mobile-biased failures.
Theme Preferences via CSS Without Duplication
WCAG lacks dark mode mandates but stresses contrast (AA: 4.5:1 text) and non-color cues (A: use-of-color). Inclusive design honors prefers-color-scheme.
Set color-scheme: dark light; (prefers dark, supports light)—browser auto-applies system colors (Canvas background, CanvasText, etc.) to doc, links, forms, scrollbars for contrast. Overridable.
Use light-dark() (requires color-scheme): color: light-dark(#000, #fff);—single rule for themes, no media query duplication. Cards/buttons adapt smoothly.
Handle forced-colors: active (Windows High Contrast Mode, system-wide): Overrides colors (color, background-color, etc.) with system palette (Canvas, ButtonFace); removes shadows; adds backplates. Users customize palettes; can't opt-out/brand it.
Impacts: Lost shadows (replace with transparent borders → CanvasText); icon fills (use forced-colors-adjust: none for critical color); buttons lose emphasis (invert Canvas/CanvasText, but watch backplates).
Layer fixes in @layer forced-colors { ... } (last layer wins specificity). Transparent borders (border: 1px solid transparent) gain visibility sans query. Tripadvisor example: Semantic elements convert well; fixes for callouts, notifications, buttons.
"Users can customize forced-colored palettes... you cannot opt your site out... they might not even see your initial styles ever." – Eckles on forced-colors inescapability, prioritizing critical overrides (SVGs, workflows) over branding.
Key Takeaways
- Define global
:focus-visiblewithmax(2px, 0.15em)outlines usingcurrentColorfor 2px+ AAA-compliant indicators. - Add
scroll-margin-block: 10dvbto nudge focused elements into view without obscuring. - Author DOM in visual order; await
reading-flow: grid-rowsfor CSS fixes. - Use
min(128px, 25vh)for spacing to scale at 400% zoom reflow. - Declare
color-scheme: dark light;+light-dark()for theme adaptation. - In
@layer forced-colors, add transparent borders andforced-colors-adjust: nonefor critical elements. - Test 320x256px viewport, forced-colors emulator (Chromium rendering panel), and Windows themes.
- Prioritize semantics; they auto-improve forced-colors handling (e.g., Tripadvisor).
- Contrast text first—focus/forced-colors inherit wins.