Nexus DSdocs

Focus

Every keyboard-reachable control paints the same ring: a real CSS outline in the focus-default colour, shown only for :focus-visible. Nothing is painted with box-shadow, so the ring follows the corner radius, never changes the box size, and never tints the surface underneath it. Tab through the specimens below.

The three recipes

Which recipe a component takes depends on what it is, not on whether it happens to have a border. Checkbox and RadioGroupItem carry real borders and still take the control recipe — the border is their own decoration, not part of the ring.

RecipeMembersRing
ControlCheckbox · RadioGroupItem · Switch · Toggle · Tabs triggernx:focus-visible:outline-2 nx:focus-visible:outline-focus-default
ButtonButton · every variantthe control ring + nx:focus-visible:outline-offset-2
FieldInput · Textarea · NativeSelect · SelectTrigger · MultiSelectTrigger · InputGroup · InputOTPSlot (active slot)nx:focus-visible:outline-default nx:focus-visible:outline-focus-default + nx:focus-visible:border-focus-default

A field ring is two halves

A field already has a border, so on focus that border recolours and becomes the ring’s inner half; the outline adds the outer half. outline-default is the outline-width twin of border-default — both are seeded from the same borderwidth primitive — so swapping the picker’s Border control scales the whole ring instead of thickening only the inside of it. The field keeps its border width at rest and focused, so focus never moves the text.

nx:border-default nx:border-border-default
nx:focus-visible:outline-default nx:focus-visible:outline-focus-default
nx:focus-visible:border-focus-default
nx:disabled:border-border-disabled

InputOTPSlot takes the same two halves under data-[active=true]: rather than focus-visible:: one transparent input sits over the slots, so the ring marks the slot the caret is in, on any focus. It has no error state. Slots overlap their full borders by one border width, so the active slot recolours all four of its sides without moving the row.

An invalid field wires an always-on error border plus an error-coloured ring on both properties. Tab into the field below to see it.

nx:aria-invalid:border-error-border
nx:aria-invalid:focus-visible:outline-focus-error
nx:aria-invalid:focus-visible:border-focus-error

One colour for every control

Primary, secondary, outline, ghost, destructive — they all focus in focus-default. Focus is a system signal (“you are here”), not a per-variant or status signal, so there is no per-variant focus colour. focus-default shares the solved primary-subtle-foreground colour, with focus surfaces included in its contrast checks. That is why the ring re-tints with the theme picker and component code never needs a brand-specific focus class. Only the error state differs, and it has its own token: focus-error.

Container focus is a separate, neutral token

border-focus is not a ring colour. It is the neutral grey border a container takes while focus is somewhere inside it — the Command search row and the DatePicker frame. It does not follow the brand. A focused control always uses focus-default; reach for border-focus only on the wrapper around it.

nx:focus-within:border-border-focus   /* container: neutral */
nx:focus-visible:outline-focus-default /* control: brand */

Where the ring does not go

The dividing line is input modality. Anything a keyboard user reaches with Tab gets the ring. Menu rows do not: Radix roving focus moves DOM focus to the row under the pointer, so a ring would flash for mouse users while giving them no steady indicator. Those rows tint from the surface they sit on instead.

SurfaceInstead
Menu and overlay rows — DropdownMenuItem, SelectItem, CommandItemnx:focus:bg-popover-hover
Destructive menu itemsnx:focus:bg-error-background
Containers — Card, Dialog body, popover surfacenothing; the ring lives on the focusable children

Two rules when you author one

Never pair nx:outline-none with a focus-outline class on the same element. Tailwind’s outline-none sets --tw-outline-style: none, and outline-<n> emits outline-style: var(--tw-outline-style) — so the ring silently never paints. To suppress a nested control’s own ring, scope the suppression to the variant instead: nx:focus-visible:outline-none.

Never use nx:transition-colors on a surface that paints a ring. Tailwind expands it to a list that includes outline-color, so the ring fades in over the duration instead of landing with the keypress. pnpm lint fails the pair wherever both land in one class string or one cva() / cn() call; two strings that only meet somewhere else pass, and the specimens on these docs pages are the one surface the check skips. Nexus ships the two ring-safe replacements — reach for those instead:

UtilityTransitionsUse on
nx:transition-controlcolor, background-color, border-colorControls — the border is decoration, so it may fade. Button adds scale to the same list
nx:transition-fieldcolor, background-colorField surfaces — the border is half the ring, so it must not fade