Tier 0Foundations · what focus must communicate, binding all four tiers. The ring drawn here is the web binding: iOS/tvOS use the system focus treatment, Android M3 state layers, Roku its own SceneGraph ring.--border-hover · --radius-lg · --component-bg-active · --shadow-focus
Focus system, three contexts, two answers
put.io has three focus contexts: keyboard (web), pointer (web, no ring), D-pad (TV). Web keyboard focus is the brand-yellow halo.
TV is different: it fills the surface with --component-bg-active and steps the border to --border-hover: no halo, no scale, no shadow. Never invent a third answer.
Keyboard · web
Tab / arrows over buttons, inputs, links
Soft 3px brand-yellow halo (35% alpha) around any button. Sits outside the border, never replaces it. Triggered by :focus-visible, not :focus.
Pointer · web
Mouse hover & click
No halo. Hover lifts background to --yellow-solid-hover on primary, or --component-bg-hover on default. Cursor carries the intent.
D-pad · TV
Apple TV · Android TV · Roku · Smart-TV web
tile
focused
A fill, not a lift: --component-bg-active + --border-hover. No scale, no halo, no shadow, matching list-item.tsx / button.tsx. Engine varies per platform (CSS on web / AndroidTV-web, focusable group on tvOS, BrightScript focus on Roku); the visual is identical.
Spec, values lifted from system/tokens.css
Ring color--shadow-focus-colorTranslucent brand yellow, hsla(47, 100%, 65%, 0.35) (Radix-style accent-a8, not --yellow-solid). Also the shadcn --ring color.
Ring · button (web)--shadow-focusSoft 3px translucent halo via box-shadow (Radix-style accent-a8). Border stays unchanged.
Ring · input (web)--field-ringQuiet, not yellow: border shifts to step-8 --border-hover + a 1px ring of the same color. Error/success states swap to red/green.
Focus · TV--component-bg-active10-foot focus is a surface fill, plus --border-hover on anything that already has a border. Yellow is not used for TV focus.
Trigger:focus-visibleNever :focus: that paints rings on clicks users don't want.