# Button and focus (/cloud/desktop/button)



```ts title="Import"
import {
  Button,
  focus,
  formatTimeAgo,
  Hint,
  HintIconButton,
  HintToggleIconButton,
  IconButton,
  Kbd,
  srOnly,
  StatusDot,
  ToggleIconButton,
} from "../components/ui.tsx";
```

File: `packages/desktop/src/renderer/src/components/ui.tsx`. "Small shared controls, styled on the
palette."

## `focus` [#focus]

Two StyleX styles, and the only two focus treatments the desktop uses. From the file's comment:

> Keyboard focus must be visible to navigate at all: `ring` for standalone controls, `ringInset`
> for rows and menu items that sit flush inside a scroll container or popover, where an outset
> ring would clip.
>
> `t.focusRing` — not `:focus-visible` — is what keeps these off the mouse. Chromium matches
> `:focus-visible` on every text field focus, so a plain ring lands on any input the user clicked
> into; focus-modality.ts drops the color to `transparent` until focus arrives by keyboard.

| Style             | Outline                                                   |
| ----------------- | --------------------------------------------------------- |
| `focus.ring`      | 2px solid `t.focusRing`, offset 1px, on `:focus-visible`  |
| `focus.ringInset` | 2px solid `t.focusRing`, offset −2px, on `:focus-visible` |

Rule: every focusable in the desktop **must** apply one of these. A control that draws its own
outline is a defect. The mechanism that gates the color is in
[Foundations › Focus](/cloud/foundations/focus).

## `srOnly` [#sronly]

A visually-hidden style (1px clipped box) for text that must be announced but not seen. Used for
tool-call state ("Running", "Failed"), live regions, and labels on controls that read by shape.

## `Button` [#button]

Wraps `@nyte-ai/ui/button` with `render={<button type={type} {...rest} />}`.

| Prop       | Type                                              | Default       | Description                                            |
| ---------- | ------------------------------------------------- | ------------- | ------------------------------------------------------ |
| `variant`  | `"primary" \| "secondary" \| "ghost" \| "danger"` | `"secondary"` | Fill treatment on `t.fill*`.                           |
| `icon`     | `IconName`                                        |               | Leading 14px glyph from [Icons](/cloud/desktop/icons). |
| `type`     | button type                                       | `"button"`    |                                                        |
| `disabled` | `boolean`                                         |               | Forwarded to Base UI.                                  |
| …rest      | `<button>` props minus `className`/`style`        |               | Spread onto the rendered element.                      |

Note the default variant is `secondary`, the opposite of `@nyte-ai/ui`'s Button. Primary is
opt-in because most desktop buttons sit in toolbars and footers.

```tsx
<Button variant="primary" onClick={save}>Save</Button>
<Button icon="refresh" onClick={refresh}>Refresh GitHub</Button>
<Button variant="danger" disabled={pending}>{pending ? "Deleting…" : "Delete"}</Button>
```

## `IconButton` [#iconbutton]

A ghost square button for one glyph.

| Prop      | Type                       | Default | Description                                                   |
| --------- | -------------------------- | ------- | ------------------------------------------------------------- |
| `icon`    | `IconName \| ReactElement` |         | A named glyph, or a custom element such as `PanelToggleIcon`. |
| `label`   | `string`                   |         | Required. Becomes `aria-label` and `title`.                   |
| `size`    | `number`                   | `15`    | Glyph size.                                                   |
| `compact` | `boolean`                  | `false` | Smaller box for tab strips and panel headers.                 |

## `ToggleIconButton` [#toggleiconbutton]

Same as `IconButton` on `@nyte-ai/ui/toggle`, so it carries `aria-pressed`.

| Prop              | Type                         | Description       |
| ----------------- | ---------------------------- | ----------------- |
| `pressed`         | `boolean`                    | Controlled state. |
| `onPressedChange` | `(pressed: boolean) => void` |                   |

`aria-pressed` and `onClick` are removed from the accepted rest props; Base UI Toggle owns them.

## `Hint`, `HintIconButton`, `HintToggleIconButton` [#hint-hinticonbutton-hinttoggleiconbutton]

`Hint` is the desktop tooltip on `@nyte-ai/ui/tooltip`: `positionMethod="fixed"`, `sideOffset` 6,
`collisionPadding` 8.

| Prop      | Type                                | Default    |
| --------- | ----------------------------------- | ---------- |
| `content` | `ReactNode`                         |            |
| `trigger` | `ReactElement`                      |            |
| `side`    | `Tooltip.Positioner.Props["side"]`  | `"bottom"` |
| `align`   | `Tooltip.Positioner.Props["align"]` | `"center"` |

`HintIconButton` and `HintToggleIconButton` wrap the two buttons in a `Hint` whose content defaults
to the button's `label`, and drop the native `title` so the tooltip is the only hint. Props:
the button's props plus `hint`, `hintSide`, `hintAlign`.

Tooltip keyboard and focus behavior is Base UI's: opens on hover after the delay and on keyboard
focus, closes on Escape, never traps focus.

## `StatusDot` [#statusdot]

A `role="img"` span with an `aria-label` for a session's mark.

| `mark`    | Label           | Draws                                    |
| --------- | --------------- | ---------------------------------------- |
| `idle`    | Idle            | dot                                      |
| `working` | Running         | dot + [Spinner](/cloud/desktop/feedback) |
| `waiting` | Needs attention | dot                                      |
| `retry`   | Retrying        | dot + Spinner                            |
| `failed`  | Failed          | dot                                      |

The switch over `SessionMark` is exhaustive with a `never` guard; adding a mark is a type error
until the label and style exist.

## `Kbd` [#kbd]

```tsx
<Kbd keys={["⌘", "K"]} />
<Kbd keys={["⌘", "N"]} plain />
```

Renders `<kbd>` with one `<span>` per key. `plain` removes the keycap box for use inside menu
meta columns. `Kbd` is display only; the shortcut it shows is bound elsewhere (see the
[client action table](/cloud/surfaces/command-palette#client-actions)).

## `formatTimeAgo(timestamp, now?)` [#formattimeagotimestamp-now]

"now", `Nm`, `Nh`, `Nd`, then a short date. Used for session rows and palette results.
