Button
The action. Six variants and four sizes, with icons, a loading state and a held state, on the same squircle corners as Input. Built on the vendored Base UI button; ButtonLink gives a link the same look.
Playground
import { Button } from "@/components/shared/button";
<Button>
Get started
</Button>Variants
Five variants, inline or full width — add className="w-full" to stretch one to its container.
import { Button } from "@/components/shared/button";
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Cancel booking</Button>
<Button variant="danger">Delete account</Button>
<Button className="w-full">Primary</Button>Sizes
xs 24px inside dense rows and tables, sm 32px, md 36px by default, lg 40px for the page's main action — the nav's CTA. Each has a square icon-only twin.
The touch floor: on a touch screen nothing is under 40px. sm and md grow to 40px tall (and their icon-only twins to 40px square); xs keeps its look and reaches 40px through an invisible hit area, so leave it room. A mouse sees the sizes above.
import { Button } from "@/components/shared/button";
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button>Medium</Button>
<Button size="lg">Large</Button>
<Button size="xs" iconOnly leadingIcon={Plus} aria-label="Add" />With icons
import { Button } from "@/components/shared/button";
import { ArrowRight02Icon, PlusIcon, SearchIcon } from "@hugeicons/core-free-icons";
<Button leadingIcon={PlusIcon}>New booking</Button>
<Button variant="primary" trailingIcon={ArrowRight02Icon}>Continue</Button>
<Button variant="secondary" iconOnly leadingIcon={SearchIcon} aria-label="Search" />Loading & disabled
Loading shows a spinner in the leading place, keeps the button focusable and ignores presses; disabled dims it. Press the first one.
import { Button } from "@/components/shared/button";
<Button loading={saving} onClick={save}>
{saving ? "Saving" : "Save changes"}
</Button>
<Button variant="primary" loading>Saving</Button>
<Button variant="secondary" disabled>Disabled</Button>As a link
A link stays a link: ButtonLink renders an <a> with the same variants, sizes and icons — never Button with a link inside.
import { ButtonLink } from "@/components/shared/button";
<ButtonLink href="#early-access" size="lg">Get early access</ButtonLink>
<ButtonLink href="/pricing" variant="ghost" trailingIcon={ArrowRight}>See pricing</ButtonLink>On dark ground
Over footage or a dark photo — the landing hero — the action is inverse: white with ink words, the primary's mirror. The glass secondary turns grey there, and ink doesn't carry.
import { Button } from "@/components/shared/button";
<Button variant="inverse" size="lg">Get early access</Button>API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "primary" | "secondary" | "ghost" | "destructive" | "danger" | "inverse" | "primary" | Primary is the one action, black with white words; secondary white glass with a hairline; ghost no fill until hover; destructive the one red, for actions that can't be taken back; danger red words on a red wash, for the way into one (Delete account opens its confirm); inverse white with ink words, the one action on dark ground (the landing hero's footage). |
| size | "xs" | "sm" | "md" | "lg" | "md" | 24, 32, 36 or 40px tall; on a touch screen sm and md are 40px, and xs reaches 40px through its hit area. |
| leadingIcon | Icon | — | An SVG icon component drawn before the label. |
| trailingIcon | Icon | — | An SVG icon component drawn after the label. |
| iconOnly | boolean | false | Square, showing only leadingIcon. Requires aria-label. |
| loading | boolean | false | Spinner in the leading place; stays focusable, ignores presses, sets aria-busy. Button only. |
| active | boolean | false | Holds the pressed look — e.g. while the menu it opened is showing. Button only. |
| disabled | boolean | false | Disables and dims the button. Button only. |
| className | string | — | Layout only — w-full, margins. Never restyle a variant here. |
Everything else passes through to the Base UI button (type, onClick, …) or, for ButtonLink, the <a> (href, target, …). For a custom element use buttonStyles().
Guidelines
- One primary per view: the black button is the single thing we most want pressed. Everything else steps down to secondary or ghost. Okami's actions are neutral; colour is the studio's, on its mark and ball.
- Destructive only confirms something that can't be taken back — cancelling a booking, removing a person — and sits beside a ghost way out, never alone.
- Labels are a verb and its object, in sentence case: “Book a slot”, “Save changes”. While loading, say what's happening (“Saving”) rather than blanking the label.
- An icon's side is a step tighter than the label's (md 12px to 16px): a glyph sits inside its box, so equal padding reads wider beside it. Each icon marks its side (
data-icon) and the size does the rest; don't pad around it by hand. - Icon-only buttons always carry an
aria-label; reach for them only where the icon is unmistakable (search, close, add). - Navigation is a link: use
ButtonLink. Base UI's Button enforces button semantics and must not wrap an<a>. - On touch-first surfaces use
mdorlg; keepxsfor pointer-dense places like table rows, where a slip costs nothing.