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.
| Recipe | Members | Ring |
|---|---|---|
| Control | Checkbox · RadioGroupItem · Switch · Toggle · Tabs trigger | nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default |
| Button | Button · every variant | the control ring + nx:focus-visible:outline-offset-2 |
| Field | Input · 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.
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.
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.
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.
| Surface | Instead |
|---|---|
| Menu rows: DropdownMenuItem, SelectItem, CommandItem | nx:focus:bg-popover-hover |
| Destructive menu items | nx:focus:bg-error-background |
| Containers — Card, Dialog body, popover surface | No 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:
| Utility | Transitions | Use on |
|---|---|---|
| nx:transition-control | color, background-color, border-color | Controls. Button also transitions scale. |
| nx:transition-field | color, background-color | Fields. The border is half the ring, so it must not fade. |