Why Does Focus State Always Break After a Refactor? A Keyboard-First Checklist

A six-point pre-release checklist — from :focus-visible to theme contrast to Tab walkthroughs — to keep keyboard focus intact after a refactor.
Visual refactors almost always sacrifice keyboard focus experience first, because developers test with a mouse, and the mouse never exposes a missing focus ring. The article dissects three high-frequency sources of breakage: global outline: none in CSS resets, new focus behaviors from replaced component libraries, and dark focus rings hardcoded for dark mode. The author's core fixes include: replacing :focus with :focus-visible to eliminate the trade-off between keyboard visibility and mouse aesthetics; insisting on outline rather than box-shadow with a 2px offset to avoid layout shifts and clipping; binding focus ring colors to theme variables and verifying at least 3:1 contrast on light, dark, and colored blocks separately. The single most important check is putting the mouse away and Tab-navigating through the whole page — the best way to expose focus order chaos, focus traps, and hidden-but-focusable elements that static CSS checks miss.
Why Does Focus State Always Break After a Refactor? A Keyboard-First Checklist
Visual refreshes often come at the cost of keyboard experience. The page looks cleaner, the colors are more pleasant, but the focus outline quietly disappeared in some version. This isn't an occasional risk — it's something that happens almost inevitably after every refactor. A frontend developer compiled a six-point checklist to run before every release, taking about twenty minutes to complete on their own site.

Why Refactors Always Eat Focus State First
Focus is invisible until you need it — that's the core of the problem. When staring at a new layout in the browser, most people click with a mouse, and the mouse never shows a focus ring. The one visual cue keyboard users depend on is exactly what developers are least likely to notice has gone missing during a refactor.
The usual culprit is the CSS reset. To kill the browser's "ugly" default focus ring, a designer casually writes outline: none, then forgets to add any replacement. The result: every button, link, and input becomes a dead end for Tab key users.
The second common cause is component library replacement. Switch from one styled component library to another, and the new components come with their own focus behavior — maybe it exists, maybe it's just a 1px gray line completely invisible on a gray background. Developers inherit someone else's decisions and never verify them.
The third cause is theme adaptation. After adding dark mode, the focus ring color gets hardcoded to a dark value, making it completely invisible on the dark canvas. Light theme is fine, dark theme becomes a literal black box.
Here's a point worth stating plainly: keyboard users are not a niche minority. Power users navigate with Tab because it's faster, screen reader users depend on it entirely, and people with motor impairments who struggle with a mouse can't do without it. When the focus ring disappears, these people simultaneously lose their position on the page.
Replace :focus with :focus-visible
The single highest-value change, according to the author, is swapping :focus for :focus-visible.
The old focus styling had an annoying trade-off: if you made :focus styles prominent for keyboard users, mouse users would see a giant focus ring every time they clicked a button, which felt awkward. So people just deleted it, and both groups suffered.
:focus-visible resolves this contradiction — the browser decides when to show it. Click a button with a mouse — no focus ring. Tab to a button with a keyboard — the focus ring appears prominently. Same CSS, different behavior depending on how the element gained focus.
The author's baseline rule: outline: 2px solid currentColor; outline-offset: 2px;, plus :focus:not(:focus-visible) { outline: none; } to eliminate the outline on mouse clicks.
The role of outline-offset is more important than most people expect. An outline hugging the element's edge reads like a border rather than a focus state. A two-pixel gap lets users clearly perceive the element is selected, and also prevents the outline from being clipped at rounded corners.
The author insists on using outline rather than box-shadow for focus rings. outline doesn't trigger layout shifts, and it won't be clipped by a parent's overflow: hidden. box-shadow focus rings often mysteriously vanish inside scroll containers, only to be discovered on some later scroll.
One more trap: never apply outline: none globally without a :focus-visible replacement. If you just write *:focus { outline: none } with no other remedy, one line of code wipes out keyboard navigation across the entire site. The author says they've seen this ship to production more than once.
The verification method is simple: put the mouse away and press Tab. Every interactive element should light up as you move. If one doesn't, either it's not actually focusable (a div pretending to be a button), or the focus ring style isn't being applied.
:focus-visibleis a pseudo-class introduced in CSS Selectors Level 4, designed to distinguish "why an element gained focus." The traditional:focusapplies to focus gained any way — including mouse clicks, touch, and keyboard navigation — which is exactly why it's hard to balance aesthetics and visibility.:focus-visibleis decided by the browser based on heuristic rules: if focus was gained via keyboard (Tab, Shift+Tab, arrow keys) or equivalent input, it matches; if it was just mouse or touch click, it usually doesn't. The exception is text inputs — even with a mouse click, inputs retain a cursor indicator, so the browser generally still matches. Browser support is now quite mature — available in Chrome, Firefox, Safari, and major mobile platforms; but older browsers will ignore this rule, so keeping a:focusfallback style is safer. In implementation this is usually written as:focus { outline: none }plus:focus-visible { outline: ... }, or like the author does with:focus:not(:focus-visible)to precisely turn off the outline in the mouse state.The
outline-offsetproperty controls the distance the outline is offset relative to the element's outer border edge. Positive values push the outline outward (away from the element), negative values pull it inward. It only affects the draw position, not box model calculations, so it won't change element size or trigger layout reflow. When an element hasborder-radius, the outline by default follows the rounded corners, but an outline hugging the edge is hard to distinguish from the border. Adding an offset of about 2px creates a clear visual gap, making the focus state identifiable at a glance. Compared tobox-shadow,outlinedoesn't take up layout space and isn't clipped byoverflow: hidden— properties that matter especially in scroll containers and nested component scenarios.
Make Focus Rings Have Enough Contrast in Both Themes
A focus ring that exists but is invisible is equivalent to no focus ring. The number the author insists on: at least 3:1 contrast between the outline color and the immediately adjacent background.
3:1 is the threshold for non-text UI elements in accessibility guidelines, which includes focus indicators. You need to check the contrast of the focus ring against the element's own background, and against the page background, because the offset gap means the focus ring is often half on the element and half on the page background.
The theme problem is where most failures hide. You need to check on every background the element might appear on: light theme, dark theme, and any colored regions like hero sections or inverted footers. A single outline color almost never passes contrast requirements against all backgrounds.
The author's solution is to bind the focus ring color to a theme variable rather than hardcoding it. Define the light theme's --focus-ring at the root variable level, override it to a light value under [data-theme="dark"], and have the focus styles reference that variable.
For elements with unpredictable backgrounds — like buttons on top of photos or videos — the author stacks a second focus ring layer: a white outline plus a black glow, so no matter what's behind it, half is always visible. Only in this scenario does the author use box-shadow as a supplement to the dual layer, accepting the clipping risk because fallback visibility is more important.
You can use any contrast tool to check contrast. Pick the outline color and background, read the value. Below 3:1, either thicken the outline to 2px or change the color. Don't make the focus ring thinner just to make it "subtle" — to the people who need it, subtle and invisible are the same thing.
Also check forced colors mode while you're at it. Windows High Contrast Mode completely overrides custom colors, and outline survives under this override more reliably than box-shadow. This is another reason the author defaults to outline.
The 3:1 contrast threshold here comes from WCAG 2.1's 1.4.11 Non-text Contrast criterion, which requires at least 3:1 between interface components and graphical objects (including focus indicators) and their adjacent colors. It measures the difference in relative luminance of colors, independent of font size, and is a different threshold from the 4.5:1 required for body text. What's complicated about focus rings is that they often touch two types of backgrounds simultaneously: the element itself and the page background outside it. If the outline has an offset gap, the gap shows the page background color, and the outline line is half over the element and half over the page, so both directions must be verified. In practice, you can use any contrast checking tool, enter the hex values of the outline color and background color to read the ratio; below 3:1, increasing outline width, deepening the color, or changing the hue can all improve it. Additionally, "forced colors" mode is an operating-system-level setting — Windows High Contrast Mode forcibly replaces most colors on the page, at which point custom backgrounds and borders may fail, while
outlineusually survives via system mapping, whereasbox-shadowis often ignored — another reason the author prioritizes outline.
Tab Through the Entire Page, Mouse Unplugged
This is the test that catches everything CSS checks miss. Put the mouse aside and Tab through the entire page from start to finish as if you had no other choice.
Focus order should follow visual layout: top to bottom, left to right. If focus jumps from the header to the footer and bounces back to the middle, that means DOM order doesn't match visual order, and keyboard users are being tossed around a page without a map.
This happens frequently after refactors because CSS allows moving visual positions without touching the DOM. Use flexbox's order or grid layout to put a sidebar located at the bottom of the HTML on the visual left — looks right, tabs wrong. Fix the DOM order, not the tab order. Avoid positive-value tabindex — that's a patch that makes things harder for the next person to maintain.
Key things to observe while traversing: every stop has a visible focus ring, with no silent jumps; no invisible focusable elements (when the mobile menu is closed, its links shouldn't appear in the tab order); no focus traps (every modal, dropdown, and date picker can be tabbed out of, and Escape closes them); the first stop is a skip link, letting keyboard users skip navigation and jump straight to main content.
Modal traps are the easiest pitfall to fall into after component replacement. A good modal intentionally constrains focus when open, and returns focus to the button that triggered it when closed; a bad modal either traps focus forever or dumps the user back at the top of the page. Open every overlay, tab around once, press Escape, and confirm focus lands somewhere sensible.
Also walk through at narrow screen widths, because mobile layouts reflow and hide elements. A perfect tab order on desktop can be a mess on mobile.
Doing this walkthrough once for every major view takes only a few minutes and is the closest thing to "being a keyboard user for five minutes." If you only run one item from the checklist, run this one.
Tab order is determined by DOM order, modulated by the
tabindexattribute. By default, browsers traverse elements in the order they appear in the HTML, including only natively focusable elements (links, buttons, inputs, select, textarea, and elements with tabindex). CSS visual reordering (flexbox'sorder, grid's explicit row/column positioning, absolute positioning) can change an element's on-screen position without touching the DOM, causing focus movement direction to diverge from visual reading direction. The fix is to adjust the HTML structure, not to force-calibrate with positivetabindexvalues — the latter forces future maintainers to constantly adjust values when adding or removing elements, is highly error-prone, and can break screen reader reading order.A "skip link" is usually an anchor placed at the very front of the page, visually hidden by default, revealed on focus. Clicking or pressing Enter sends focus directly to the main content container, sparing keyboard users from stepping through the entire navigation. Focus management also involves two concepts: "focus trap" and "focus return." When a modal opens, focus should be constrained within the dialog; when it closes, focus should return to the control that triggered it. Component libraries each have their own implementation of this, and after switching libraries you need to verify each one manually. If hidden content like mobile menus and collapsible panels doesn't use
display: none, thehiddenattribute, orinert, its links may still appear in the tab order, causing focus to jump to invisible places.
FAQ
What most easily breaks focus state in a refactor? Global outline: none in CSS resets, different focus behaviors from new components after library replacement, and dark focus rings hardcoded for dark themes are the three most frequent sources of breakage.
Why recommend :focus-visible over :focus? The browser can automatically judge based on how the element gained focus: mouse clicks don't show a focus ring, keyboard Tab does. This eliminates the long-standing trade-off between "keyboard visibility" and "mouse aesthetics."
What contrast should focus rings achieve? At least 3:1 between the outline color and the immediately adjacent background. And it must be verified separately on light, dark, and all colored blocks — a single color often can't pass every scenario.
The Bottom Line
Run the four checks in order and you'll catch almost every focus regression a refactor introduces: switch to :focus-visible, give the focus ring a 2px offset, maintain 3:1 contrast against all backgrounds (including dark mode), then put the mouse away and Tab through the entire page.
If you can only keep one item, make it the Tab walkthrough. The CSS in a stylesheet can look perfect and still fail in real flow, and the only way to know is to use the page like a keyboard user.
Add this check to your pre-release routine and it becomes muscle memory. The check you have to remember is the check that gets skipped on a busy day.
Related articles

Felix Rieseberg Personal Homepage
Felix Rieseberg's personal homepage sparks attention on Hacker News. This article covers the Slack engineer's technical background, the rise and fall of nativefier, and the value of a developer homepage as a tech calling card.

@ai-sdk/vue 3.0.306 Patch Release Syncs with [email protected] Update
@ai-sdk/vue 3.0.306 released, synced with [email protected]. A routine patch release for the Vercel AI SDK ecosystem with no public API changes; the Vue adapter layer stays version-aligned with the core package.

Gemini 4 Argon API Launches: The Era of Million-Token Output Begins
Google launches Gemini 4 Argon with sparse MoE architecture, up to 1M output tokens, and a 68.90% Vals Index score, available across API, AI Studio, and the Gemini app.