Skip to main content
Nexus Design System
GitHub

Focus

Every control reachable with Tab shows the same focus ring. The ring is a CSS outline in the focus-default color, shown on :focus-visible.

The ring follows the corner radius and never changes the control’s size. Press Tab to move through the examples on this page.

The three recipes

Each component uses one of three ring recipes (a fixed set of focus classes), based on what kind of control it is. Checkbox and RadioGroupItem use the control recipe. Their border is 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

On focus, a field’s border changes color and forms the inner half of the ring. The outline adds the outer half.

outline-default and border-default share one width, so the whole ring scales with stroke (Border Width in the theme picker). The border width doesn’t change on focus, so the text never moves.

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 uses the same two halves, but on data-[active=true]: instead of focus-visible:. The ring marks the slot with the caret. InputOTPSlot has no error state.

An invalid field always shows an error border. On focus, both halves of the ring turn the error color. Press Tab to focus the invalid field example.

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

One color for every control

Every variant uses focus-default, including destructive buttons. The ring says “you are here”, not what the control does.

focus-default follows the brand color, so you never need a brand-specific focus class. Invalid fields use focus-error.

Container focus is a separate, neutral token

border-focus is the gray border a container shows while something inside it has focus. Command’s search row and DatePicker’s frame use it.

border-focus doesn’t follow the brand. Use focus-default on the focused control, and 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

Anything you reach with Tab gets the ring. Menu rows don’t, because they take focus when the pointer moves over them, and a ring would flash for mouse users. Menu rows change background instead.

SurfaceInstead
Menu rows: DropdownMenuItem, SelectItem, CommandItemnx:focus:bg-popover-hover
Destructive menu itemsnx:focus:bg-error-background
Containers — Card, Dialog body, popover surfaceNo ring. The controls inside get it.

Write your own focus styles

Never pair nx:outline-none with a focus-outline class on the same element. outline-none hides every outline on the element, so the ring never shows. To hide a nested control’s ring, use nx:focus-visible:outline-none instead.

Never use nx:transition-colors on a surface that paints a ring. transition-colors animates outline-color, so the ring fades in instead of appearing on the keypress.

The nxds ESLint rules flag transition-colors and a ring class in the same class string, cva() or cn() call. Use nx:transition-control or nx:transition-field instead:

UtilityTransitionsUse on
nx:transition-controlcolor, background-color, border-colorControls. Button also transitions scale.
nx:transition-fieldcolor, background-colorFields. The border is half the ring, so it must not fade.