# Tool Button

> A small button in a glass toolbar, with its label and key taught in a tooltip. A press can leave its result in place, with Undo and Retry.

- Name: `tool-button`
- Status: beta
- Categories: actions
- File: the install writes `tool-button.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`, `class-variance-authority`. The install adds them.
- Other registry items: `@arbokui/motion`, `@arbokui/tooltip`, `@arbokui/kbd`. The install adds them.
- Gallery: https://arbokui.com/components/tool-button
- Registry item: https://arbokui.com/r/tool-button.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/tool-button
   ```

## Use it for

- Small toolbars: the quick actions on a row, an editor's formatting bar, a bar of bulk actions.
- Put ToolButtons and ToolDividers inside a Toolbar. The toolbar is one Tab stop, and the arrow keys move between its buttons.
- A ToolButton alone, outside a Toolbar, is a single quiet button with the same tooltip and result, like Copy on a code block.
- Give every button a label. It names the button and fills its tooltip, so an icon button is never unnamed.
- Give shortcut the key the app listens for, like R. The tooltip teaches it. ActionKeys wires single keys for you.
- Give result to a press that changes something, like a reminder. The button says what happened, offers Undo with onUndo, and commits when the window ends.
- Return a promise from onClick to show a spinner until it settles. A failure turns the button into Retry.
- A change with no button, like a dragged row, calls undoable(result) to join the page's ⌘Z stack. A Toaster calls setToastSlot, and then takes the Undo or Retry of a button that leaves.
- Use pressed for a toggle, like Bold, and keepFocus so a press leaves the text selection alone.

## Avoid it for

- A page's main action, like Save. Use Button.
- A toast that repeats what the button already says. The button carries the result.
- More than a handful of buttons in a row's quick actions. Keep two to four, and put the rest in a Menu.

## Props

- `Toolbar`: `ComponentProps<"div"> & { orientation?: "horizontal" | "vertical"; "aria-label"?: string }`. Root. The glass bar. role=toolbar, one Tab stop, and arrow keys between its buttons. Name it with aria-label.
- `ToolButton`: `component`. Part. One button in the toolbar.
- `ToolDivider`: `component`. Part. The thin line between groups of buttons.
- `label`: `string`. ToolButton. Required. What it does, like Reply. It names the button and fills its tooltip.
- `icon`: `ReactNode`. ToolButton. The icon, like a lucide icon. With no icon, the label shows as text.
- `text`: `boolean`. Default `false`. ToolButton. Shows the label beside the icon.
- `shortcut`: `string`. ToolButton. The key as the tooltip teaches it, like R or ⌘B. A single key also sets aria-keyshortcuts and data-shortcut, which ActionKeys reads.
- `pressed`: `boolean`. ToolButton. Makes it a toggle. Sets aria-pressed.
- `tone`: `"neutral" | "danger"`. Default `"neutral"`. ToolButton. Color role. Use danger for a destructive action.
- `keepFocus`: `boolean`. Default `false`. ToolButton. A press leaves focus and the text selection where they are, as an editor's toolbar needs.
- `tooltip`: `boolean`. Default `true`. ToolButton. Shows the tooltip.
- `loading`: `boolean`. Default `false`. ToolButton. Shows a spinner and blocks presses. The button stays focusable.
- `disabled`: `boolean`. Default `false`. ToolButton. Blocks presses. The button stays focusable, so its tooltip still shows.
- `onClick`: `(event?: MouseEvent) => void | Promise<unknown>`. ToolButton. Runs on press. Return a promise to show a spinner until it settles. A rejection turns the button into Retry.
- `result`: `ActionResult`. ToolButton. What the press leaves behind. The button shows result.label with a check, offers Undo while the window lasts if onUndo is set, then commits and stays as result.doneLabel, a record that can't be pressed.
- `ActionResult`: `{ label: string; doneLabel?: string; say?: string; failed?: string; onUndo?: () => void; undone?: string; onCommit?: () => void | Promise<unknown>; seconds?: number; toast?: string }`. Type. seconds is the Undo window, default 8. onCommit runs when it ends, or when the button leaves the page mid-window. A rejected onCommit turns the button into Retry. toast goes to the toast slot, only for what the button can't show.
- `useActionResult`: `({ label, onPress, result, onSlip?, live? }) => { phase, armed, undoShown, telling, choice, failText, press, handlers, message }`. Hook. The same press-to-result flow for a control of your own, on the same ⌘Z stack. Call press from its click, spread handlers on it, and render message in a polite live region. Pass live: 'page' for a control that may leave before it is heard, like one in a peek, and the page's one region says it instead.
- `undoable`: `(result: ActionResult) => { undo, restart }`. Puts a change with no control, like a saved inline edit, on the page's ⌘Z stack for its window. restart gives it the whole window again.
- `setToastSlot`: `(slot: ((toast: ResultToast) => () => void) | null) => void`. For a Toaster. A button that leaves mid-window hands it its Undo, and one that leaves showing Retry hands it Retry. With no slot, it commits as before.
- `toolbarVariants`: `() => string`. The toolbar's glass look, for a bar of your own.
- `className`: `string`. Toolbar, ToolButton, or ToolDivider. Extra classes. Later classes win.

## Accessibility

- Toolbar has role=toolbar. It is one Tab stop, and the arrow keys move between its buttons, with Home and End to the ends. Give it an aria-label.
- Every ToolButton is named by its label, as aria-label. The type requires a label.
- A single-key shortcut sets aria-keyshortcuts. The tooltip shows the key, but only to sighted mouse and keyboard users.
- The button's name follows its state: the result once pressed, '… Press to undo.' while Undo is on offer, the record once done, and 'Retry: …' after a failure.
- The page's one polite live region says the result, Undone, or what failed, once. So it is still heard after a hover bar hides the button or its row leaves, and while a modal dialog is open.
- After a mouse press, Undo arms only once the pointer leaves. After a key or touch press it arms in 1.2 seconds, and a touch press then shows Undo. So a double press never undoes. The window holds while an armed button is pointed at or has keyboard focus.
- Disabled, loading, and done buttons stay focusable, so their tooltips still show.
- ⌘Z or Ctrl+Z undoes the newest change still in its window, whatever shows it: a button, a peek, or undoable(). In a text field it stays the field's own undo.

## Examples

### A formatting toolbar

```tsx
import { Bold, Italic, Link } from "lucide-react";
import { useState } from "react";

function FormatBar() {
  const [bold, setBold] = useState(false);
  return (
    <Toolbar aria-label="Formatting">
      <ToolButton icon={<Bold />} label="Bold" shortcut="⌘B" aria-keyshortcuts="Meta+B" pressed={bold} keepFocus onClick={() => setBold(!bold)} />
      <ToolButton icon={<Italic />} label="Italic" shortcut="⌘I" aria-keyshortcuts="Meta+I" keepFocus />
      <ToolDivider />
      <ToolButton icon={<Link />} label="Add link" keepFocus />
    </Toolbar>
  );
}
```

### A press that leaves its result, with Undo

```tsx
import { BellRing } from "lucide-react";

function RemindButton({ send, cancel }: { send: () => Promise<void>; cancel: () => void }) {
  return (
    <Toolbar aria-label="Person">
      <ToolButton
        icon={<BellRing />}
        label="Remind"
        text
        shortcut="R"
        result={{ label: "Sent", doneLabel: "Reminded", say: "Reminder sent to Tom Hale", onUndo: cancel, onCommit: send }}
      />
    </Toolbar>
  );
}
```
