# Button

> A button for an action, with or without a label; its color says what kind of action it is, and its size follows where it sits.

Source: https://raft-design.botiverse.build/components/button. Generated from the current release; where it differs from what you remember, this page is right.

A button starts an action. Its color says what kind of action it is: pink for the one action a screen exists for, an ink border for the rest, no frame for cancel and minor actions, orange for what interrupts and red for what cannot be undone. An icon-only button is the same component at an icon size.

```tsx
import { Button } from "@botiverse/raft-ui";

export function ButtonPreviewContent() {
  return <Button size="md">Button</Button>;
}
```

## When to use

| Situation                                                                                 | Use                                                                  |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| An action with a label                                                                    | Button                                                               |
| An action whose icon is understood without words: search, more, back, close, copy, attach | Button at an icon size; see [Icon buttons](#icon-buttons)            |
| An action whose icon needs words to be understood                                         | Button with a label                                                  |
| The quietest control on a screen, action or not                                           | [TextLink](https://raft-design.botiverse.build/components/text-link.md): text that underlines on hover |
| A list of actions on one thing                                                            | [DropdownMenu](https://raft-design.botiverse.build/components/dropdown-menu.md)                    |
| Picking a value                                                                           | [Select](https://raft-design.botiverse.build/components/select.md)                                        |

## Variants

| Variant              | Use for                                                                                      | Examples                             |
| -------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------ |
| `variant="primary"`  | The action the screen exists for: a pink sticker                                             | Send, Create Channel, Bring an Agent |
| `variant="default"`  | Every other action, including one that every row of a list repeats: an ink border            | Assign, Approve, Claim               |
| `variant="ghost"`    | Cancel and minor actions inside a surface that draws its own edge: no frame                  | Cancel in a dialog                   |
| `variant="warning"`  | Actions that interrupt but can be undone: an orange sticker                                  | Stop Wug, Leave Channel, Reset       |
| `variant="danger"`   | Actions that cannot be undone, on the confirm button of a confirmation dialog: a red sticker | Delete Agent                         |
| `variant="on-frame"` | A frameless button on the frame: the rail, the web top bar, the desktop title bar            | The workspace button                 |

`accent` renders the same as `primary`, and `outline`, `information`, `muted` and `success` the same as `default`. Use the names in the table.

```tsx
import { useCallback, useState } from "react";
import { ArrowLeft, ArrowRight, ChevronDown, History } from "lucide-react";
import {
  Button,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@botiverse/raft-ui";

const LAST_PAGE = 2;

export function ButtonOnFrameComposition() {
  const [page, setPage] = useState(LAST_PAGE);
  const [workspace, setWorkspace] = useState("Acme");
  const handleBack = useCallback(() => setPage((current) => Math.max(0, current - 1)), []);
  const handleForward = useCallback(
    () => setPage((current) => Math.min(LAST_PAGE, current + 1)),
    [],
  );
  const handleHistory = useCallback((value: string) => setPage(Number(value)), []);

  return (
    <div className="flex items-center gap-1 rounded-control bg-layer-frame p-1.5">
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button variant="on-frame" />}>
          {workspace}
          <ChevronDown data-icon="inline-end" />
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuRadioGroup value={workspace} onValueChange={setWorkspace}>
            <DropdownMenuRadioItem value="Acme">Acme</DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="Botiverse">Botiverse</DropdownMenuRadioItem>
          </DropdownMenuRadioGroup>
        </DropdownMenuContent>
      </DropdownMenu>
      <Button
        variant="on-frame"
        size="icon-md"
        aria-label="Back"
        disabled={page === 0}
        focusableWhenDisabled
        onClick={handleBack}
      >
        <ArrowLeft />
      </Button>
      <Button
        variant="on-frame"
        size="icon-md"
        aria-label="Forward"
        disabled={page === LAST_PAGE}
        focusableWhenDisabled
        onClick={handleForward}
      >
        <ArrowRight />
      </Button>
      <DropdownMenu>
        <DropdownMenuTrigger
          render={<Button variant="on-frame" size="icon-md" />}
          aria-label="History"
        >
          <History />
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuRadioGroup value={String(page)} onValueChange={handleHistory}>
            <DropdownMenuRadioItem value="2">Wug</DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="1">#release-review</DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="0">#launch-week</DropdownMenuRadioItem>
          </DropdownMenuRadioGroup>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
}
```

On the frame, a button takes the frame's ink (`foreground-on-frame`) and lays `fill-frame-medium` over itself on hover, on press and while its popup is open. The text and icon sizes become the 36px frame item.

### Which color

**Emphasis is relative to the surface that holds the button, not a property of the action.** A frameless Cancel reads as a control inside a dialog and as a line of text on a page; the action did not change, what encloses it did. The test is whether the surface draws its own edge and takes focus while it is open — a dialog, a sheet, a menu, a popover, a toolbar, a card, a panel header all do; a page and a section of a page do not. When a surface is not on that list, apply the test rather than guessing: if the button would be the only thing marking where it sits, give it a border.

Every button is flat: the ink border or the sticker marks it as a control, never a shadow ([Elevation](https://raft-design.botiverse.build/foundations/elevation.md)). Colored buttons carry ink type ([Color](https://raft-design.botiverse.build/foundations/color.md#stickers)).

- Make the primary action pink. Use at most one pink button per screen; a screen may have none. Unread counts and attention dots don't count toward the one.
- When a screen has a second primary action, make it `default`.
- Give every other action an ink border (`default`).
- Make cancel and minor actions frameless (`ghost`) inside a surface that draws its own edge. On a page, give the same action an ink border (`default`).
- Use [TextLink](https://raft-design.botiverse.build/components/text-link.md) for the bottom of the ladder: where even a frameless button is too much weight, such as Forgot Password on a sign-in page or a See All beside a section title. It may carry an action, but it is the quietest control on the screen, never a way to fit one more button into a crowded row. It is not a Button variant: a text link is a word in a line, not a box, so it takes the size of the text around it, and a Button would bring a height, a border and padding that each have to be taken off again.
- Use orange (`warning`) for actions that interrupt but can be undone: stop, leave, reset.
- Use red (`danger`) only for actions that cannot be undone, and only in a confirmation dialog ([AlertDialog](https://raft-design.botiverse.build/components/alert-dialog.md)). Don't color a destructive item in a menu ([DropdownMenu](https://raft-design.botiverse.build/components/dropdown-menu.md)).
- Don't use `danger-secondary` or `danger-outline`: red text on an outline button puts red outside a confirmation dialog.
- Never make a button yellow: yellow means "you" ([Color](https://raft-design.botiverse.build/foundations/color.md#meanings-of-color)).

### Sizes

Size follows the purpose. The same purpose has the same size everywhere, and there are no other sizes. `lg`, `icon-lg` and `xl` take the control height ([Spacing](https://raft-design.botiverse.build/foundations/spacing.md#heights)): a finger's size on a phone and a touch screen, and Feishu's PC sizes on a desktop (from 768px with a mouse or trackpad). On a desktop `lg` is as tall as `md`, with its wider padding and its `text-subhead` label. Still pick `lg` by purpose, as the table says: the same code runs on a phone, where `lg` is the finger-sized button the purpose needs, and on a desktop it costs nothing in height.

| Size                  | Height                                             | Use for                                                                                                                                                 |
| --------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `size="lg"`           | 44px on a phone; 32px, 44px hit area, on a desktop | Dialog and drawer footers, form submit buttons, a full-width column of actions at the bottom of a panel (Remove Member, Leave Channel, Archive Channel) |
| `size="md"` (default) | 32px, 44px hit area                                | Actions in list rows, cards, banners, toolbars and panel headers                                                                                        |
| `size="xl"`           | 52px on a phone, 40px on a desktop                 | Only the full-width pink button across a sign-in, sign-up or setup form                                                                                 |

These three and the two icon sizes, `icon-md` and `icon-lg`, are all the sizes. `xs` and `sm` render exactly as `md`, and `icon-xs` and `icon-sm` exactly as `icon-md`. They are deprecated aliases, kept until raft-ui 1.0. `node node_modules/@botiverse/raft-ui/codemods/button-sizes.mjs <src>` renames them, and nothing on screen changes.

When a place looks too tight for a 32px button, change the container, not the button. A banner with a button is a standard banner, not `sm`.

#### Size by place

Every place has one answer. Where a part of the container sets the place, follow it: the size comes with the place, not with the button.

| Place                                                                       | Size                                   | Who decides                                                                                               |
| --------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Dialog, drawer and sheet footers                                            | `lg`                                   | `DialogFooter`: Cancel and the submit button are `lg`. `DialogCommit` moved into a phone's header is `md` |
| A form's submit; Save on a card that holds fields                           | `lg`                                   | You, because fields are above it                                                                          |
| Beside a field: copy a link, remove a row, add                              | `lg`, `icon-lg`                        | You: it takes the field's height ([Icon buttons](#icon-buttons))                                          |
| A full-width button across a sign-in, sign-up or setup form                 | `xl`                                   | You                                                                                                       |
| A full-width column of actions at the bottom of a panel                     | `lg`                                   | You                                                                                                       |
| A banner's button and its Dismiss                                           | `md`                                   | `BannerAction` places them; `BannerClose` is its own 32px ×                                               |
| A toast's action                                                            | none                                   | `ToastAction` is the toast's own text action; it takes no Button size                                     |
| A desktop panel header                                                      | none                                   | `PanelAction` and `PanelToggleAction` are their own buttons, 32px on a desktop and 44px on a phone        |
| Card and message action card actions; a single action in a settings section | `md`                                   | You                                                                                                       |
| The end of a list row or a table row                                        | `md`, `icon-md`                        | You                                                                                                       |
| Toolbars, filter bars, empty states, the bottom of a popover, Load More     | `md`, `icon-md`                        | You                                                                                                       |
| A phone top bar; the media viewer                                           | `icon-lg`                              | The top bar, `Lightbox`                                                                                   |
| The rail, the web top bar, the desktop title bar                            | `on-frame` at `md` or `icon-md` (36px) | The frame                                                                                                 |
| A menu, a phone's overflow menu                                             | No Button: a menu item                 | `DropdownMenu`, the sheet                                                                                 |

#### Inside a floating toolbar

A toolbar that floats over content shows on every message under the pointer, so it is more compact than the page. The message hover toolbar and the text-selection toolbar are 36px high, with 28px buttons 4px from the frame and 4px apart, at `radius-chip`. The container sizes them: `MessageItemToolbarButton` and `MessageTextSelectionToolbarButton` are the toolbars' own parts, and `MessageMultiSelectToolbarActions` brings a `md` Button in it to 28px ([MessageItem](https://raft-design.botiverse.build/components/message-item.md)). The only other 28px button is `SidebarSectionControl` on a sidebar section's 16px header line. 28px is not a Button size, so don't make a Button 28px yourself.

### Icon buttons

| Where                                                                   | Props                               | Size and icon                                                           |
| ----------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------- |
| A phone top bar, or beside a field                                      | `variant="ghost" size="icon-lg"`    | 44px, 22px icon on a phone; 32px, 18px icon, 44px hit area on a desktop |
| A desktop panel header, composer tools, copy in cards and command lines | `variant="ghost" size="icon-md"`    | 32px, 18px icon, 44px hit area                                          |
| A tool that stands alone in content                                     | `variant="default"` at an icon size | Ink border on `layer-card`                                              |
| Send                                                                    | `variant="primary"` at an icon size | A pink sticker                                                          |
| The rail, the web top bar, the desktop title bar                        | `variant="on-frame" size="icon-md"` | 36px, 20px icon                                                         |

- Make an icon button next to an input (copy a link, remove a row) the input's height: `size="icon-lg"`, which follows the field's height on a phone and on a desktop.
- Put several icon buttons in a panel header inside `PanelActions` ([Panel](https://raft-design.botiverse.build/components/panel.md)), which aligns them to the end and spaces them.
- Mark a bell that has something new with an attention dot on its top-right corner.
- Count the send button as the screen's pink button.

Icon sizes by context are in [Iconography](https://raft-design.botiverse.build/foundations/iconography.md#sizes).

## States

Hover, press, focus and disabled follow [States](https://raft-design.botiverse.build/foundations/states.md): every button hovers and presses with the medium tier ([Hover and press](https://raft-design.botiverse.build/foundations/states.md#hover-and-press)): a sticker lays `sticker-medium` over itself, a paper-colored button `fill-medium`.

- **Disabled, frameless:** a frameless icon button (`ghost` at an icon size) and an `on-frame` button only turn their icon and text `foreground-disabled`. No fill or border appears.
- **Disabled, colored:** a disabled pink, orange or red button is no longer a sticker: it takes the shared disabled look.
- **Loading:** `loading` shows the pixel spinner ([Spinner](https://raft-design.botiverse.build/components/spinner.md)) and keeps the button's width and color. Use it for any wait a button started ([Choosing a component](https://raft-design.botiverse.build/components.md#waiting)).

## Content

- Start with a verb and name the result: Send for Review, not OK or Submit. In a dialog the title names the object, so its main button says the verb alone: Create in Create Channel ([Writing](https://raft-design.botiverse.build/content/writing.md#actions-and-their-dialogs)).
- Use Title Case ([Writing](https://raft-design.botiverse.build/content/writing.md#casing)), and repeat the button's words in the dialog it opens ([Writing](https://raft-design.botiverse.build/content/writing.md#actions-and-their-dialogs)).
- Put an icon before the label (`data-icon="inline-start"`), and Return or an arrow after it (`data-icon="inline-end"`).
- While loading, say what is happening: Creating….

## Accessibility

Button wraps Base UI Button and renders a native `<button>`. For navigation, render a real link instead: `render={<a href="…" />}`.

- Give every icon-only button an `aria-label` in Title Case (Add Reaction). On devices with a pointer, add a [Tooltip](https://raft-design.botiverse.build/components/tooltip.md) with the same name and the shortcut.
- While `loading`, the button is `aria-disabled` and the spinner is named by `loadingLabel`. Set `loadingLabel` to what is happening.
- Use `focusableWhenDisabled` when a Tooltip has to explain why an action is unavailable: the button stays focusable and keeps the disabled look.

## API

### Button

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `false` |  |
| `loadingLabel` | `string` | `"Loading"` |  |
| `size` | `"md" \| "lg" \| "xl" \| "icon-md" \| "icon-lg" \| DeprecatedButtonSize` | `"md"` | `md` (32px) and `icon-md` for rows, cards, banners, toolbars and panel headers; `lg` and `icon-lg` (the control height) for dialog footers, forms and beside a field; `xl` for a full-width call to action. The size follows the place: see Button › Size by place. |
| `variant` | `"primary" \| "accent" \| "warning" \| "danger" \| "default" \| "outline" \| "information" \| "muted" \| "success" \| "danger-secondary" \| "danger-outline" \| "ghost" \| "on-frame"` | `"default"` |  |
| `focusableWhenDisabled` | `boolean` | `false` | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.8.0 (Breaking Changes): **`Button` no longer has `variant="link"` or `size="inline"`.** Underlined text is now `TextLink`. A link inside a sentence (a banner's "View Billing") becomes `<TextLink variant="strong">`; a link on its own (Forgot Password, See All) becomes `<TextLink>`, which is muted. `TextLink` has no height, border or padding, so drop the classes that took them off.
- 0.6.1 (Added): `Button` `variant="on-frame"`: a frameless button on the yellow frame (rail, web top bar, desktop title bar); on the frame `md` and `icon-md` are the DS 36px frame item.
- 0.6.0 (Changed): `Button` transitions are colour-only (no `transition-all`); `xs` / `sm` render as `md`, `icon-xs` / `icon-sm` as `icon-md`.
