# Button

> A button that runs an action, with tones, emphasis levels, sizes, and a loading state.

- Name: `button`
- Status: beta
- Categories: actions
- File: the install writes `button.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`. The install adds them.
- Other registry items: `@txbrendan/motion`, `@txbrendan/button-variants`, `@txbrendan/dither`, `@txbrendan/motion-timing`. The install adds them.
- Gallery: https://txbrendan.dev/components/button
- Registry item: https://txbrendan.dev/r/button.json

## Install

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

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

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

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui 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 @txbrendan/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @txbrendan/button
   ```

## Use it for

- Running an action on the current page: save, submit, open a dialog, delete.
- Use tone to say what kind of action it is and emphasis to say how important it is.
- Use one solid brand button per view for the main action. Use soft, outline, or ghost for the rest.
- Use loading while an action is running. It keeps the width and blocks repeat clicks.
- Change the words to show a new state in place, like Save to Saved. The button eases to the new width while the words crossfade. Compose changing words and icons with MotionSwap for one coordinated transition.
- Words too long for the space end in an ellipsis, beside an icon too. In a flex row, give the box that holds the button min-w-0. In a row that does not wrap, also add className="min-w-0 shrink" to the button. Words inside your own element, like a span or a fragment, are yours to cut. A link styled with buttonVariants() does not cut its words.
- When a press cannot go ahead, answer on the button itself: call shake() on it, and change its words to what is missing, like "Agree to First aid consent first". The button stays where it was pressed.

## Avoid it for

- Going to another page. Use a link styled with buttonVariants() so it stays a link. In a server file, import it from button-variants, since Button runs in the browser.
- Turning a setting on and off. Use a switch or a toggle instead.
- Disabling a submit button to hide form errors. Keep it enabled and show the errors.
- Showing why a press failed in a new line above the button. It pushes the button out from under the pointer. Say it on the button, or beside or below it.
- Using tone='danger' for anything that is not destructive.

## Props

- `tone`: `"neutral" | "brand" | "danger" | "success" | "warning"`. Default `"neutral"`. Color role.
- `emphasis`: `"solid" | "soft" | "outline" | "ghost"`. Default `"solid"`. How loud the button is.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. Height and padding. Density scales them.
- `loading`: `boolean`. Default `false`. Shows a spinner, keeps the width, blocks clicks, and stays focusable.
- `disabled`: `boolean`. Default `false`. Blocks interaction and removes the button from the tab order.
- `iconOnly`: `boolean`. Default `false`. Square button for one icon. TypeScript requires aria-label when true.
- `render`: `ReactElement`. Render prop. Renders this element instead of the \<button>, with the same props. Pass nativeButton={false} if it is not a \<button>.
- `texture`: `"flat" | "dither" | "gradient" | "grain"`. Adds dots, a soft shine, or grain to the lower half, and a glow on dark pages, on solid and outline. Hover clears the shine and grain. Soft and ghost stay flat. buttonVariants on a link stays flat. Left out, the button follows the Texture around it.
- `className`: `string`. Extra classes. Later classes win, so className='rounded-none' replaces the radius.
- `shake`: `(element: HTMLElement | null) => void`. Shakes a button once, side to side, to say a press could not go ahead. It ends where it started, and does nothing when motion is off or reduced.

## Accessibility

- Renders a native \<button type="button">, so Enter and Space work and screen readers announce a button.
- loading sets aria-disabled and aria-busy and keeps focus on the button, so keyboard users do not lose their place.
- iconOnly requires aria-label at the type level. The label names the action, like "Delete row", not the icon.
- The focus ring uses --ring, which the theme tests hold to 3:1 against the page and cards.
- Press scale follows the --motion-\* tokens, and reduced motion turns it off. The spinner turns at a fixed speed.
- Words that change are read as the button's new name. The old words are hidden from screen readers while they fade. A shake says nothing, so pair it with a message that is read out, like role="alert".
- The label transition and shake follow the --motion-\* tokens. Reduced motion lands the new width at once, keeps only the words' fade, and skips the shake.

## Examples

### Main action

```tsx
<Button tone="brand">Save changes</Button>
```

### Destructive action with a quieter cancel

```tsx
<div className="flex gap-2">
  <Button emphasis="ghost">Cancel</Button>
  <Button tone="danger">Delete project</Button>
</div>
```

### Loading while a form submits

```tsx
"use client";
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <Button type="submit" tone="brand" loading={pending}>
      Create account
    </Button>
  );
}
```

### Icon-only

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

<Button iconOnly aria-label="Delete row" emphasis="ghost" tone="danger">
  <Trash2 />
</Button>
```

### A press that cannot go ahead says why on the button

```tsx
"use client";
import { useRef, useState } from "react";
import { Button, shake } from "@/components/ui/button";

function ContinueButton({ ready, onContinue }: { ready: boolean; onContinue: () => void }) {
  const [asking, setAsking] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
  return (
    <>
      <Button
        tone={asking ? "danger" : "brand"}
        emphasis={asking ? "soft" : "solid"}
        onClick={(event) => {
          if (ready) return onContinue();
          shake(event.currentTarget);
          setAsking(true);
          clearTimeout(timer.current);
          timer.current = setTimeout(() => setAsking(false), 3200);
        }}
      >
        {asking ? "Fill in your email first" : "Continue"}
      </Button>
      {asking && (
        <span role="alert" className="sr-only">
          Fill in your email first.
        </span>
      )}
    </>
  );
}
```

### A link that looks like a button

```tsx
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button-variants";

<Link href="/pricing" className={buttonVariants({ tone: "brand", emphasis: "soft" })}>
  See pricing
</Link>
```

### Grain on the lower half, cleared on hover

```tsx
<Button tone="brand" texture="grain">Save changes</Button>
```
