# Focus (/docs/focus)





Every focusable thing in the kit draws the same keyboard-focus ring. `focusStyles` is that ring,
exported so your own elements can wear it without copying class strings out of the kit's compiled
CSS.

```tsx
import { cn, focusStyles } from '@42-central/ui-react';

<button type="button" className={cn(focusStyles(), 'rounded-full')}>
  Custom trigger
</button>
```

Tab to the examples below to see it — the ring is `:focus-visible`, so it appears for keyboard
focus and stays out of the way on a mouse click.

<Preview align="block" code="&#x22;<Button>Kit component</Button>\n<button type=\&#x22;button\&#x22; className={cn(focusStyles(), 'rounded-md px-3 py-1.5')}>\n  Your own element\n</button>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <Button>
      Kit component
    </Button>

    <button
      type="button"
      className="cn(
      focusStyles(),
      &#x22;rounded-md border border-brand-900/20 bg-white/4 px-3 py-1.5 text-sm dark:border-white/15&#x22;,
    )"
    >
      Your own element
    </button>
  </div>
</Preview>

## The ring doesn't track `color` [#the-ring-doesnt-track-color]

Unlike every other visual axis in the kit, focus is deliberately **not** part of the
[color system](/docs/colors). It's a fixed `brand-500` ring, so a green badge and a red one
focus identically:

<Preview align="block" code="&#x22;<Badge asChild color=\&#x22;green\&#x22;><button type=\&#x22;button\&#x22;>Validated</button></Badge>\n<Badge asChild color=\&#x22;red\&#x22;><button type=\&#x22;button\&#x22;>Failed</button></Badge>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <Badge color="green">
      <button type="button">
        Validated
      </button>
    </Badge>

    <Badge color="red">
      <button type="button">
        Failed
      </button>
    </Badge>
  </div>
</Preview>

Focus is an accessibility affordance, not decoration — keeping it one recognisable shape means a
keyboard user never has to re-learn what "focused" looks like as they move across a page.

The exception is a control that sits *inside* an already-accented surface — `Pill`'s remove ×,
`Slider`'s thumb, `Rating`'s symbols, `Schedule`'s chips. There a brand ring reads as a second,
unrelated colour stacked on the one the surface already carries, so those pass
`focusStyles({ ring: 'palette' })` and ring in the active palette's `--c-text`:

```tsx
<button type="button" className={cn(focusStyles({ ring: 'palette', offset: false }))}>…</button>
```

`--c-text` rather than `--c-solid`, and at full opacity. It's the one slot that flips with the
theme — 700 in light, 300 in dark — so the ring can be dark on a pale surface and pale on a dark one.
`--c-solid` is 500 in *both*, which leaves it a mid-tone with nowhere to go against a light
background; measured on a Schedule chip it rings at 2.69:1 against the grid, where
[SC 1.4.11](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast) asks for 3:1. Lowering the
opacity can't rescue that — alpha only composites the ring *toward* whatever sits behind it.

## Display components [#display-components]

`Badge`, `Kbd` and `ThemeIcon` are display elements — a `<span>`, a `<kbd>`, a `<div>`. They carry
the ring classes anyway, which costs nothing: `:focus-visible` never matches a non-focusable
element. The moment you compose one onto a real control with `asChild`, the ring is already there.

<Preview align="block" code="&#x22;<Kbd asChild>\n  <button type=\&#x22;button\&#x22; onClick={openPalette}>⌘K</button>\n</Kbd>&#x22;">
  <Kbd>
    <button type="button">
      ⌘K
    </button>
  </Kbd>
</Preview>

This also means you don't have to tell the component it's interactive — there's no `interactive`
prop to forget, and nothing that can fall out of sync with whether the element is actually
focusable. If you reach for `tabIndex` instead of `asChild`, that works too.

## Turning off the offset [#turning-off-the-offset]

By default the ring sits 2px clear of the element, over a background-colored gap, so it stays
legible against the element's own fill. For something flush inside a container — a cell in a grid,
a row in a scrolling list — that gap punches a hole in the surrounding surface. Pass
`offset: false`:

```tsx
<div className={cn(focusStyles({ offset: false }), 'border-b')} tabIndex={0}>
  Flush row
</div>
```

The rule behind that: **`offset: true` means the gap is painted in the *page* background** (white,
or `gray-dark-950` in dark). An element sitting on any other surface — a tinted table header, a
coloured chip — needs a gap that matches *that* surface, so it turns the offset off and supplies
its own pair. `DataTable`'s sortable header is the worked example: its `thead` is `bg-brand-50/90`,
so a page-coloured gap would read as a white band cut out of the header.

```tsx
cn(
  focusStyles({ offset: false }),
  'focus-visible:ring-offset-2 focus-visible:ring-offset-brand-50 dark:focus-visible:ring-offset-brand-950',
)
```

## Rows stacked in a scroller [#rows-stacked-in-a-scroller]

Rows that sit flush against each other inside a scrolling list — `TreeSelect`'s tree — can't take
an outer ring at all: the scroller clips it at the edges, and the next row paints over its bottom
edge. `inset: true` draws the ring inside the element's own edge instead. Pair it with
`offset: false`:

```tsx
<div className={cn(focusStyles({ offset: false, inset: true }), 'rounded-sm')} tabIndex={-1}>
  Tree row
</div>
```

## Controls whose focus lives elsewhere [#controls-whose-focus-lives-elsewhere]

Some controls put the real focus on a visually-hidden `<input>` and mirror the state onto the parts
you can actually see — `Checkbox`, `RadioGroup`, `Switch`, `ChoiceCardGroup`. `:focus-visible` can
never match those parts, because they are never focused. Zag sets a `data-focus-visible` attribute
on them instead, and `source` keys the ring off that:

```tsx
focusStyles({ source: 'data-focus-visible' });
```

This source emits no `outline-none`: the part it lands on isn't focusable, so there's no native
outline to suppress.

<Callout type="info">
  Most consumers never need this — reach for it only when you're styling a part of an Ark/Zag
  primitive yourself. Zag only sets the attribute for *keyboard* focus, so the ring stays out of the
  way on a mouse click, exactly like `:focus-visible`.
</Callout>

## API [#api]

```ts
import { focusStyles, type FocusStylesOptions } from '@42-central/ui-react';

focusStyles();                                 // brand ring + offset (default)
focusStyles({ offset: false });                // brand ring only
focusStyles({ ring: 'palette' });              // --c-text, for a nested control
focusStyles({ offset: false, inset: true });   // inside the edge, for flush rows
focusStyles({ source: 'data-focus-visible' }); // Zag-driven controls
```

Width and alpha are deliberately *not* options. Every ring the kit draws is 2px and fully opaque —
that consistency is the whole point of having one utility.

<TypeTable
  type="{
  source: {
    type: '&#x22;focus-visible&#x22; | &#x22;data-focus-visible&#x22;',
    default: '&#x22;focus-visible&#x22;',
    description:
      &#x22;Where the focused state lives: the CSS pseudo-class, or Zag's attribute for a control whose real focus target is a visually-hidden input.&#x22;,
  },
  ring: {
    type: '&#x22;brand&#x22; | &#x22;palette&#x22;',
    default: '&#x22;brand&#x22;',
    description:
      &#x22;Ring colour: the kit's fixed brand-500, or the active palette's --c-text for a control nested inside an accented surface.&#x22;,
  },
  offset: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description:
      &#x22;Whether the ring sits clear of the element with a gap painted in the page background. Turn it off for elements flush inside a container, or sitting on any surface that isn't the page background.&#x22;,
  },
  inset: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description:
      &#x22;Draw the ring inside the element's edge, for rows stacked flush in a scroller where an outer ring would be clipped. Pair with offset: false.&#x22;,
  },
}"
/>

`focusStyles` lives on the package root (`@42-central/ui-react`), alongside [`cn`](/docs/theming) and
[`COLORS`](/docs/colors). It returns a plain class string, so it composes with `cn` like any other.

<Callout type="info">
  Your Tailwind build needs to see the kit's classes for these to be generated. The
  `@source "../node_modules/@42-central/ui-react/dist/**/*.js"` line from the
  [install guide](/docs) already covers it.
</Callout>
