Skip to content

Button

Stable

Triggers an action. The most important interactive element in any interface — and the easiest one to overuse.

Anatomy

  1. 1ContainerCarries the variant's fill, border and focus ring. Heights align with inputs.
  2. 2Leading iconOptional. Sized and spaced by the button; padding tightens on this side.
  3. 3LabelA verb that names the outcome, in sentence case.
  4. 4Trailing iconOptional. Signals direction or a menu.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/button.json

Usage

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.

VariantUse it for
defaultThe single primary action of a view: Save, Create project, Continue.
brandA primary action that should carry the brand, typically on marketing surfaces.
secondarySupporting actions that sit next to a primary one.
outlineNeutral actions with a clear boundary: Cancel, Export, toolbar actions.
ghostLow-emphasis actions in dense UI: toolbars, table rows, card headers.
destructiveIrreversible actions: Delete, Revoke. Usually inside a confirmation.
linkNavigation that must look like text, inside sentences or footers.

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.

Why a prop and not composition?
Swapping the label for a spinner by hand usually shrinks the button and drops focus when it becomes disabled. loading preserves both. You can still compose a Spinner yourself when you need a different layout.

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 buttonVariants when 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.

Do.One primary action, supported by quieter ones.
Don’t.Several primary buttons compete — nothing reads as most important.

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.

Do.Specific verbs tell people exactly what will happen.
Don’t.Generic answers force people to reread the question.

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.

KeyBehavior
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 sm up meets the 24×24px minimum of WCAG 2.2 (2.5.8). Use xs and icon-xs only 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 focusableWhenDisabled with a tooltip.
  • Loading. loading sets aria-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.

PropTypeDefault
variant

Visual emphasis of the button.

"default" | "brand" | "secondary" | "outline" | "ghost" | "destructive" | "link""default"
size

Height and padding. Icon sizes render a square button.

"xs" | "sm" | "default" | "lg" | "xl" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""default"
loading

Shows a spinner, sets aria-busy and blocks activation while keeping width and focus.

booleanfalse
disabled

Disables the button and removes it from the tab order.

booleanfalse
focusableWhenDisabled

Keeps a disabled button focusable, e.g. to show a tooltip explaining why.

booleanfalse
nativeButton

Set to false when render produces a non-button element; Base UI then adds role="button" and keyboard activation. For navigation, use a link with buttonVariants instead.

booleantrue
render

Replaces the rendered element while keeping behavior and styles.

ReactElement | (props, state) => ReactElementNo default

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>