Button
StableTriggers an action. The most important interactive element in any interface — and the easiest one to overuse.
Anatomy
- 1ContainerCarries the variant's fill, border and focus ring. Heights align with inputs.
- 2Leading iconOptional. Sized and spaced by the button; padding tightens on this side.
- 3LabelA verb that names the outcome, in sentence case.
- 4Trailing iconOptional. Signals direction or a menu.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/button.jsonUsage
import { Button } from "@/components/ui/button"<Button variant="outline">Cancel</Button>
<Button>Save changes</Button>Examples
Variants
Seven variants express hierarchy, not decoration. prfct's primary button is monochrome by design: color is reserved for meaning — selection, focus, status — so the one colored action on a surface is always the most important one.
Sizes
Buttons share their heights with inputs and selects — xs 28, sm 32, default 36, lg 40, xl 48 — so controls always align on a row. Icon-only buttons are square at every size.
With icons
Mark icons with data-icon="inline-start" or data-icon="inline-end". The button tightens its padding on that side so the optical balance stays even, and sizes the icon for you — never add sizing classes to icons inside a button.
<Button>
<SendIcon data-icon="inline-start" />
Send invite
</Button>Icon only
An icon-only button has no visible text, so it must have an aria-label. Pair it with a tooltip that repeats the label for sighted mouse users.
Loading
loading replaces the label with a spinner without changing the button's width, sets aria-busy, and blocks clicks — but keeps keyboard focus on the button, so screen reader and keyboard users don't lose their place.
loading preserves both. You can still compose a Spinner yourself when you need a different layout.As a link
An action that navigates is a link, even when it looks like a button: screen readers announce it as a link, and people can open it in a new tab. Style the link with buttonVariants rather than rendering a Button as an anchor, which Base UI would give role="button".
import Link from "next/link"
import { buttonVariants } from "@/components/ui/button"
<Link href="/docs/installation" className={buttonVariants()}>
Get started
</Link>Guidelines
When to use
- To trigger an action on the current page: submit a form, open a dialog, save, delete.
- To start a flow: Create project, Invite teammate.
When not to use
- To navigate. Use a link — styled with
buttonVariantswhen it should look like a button. - To toggle a setting on and off — use a Switch.
- To choose one option from a small set — use a Toggle Group.
Hierarchy
Give each view one primary action. Everything else steps down to outline, secondary or ghost. Place the primary action last in a row (right-aligned in footers), and put dismissive actions like Cancel on the far side.
Labels
Lead with a verb and name the outcome: Save changes, Delete project, Send 3 invites. Use sentence case, keep it to one to three words, and never end with punctuation. A label should still make sense when read out of context by a screen reader.
Accessibility
prfct's button renders a native <button> through Base UI, so it is focusable, announces its role, and activates with Enter and Space without extra work.
| Key | Behavior |
|---|---|
Tab | Moves focus to the button. The focus ring is a 2px outline offset from the edge, visible in every theme and in forced-colors mode. |
EnterSpace | Activates the button. |
- Target size. Every size from
smup meets the 24×24px minimum of WCAG 2.2 (2.5.8). Usexsandicon-xsonly in dense, pointer-first UI. - Contrast. Label text on every filled variant meets 4.5:1 in both modes; see Color.
- Disabled. Disabled buttons are removed from the tab order. If people need to discover why an action is unavailable, keep the button enabled and explain on click, or use
focusableWhenDisabledwith a tooltip. - Loading.
loadingsetsaria-busy="true"and keeps focus; pair it with a polite live region or toast that announces the result.
API reference
Button
Renders a <button> element. Accepts every prop of the Base UI Button.
buttonVariants
The class generator is exported so other elements — above all, links — can look like buttons without being buttons. Icons marked with data-icon are sized and spaced exactly as in a Button.
import { buttonVariants } from "@/components/ui/button"
<Link href="/pricing" className={buttonVariants({ variant: "outline", size: "sm" })}>
Pricing
</Link>