Skip to content

Replace focus rings with a token shift in the chrome surface #2718

Closed

Description

@malibio

Overview

Replace the five inconsistent shadcn focus-ring idioms in the chrome surfaces with a painted-only token shift: a focused control adopts its own --*-hover appearance. No rings, no outlines, no offsets, and — the hard constraint — nothing that can change an element's size or position.

The hard constraint: focus must never move anything

A focus treatment may change painted properties only:

Allowed Forbidden
background-color border-width, border shorthand
color padding, margin
outline, outline-offset
box-shadow / Tailwind ring-* / ring-offset-*
transform, font-weight, letter-spacing

The mechanism matters, because the three forbidden groups fail for different reasons:

  • border-width / padding genuinely reflow. Going from border: 0 to border: 2px grows the box by 4px and shoves every sibling after it. This is the one that actually "jerks" the layout.
  • outline with a negative offset is what the historical rules used (outline: 2px solid; outline-offset: -2px, removed in Split-Pane Layout System #300). It does not reflow, but it paints inside the element's edge, reading as the element having suddenly grown a thick inner edge.
  • ring-offset-* paints a gap-plus-ring around the control. It does not reflow either, but it visually enlarges the control's footprint, which is indistinguishable from movement to a user.

Forbidding all three is deliberate: the distinction between "actually reflows" and "looks like it reflows" is not one a reader should have to re-derive, and only painted properties are safe in every case.

Current state is already compliant — every focus utility in the codebase today is ring-* or outline-none; nothing changes geometry. This rule prevents regression as much as it fixes the present.

The two surfaces are not the same, and only one is in scope

Editor surface — OUT OF SCOPE, leave exactly as is. In the node tree, tab system and sidebar, Tab is bound to indent/outdent, not focus traversal. Focus styles were removed there deliberately (#300), with the reasoning recorded in the source:

/* Removed :focus-visible border - Tab key used for indent/outdent, not UI navigation */

(node-tree.svelte:183, tab-system.svelte:464,562, navigation-sidebar.svelte:771,827)

That decision stands.

Chrome surface — IN SCOPE. Settings panes, dialogs and modals are ordinary forms where Tab is the only way to move between controls, and where a user currently cannot tell which control Enter will activate.

Current state: five idioms, three defects

Component Current Issue
ui/button/types.ts:30 outline-none, nothing else no indicator at all
ui/select/select-trigger.svelte:17 focus-visible:outline-none no indicator at all
schema/schema-field-leaf.svelte:72 focus-visible:outline-none no indicator at all
schema/nested-field-trigger.svelte:31 focus-visible:outline-none no indicator at all
ui/checkbox/checkbox.svelte:23 ring-ring/50 ring-[3px] 1.84:1 light / 2.28:1 dark
ui/switch/switch.svelte:17 ring-2 ring-offset-2 ring idiom
ui/tabs/tabs-trigger.svelte:12 ring-2 ring-offset-2 ring idiom
ui/tabs/tabs-content.svelte:12 ring-2 ring-offset-2 ring on a scroll container
ui/radio-group/radio-group-item.svelte:17 focus: + ring-1 ring idiom; focus: fires on mouse
ui/dialog/dialog-content.svelte:36 focus:ring-2 ring idiom; focus: fires on mouse
relationships/relationship-viewer-modal.svelte ×5 ring-1 ring idiom

The checkbox's ring-ring/50 is the theme-blind alpha compositing DESIGN.md already bans for hover fills — the ban's wording just does not reach focus. Verified: 1.84:1 light, 2.28:1 dark, against a 3:1 floor.

Button having no indicator is the sharpest case: it is the most-used control in the app, and in the delete-confirmation dialog a keyboard user cannot see whether Enter cancels or removes a database.

Approach

A focused control adopts its --*-hover fill — the tokens shipped in #2673. Focus and hover look the same; both mean "this is the control you are about to act on." Precedent already exists and ships without complaint: dropdown-menu-item uses focus:bg-accent focus:text-accent-foreground.

focus-visible:bg-primary-hover        /* filled buttons reuse their hover token */
focus-visible:bg-hover-background     /* neutral surfaces */

Controls with no fill to shift get no focus treatment. Checkbox, switch and radio have only a border or track to change, and a border treatment is the idiom that has twice been removed from this codebase. They are left alone rather than given a treatment that invites the geometry change this issue exists to prevent.

Rules:

  • :focus-visible only, never bare :focus — bare :focus fires on mouse clicks and would put the treatment on the resting UI
  • Painted properties only, per the table above
  • Opaque tokens only; no alpha fills (the checkbox bug)
  • One idiom for the whole chrome surface, defined once

Acceptance Criteria

  • A single painted-only focus idiom replaces all five current ones across the chrome surface
  • Button, select-trigger, schema-field-leaf and nested-field-trigger have a visible focus treatment
  • Zero ring-*, ring-offset-*, outline (other than outline-none), border-width, or padding changes in any focus declaration, app-wide
  • No alpha-composited focus values; every focus treatment clears 3:1 against its surface in both themes
  • Bare :focus replaced by :focus-visible on radio-group-item and dialog-content
  • Checkbox, switch and radio have no focus treatment rather than a border-based one
  • The editor surface is untouched — the four "Removed :focus-visible border" sites still have no focus treatment
  • A test enumerates every focus utility in the codebase and fails on any that changes geometry, in the manner of filled-button-variants.test.ts
  • DESIGN.md records the painted-only rule and the editor/chrome distinction

Related

Activity

  1. malibio commented on Sep 17, 2026

    @malibio
    CollaboratorAuthor

    Frontend baseline: 251 test files, 5154 tests passed (59.15s).

  2. self-assigned this
    on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions