<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# Button

A button, or a link styled as one. The obvious name for any action: pressing it gives physical feedback, it has a 44px touch target on phones, and `loading` handles the busy state without the layout jumping.

```tsx
import { Button } from "@/components/ouro/button/button";
```

## Example

```tsx
<Button onClick={save} loading={saving}>Save</Button>
<Button href="/recipes/new" startIcon={PlusIcon}>New recipe</Button>
<Button tone="destructive" variant="soft" type="submit">Delete</Button>
```

## Usage

Use `href` to navigate (it renders a link); use `type="submit"` in a form.

## Use instead when

- Use `Text` with an `<a>` instead when a link sits inside running text.

## Props

The props the modes share. Type: `ButtonProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  | The label. For an icon-only button, also pass `aria-label`. |
| `className` | `string` |  | Layout only: margins, width, grid placement (`mt-4`, `col-span-2`). Your classes always win over Ouro UI's. Appearance classes warn in development; use `tone`, `variant` and `size` instead. |
| `endIcon` | `ButtonIcon` |  | Icon after the label. Pass the component, not an element: `endIcon={ArrowRightIcon}`. |
| `fullWidth` | `boolean` |  | Stretches the button to the width of its container. |
| `loading` | `boolean` |  | Shows a centered spinner, keeps the width, sets `aria-busy` and disables the button. It stays focusable, so focus is not lost when a pressed button turns busy. |
| `loadingLabel` | `string` | `"Loading"` | What screen readers hear, politely, when `loading` turns on. It is spoken through a shared live region, so the button's name stays its label. Use the action in progress: `loadingLabel="Saving recipe"`. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Control size. Each size is taller on phones and steps down to desktop density from the `sm` breakpoint (640px). shadcn's `default` is `md`; for an icon-only button, pass the icon as children and an `aria-label`. Any other string is a type error whose message names the fix. |
| `startIcon` | `ButtonIcon` |  | Icon before the label. Pass the component, not an element: `startIcon={PlusIcon}`. |
| `tone` | `"primary" \| "secondary" \| "destructive" \| "success" \| "warning"` | `"primary"` | Color role from the theme, named after its token: `primary` reads `--primary`. A solid primary button, the page's main action, reads `--ouro-action` and `--ouro-action-foreground` instead when the theme sets them, with `href` too, so a theme can give its actions their own colour. Only the solid look reads them: soft, outline and plain primary buttons keep `--primary`, as do `Link`, checkboxes, switches and the current tab. Optional `--ouro-action-hover` and `--ouro-action-active` set its state fills without changing the state layers. `--ouro-action-disabled`, `--ouro-action-disabled-foreground` and `--ouro-action-disabled-opacity` set its disabled look; opacity defaults to 0.5. shadcn's `variant="destructive"` is `tone="destructive"`, and its `variant="secondary"` is `tone="secondary"`. |
| `variant` | `"solid" \| "soft" \| "outline" \| "plain"` | `"solid"` | Visual weight: `solid` for the primary action, `soft` for secondary actions, `outline` for a quiet bordered control, `plain` for a borderless one. shadcn's `default` is `solid` (the default), `ghost` and `link` are `plain`, and `destructive` and `secondary` are tones: `tone="destructive"`. Any other string is a type error whose message names the fix. Variant is visual only. For a toggle, pass `aria-pressed`; for the current nav item, `aria-current="page"`. |

## Modes

`ButtonProps` is a union: pass the props of exactly one mode. Mixing them is a type error.

### Button: `ButtonButtonProps`

A Button without `href`: renders a native `<button>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `ref` | `Ref<HTMLButtonElement>` |  | Reaches the rendered element. |
| `type` | `"button" \| "submit" \| "reset"` | `"button"` (never submits a form by accident) | The button's role in a form: `submit` sends it, `reset` clears it. |

Also accepts the attributes in React's `ButtonHTMLAttributes` and `HTMLAttributes` (`id`, `aria-*`, event handlers), passed to the rendered element, except those named on this page.

Not in this mode (typed `never`):

- `href`: Not set: this is a `<button>`. Pass `href` to render a link instead.
- `reloadDocument`: Document navigation needs a link: pass `href` with `reloadDocument`.

### Link: `ButtonLinkProps`

A Button with `href`: renders a link that looks like a button.

| Prop | Type | Description |
| --- | --- | --- |
| `href` (required) | `string` | Where the link goes. Internal paths render through `OuroProvider`'s `linkComponent` (your router's link); `http(s)://`, `mailto:` and other absolute URLs render as a plain `<a>`. An absolute URL with `target="_blank"` always gets `rel="noopener noreferrer"`, merged with any `rel` you pass. |
| `ref` | `Ref<HTMLAnchorElement>` | Reaches the rendered element. |
| `reloadDocument` | `boolean` | Loads the document instead of using the provider's router link, for a server sign-in or a resource route. A present `download` attribute does the same. |

Also accepts the attributes in React's `AnchorHTMLAttributes` and `HTMLAttributes` (`id`, `aria-*`, event handlers), passed to the rendered element, except those named on this page.

Not in this mode (typed `never`):

- `disabled`: A link cannot be disabled. Remove `href` to render a disabled button, or don't render the link.
- `type`: Links have no `type`. Remove `href` to make a submit button: `<Button type="submit">`.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `asChild`: Not a prop. To render a link, pass `href`: `<Button href="/recipes">`. For other elements, use parts.
- `color`: Not a prop. Ouro UI uses `tone`: `<Button tone="destructive">`.
- `isLoading`: Not a prop. Ouro UI uses `loading`: `<Button loading>`.
- `leftIcon`: Not a prop. Ouro UI uses `startIcon`: `<Button startIcon={PlusIcon}>`.
- `rightIcon`: Not a prop. Ouro UI uses `endIcon`: `<Button endIcon={ArrowRightIcon}>`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`). Your classes always win over Ouro UI's: Button merges `className` last with `cn` (tailwind-merge), so a class that conflicts with its own replaces it.[^layout-classes]

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Types

### `ButtonIcon`

An icon component, such as one from `lucide-react` or `@heroicons/react`.

```ts
type ButtonIcon = ComponentType<SVGProps<SVGSVGElement>>;
```

[^layout-classes]: test:registry/ouro/components/ouro/button/button.test.tsx::className > lets app layout classes win over library styles verify:testExists(registry/ouro/components/ouro/button/button.test.tsx::className > lets app layout classes win over library styles)
