

## Overview [#overview]

The **Button** component provides a versatile and customizable button system with multiple variants, appearances, sizes, and shapes. It supports polymorphic rendering with the `render` prop (Base UI pattern), as well as special features like pending states and URL state management.

Use `Button.Wrapper` to visually attach adjacent buttons for split-button layouts while keeping each inner action as a real `Button`.

## Usage [#usage]

```tsx
import { Button } from "@tilt-legal/cubitt-components/button";
```

```tsx
<Button>Click me</Button>
```

<Accordions type="single">
  <Accordion title="URL State Management">
    Using Cubitt's URL-state hooks, you can sync button clicks with the URL by providing `paramName` and `paramSetValue`:

    ```tsx
    // Clicking sets ?action=save in the URL
    <Button paramName="action" paramSetValue="save">
      Save Draft
    </Button>

    // Advanced options:
    <Button
      paramName="action"
      paramSetValue="publish"
      paramDebounce={300}           // Debounce URL updates
    >
      Publish
    </Button>
    ```
  </Accordion>

  <Accordion title="Polymorphic Rendering">
    Use the `render` prop to render the button as a different element while preserving button styling:

    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { Link } from "@tanstack/react-router";

    <Button render={<Link href="/home" />}>Go Home</Button>;
    ```
  </Accordion>
</Accordions>

## Examples [#examples]

### Secondary [#secondary]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SecondaryButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return <Button variant="secondary">Secondary</Button>;
    }
    ```
  </Tab>
</Tabs>

### Frost [#frost]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="FrostButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return <Button variant="frost">Frost</Button>;
    }
    ```
  </Tab>
</Tabs>

### Destructive [#destructive]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DestructiveButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return <Button variant="destructive">Destructive</Button>;
    }
    ```
  </Tab>
</Tabs>

### Outline [#outline]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="OutlineButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return <Button variant="outline">Outline</Button>;
    }
    ```
  </Tab>
</Tabs>

### Ghost [#ghost]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="GhostButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return <Button variant="ghost">Ghost</Button>;
    }
    ```
  </Tab>
</Tabs>

### With Icon [#with-icon]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="WithIconButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { Archive, UserPen } from "@tilt-legal/cubitt-icons/ui/outline";

    export default function Component() {
      return (
        <Button variant="secondary">
          <UserPen />
          With Icon
        </Button>
      );
    }
    ```
  </Tab>
</Tabs>

### Icon Only [#icon-only]

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="IconOnlyButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { UserPen } from "@tilt-legal/cubitt-icons/ui/outline";

    export default function Component() {
      return (
        <Button aria-label="Edit profile" variant="secondary" mode="icon">
          <UserPen />
        </Button>
      );
    }
    ```
  </Tab>
</Tabs>

### Tooltip [#tooltip]

Use `tooltip` to attach compact context to icon-only or disabled actions without wrapping the button manually.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="TooltipButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { UserPen } from "@tilt-legal/cubitt-icons/ui/outline";

    export default function Component() {
      return (
        <div className="flex items-center gap-4">
          <Button
            aria-label="Edit profile"
            mode="icon"
            tooltip="Edit profile"
            variant="secondary"
          >
            <UserPen />
          </Button>

          <Button
            aria-label="Archive"
            disabled
            mode="icon"
            tooltip={{
              content: "Archiving is unavailable",
              side: "bottom",
              sideOffset: 6,
            }}
            variant="secondary"
          >
            <Archive />
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Icon Label Transition [#icon-label-transition]

Keep a plain text label mounted and toggle `mode="icon"` when a button needs to collapse to its icon-only form. Cubitt handles the visual label transition while preserving the button's accessible name.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="IconLabelTransitionButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Button } from "@tilt-legal/cubitt-components/button";
    import { Bell, UserPen } from "@tilt-legal/cubitt-icons/ui/outline";
    import { useState } from "react";

    export default function Component() {
      const [secondaryCollapsed, setSecondaryCollapsed] = useState(true);
      const [variantCollapsed, setVariantCollapsed] = useState(true);

      return (
        <div className="flex flex-col items-center gap-3">
          <Button
            aria-pressed={!secondaryCollapsed}
            mode={secondaryCollapsed ? "icon" : undefined}
            onClick={() => setSecondaryCollapsed((current) => !current)}
            variant="secondary"
          >
            <UserPen />
            Edit profile
          </Button>

          <Button
            aria-pressed={!variantCollapsed}
            mode={variantCollapsed ? "icon" : undefined}
            onClick={() => setVariantCollapsed((current) => !current)}
            variant={variantCollapsed ? "ghost" : "outline"}
          >
            <Bell />
            Notifications
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Pending [#pending]

Shows a loading spinner while an action is in progress.

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="PendingButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Button } from "@tilt-legal/cubitt-components/button";
    import { useEffect, useState } from "react";

    export default function Component() {
      const [isPending, setIsPending] = useState(false);

      useEffect(() => {
        if (!isPending) {
          return;
        }

        const timeout = window.setTimeout(() => {
          setIsPending(false);
        }, 2000);

        return () => window.clearTimeout(timeout);
      }, [isPending]);

      return (
        <Button
          variant="secondary"
          onClick={() => setIsPending(true)}
          pending={isPending}
        >
          Submit
        </Button>
      );
    }
    ```
  </Tab>
</Tabs>

### With Badge [#with-badge]

Combine buttons with badges for notifications and counters.

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="BadgeButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { Bell, Inbox, CircleCheck } from "@tilt-legal/cubitt-icons/ui/outline";

    export default function Component() {
      return (
        <div className="flex flex-col items-center gap-10">
          <Button variant="outline" mode="icon" className="relative">
            <Bell />
            <span className="border-2 border-background rounded-full size-3 bg-primary absolute -top-1 -inset-e-1 animate-bounce" />
          </Button>

          <Button variant="outline" mode="icon" className="relative">
            <Inbox />
            <Badge
              variant="primary"
              shape="circle"
              size="sm"
              className="absolute top-0 inset-s-full -translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2"
            >
              5
            </Badge>
          </Button>

          <Button variant="outline" className="relative">
            <Inbox />
            Messages
            <Badge
              variant="destructive"
              shape="circle"
              size="sm"
              className="absolute top-0 inset-s-full -translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2"
            >
              5
            </Badge>
          </Button>

          <Button variant="outline">
            <CircleCheck />
            Notifications
            <Badge variant="brand" size="sm">
              10+
            </Badge>
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Sizes [#sizes]

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="SizeButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { UserPen } from "@tilt-legal/cubitt-icons/ui/outline";

    export default function Component() {
      return (
        <div className="flex flex-col items-center justify-center gap-6">
          <div className="flex items-center gap-4">
            <Button variant="outline" size="sm">
              <UserPen />
              Small
            </Button>
            <Button variant="outline">
              <UserPen />
              Medium
            </Button>
            <Button variant="outline" size="lg">
              <UserPen />
              Large
            </Button>
          </div>
          <div className="flex items-center gap-4">
            <Button variant="outline" size="sm" mode="icon">
              <UserPen />
            </Button>
            <Button variant="outline" mode="icon">
              <UserPen />
            </Button>
            <Button variant="outline" size="lg" mode="icon">
              <UserPen />
            </Button>
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Link [#link]

Style buttons as links using `variant="link"`.

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="LinkButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { Link } from "@tanstack/react-router";

    export default function Component() {
      return (
        <div className="flex flex-col items-center gap-6">
          <Button variant="link" size="sm" render={<Link href="#" />}>
            Link Button
          </Button>
          <Button variant="link" render={<Link href="#" />}>
            Link Button
          </Button>
          <Button variant="link" size="lg" render={<Link href="#" />}>
            Link Button
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Split Button [#split-button]

Use `Button.Wrapper` when you want a primary action and an adjacent secondary trigger to read as a single control.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SplitButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { Button } from "@tilt-legal/cubitt-components/button";
    import {
      Menu,
      MenuContent,
      MenuItem,
      MenuSeparator,
      MenuTrigger,
    } from "@tilt-legal/cubitt-components/menu";
    import { useEffect, useState } from "react";

    export default function Component() {
      const PENDING_RESET_DELAY_MS = 2000;
      const variants = [
        { label: "Default", moreLabel: "More primary actions", variant: "default" },
        {
          label: "Secondary",
          moreLabel: "More secondary actions",
          variant: "secondary",
        },
        {
          label: "Destructive",
          moreLabel: "More destructive actions",
          variant: "destructive",
        },
        { label: "Frost", moreLabel: "More frost actions", variant: "frost" },
        { label: "Outline", moreLabel: "More outline actions", variant: "outline" },
      ] as const;

      function SplitButtonItem({
        label,
        moreLabel,
        variant,
      }: (typeof variants)[number]) {
        const [isPending, setIsPending] = useState(false);
        const actionTooltip =
          variant === "outline" ? "Run outline action" : undefined;
        const menuTooltip =
          variant === "outline"
            ? {
                content: "More outline actions",
                side: "bottom" as const,
                sideOffset: 6,
              }
            : undefined;

        useEffect(() => {
          if (!isPending) {
            return;
          }

          const timeout = window.setTimeout(() => {
            setIsPending(false);
          }, PENDING_RESET_DELAY_MS);

          return () => window.clearTimeout(timeout);
        }, [isPending]);

        return (
          <Menu>
            <Button.Wrapper>
              <Button
                onClick={() => setIsPending(true)}
                pending={isPending}
                tooltip={actionTooltip}
                variant={variant}
              >
                {label}
              </Button>
              <MenuTrigger
                render={
                  <Button
                    aria-label={moreLabel}
                    mode="icon"
                    tooltip={menuTooltip}
                    variant={variant}
                  />
                }
              >
                <Button.Arrow className="me-0 ms-0" />
              </MenuTrigger>
            </Button.Wrapper>
            <MenuContent align="end">
              <MenuItem>Duplicate</MenuItem>
              <MenuItem>Move</MenuItem>
              <MenuSeparator />
              <MenuItem variant="destructive">Archive</MenuItem>
            </MenuContent>
          </Menu>
        );
      }

      return (
        <div className="flex flex-wrap items-center gap-4">
          {variants.map((item) => (
            <SplitButtonItem key={item.variant} {...item} />
          ))}
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Disabled [#disabled]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DisabledButtonExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return (
        <div className="flex items-center gap-4">
          <Button disabled>Primary</Button>
          <Button variant="secondary" disabled>
            Secondary
          </Button>
          <Button variant="outline" disabled>
            Outline
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### URL State [#url-state]

Sync button clicks with URL parameters. Click a button to see the URL update with the corresponding action.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="ButtonURLStateExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";

    export default function Component() {
      return (
        <div className="flex items-center gap-4">
          <Button paramName="demo-action" paramSetValue="save" variant="secondary">
            Save Draft
          </Button>
          <Button paramName="demo-action" paramSetValue="publish">
            Publish
          </Button>
          <Button
            paramName="demo-action"
            paramSetValue="archive"
            variant="destructive"
          >
            Archive
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

## API Reference [#api-reference]

### Button [#button]

The root component for creating buttons with multiple variants, appearances, sizes, and shapes. Supports polymorphic rendering via the `render` prop and URL state management.

| Prop           | Type                                                                                                            | Default     | Description                                                                                                            |
| -------------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
| `variant`      | `"default" \| "secondary" \| "mono" \| "inverse" \| "destructive" \| "frost" \| "outline" \| "ghost" \| "link"` | `"default"` | Visual style variant.                                                                                                  |
| `size`         | `"sm" \| "md" \| "lg"`                                                                                          | `"md"`      | Button size.                                                                                                           |
| `mode`         | `"icon" \| "input"`                                                                                             | —           | Layout mode. Use `icon` for square icon buttons and `input` for form trigger styling.                                  |
| `unstyled`     | `boolean`                                                                                                       | `false`     | Render as a completely unstyled button.                                                                                |
| `pending`      | `boolean`                                                                                                       | `false`     | Show a loading spinner. Leading icons swap immediately; other layouts keep the animated inline indicator.              |
| `tooltip`      | `ButtonTooltip`                                                                                                 | —           | Optional tooltip content or configuration. Disabled tooltip buttons use `aria-disabled` so the tooltip can still open. |
| `selected`     | `boolean`                                                                                                       | `false`     | Mark the button as selected for toggle or popup-trigger states.                                                        |
| `disabled`     | `boolean`                                                                                                       | `false`     | Disable the button.                                                                                                    |
| `reduceMotion` | `boolean`                                                                                                       | `false`     | Disable Button-owned animations.                                                                                       |
| `render`       | `ReactElement \| function`                                                                                      | —           | Render as another element while preserving Button behavior.                                                            |
| `className`    | `string`                                                                                                        | —           | Additional classes for the button.                                                                                     |

#### ButtonTooltip [#buttontooltip]

The `tooltip` prop accepts a `ReactNode` or an object with these fields.

| Property      | Type                                                                       | Default    | Description                                 |
| ------------- | -------------------------------------------------------------------------- | ---------- | ------------------------------------------- |
| `content`     | `ReactNode`                                                                | —          | Tooltip content. Required for object form.  |
| `delay`       | `number`                                                                   | `500`      | Hover/focus delay in milliseconds.          |
| `side`        | `"top" \| "right" \| "bottom" \| "left" \| "inline-start" \| "inline-end"` | `"top"`    | Preferred tooltip side.                     |
| `sideOffset`  | `number`                                                                   | `8`        | Distance from the button in pixels.         |
| `align`       | `"start" \| "center" \| "end"`                                             | `"center"` | Alignment along the selected side.          |
| `alignOffset` | `number`                                                                   | `0`        | Offset along the alignment axis.            |
| `className`   | `string`                                                                   | —          | Additional classes for the tooltip content. |

#### URL State Props [#url-state-props]

When provided with a `paramName`, the button will set URL parameters when clicked.

| Prop            | Type     | Default | Description                                         |
| --------------- | -------- | ------- | --------------------------------------------------- |
| `paramName`     | `string` | —       | URL parameter name. Enables URL state management.   |
| `paramSetValue` | `string` | —       | Value to set in the URL when the button is clicked. |
| `paramThrottle` | `number` | —       | Throttle URL updates in milliseconds.               |
| `paramDebounce` | `number` | —       | Debounce URL updates in milliseconds.               |
