Skip to content
Component installs need the registry setup
VegaStack Design

Button

Displays a button, or a component that looks like one — six variants, eight sizes, and a loading state.

Status
stable
Since
0.1.0
Accessibility pattern
native button

Last updated

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Button before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify button

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/button

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/spinner.

Usage

import { Button } from "@/components/ui/button";

<Button variant="outline">Button</Button>;

Examples

Cursor

Tailwind v4 switched from cursor: pointer to cursor: default for buttons. VegaStack keeps the hand cursor for every clickable control, globally, from base.css (INT-1) — shadcn's own --pointer rule over a wider selector list — so no component sets a cursor of its own and a disabled button keeps the same cursor it had.

Size

Use the size prop to change the size of the button.

Default

Outline

Secondary

Ghost

Destructive

Icon

With Icon

Add the data-icon="inline-start" or data-icon="inline-end" attribute to the icon for the correct spacing.

Rounded

Use the rounded-full class to make the button rounded.

Spinner

Render a Spinner inside the button to show a loading state, with the same data-icon attribute for spacing. For the common case — a pending submit that must keep its width and its accessible name — pass the loading prop instead; see Loading below.

Button Group

To join buttons into one control, use ButtonGroup. See the Button Group page for the full API.

Use the buttonVariants helper to make a link look like a button. Do not render an anchor through Button: Base UI's Button always applies role="button", which overrides the link role.

RTL

Every size and variant is written in logical properties, so a right-to-left document mirrors padding, icon spacing and group seams without any extra work.

Loading

loading is ours (API-5). It shows a spinner over the label, sets aria-busy, and blocks activation while keeping the label's box at opacity: 0 — so the button's width does not move and its accessible name survives (A11Y-12).

API Reference

PropTypeDefaultDescription
loadingboolean—Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its box at opacity: 0, so the button's width does not move and its accessible name survives (API-5, A11Y-12).
size"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null—
variant"default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null—

Accessibility

  • Renders a native <button> through Base UI's Button; Enter and Space activate it.
  • On :focus-visible the global background tint in base.css applies (FOC-1). The component carries no focus ring, no ring-3 glow and no outline-none of its own (FOC-6).
  • disabled renders aria-disabled rather than the native attribute, so the control keeps its pointer events and a Tooltip can explain why it is unavailable (FRM-4). Base UI still suppresses activation.
  • loading sets aria-busy="true" and keeps the label in the accessibility tree (A11Y-12).
  • An icon-only button (size="icon" and the three other icon tiers) has no visible text — give it an aria-label.
ContractStates tested
Behaviourdefault, active, disabled, error, invalid, loading, success
Accessibilitybusy, disabled, focus-visible, invalid, labeled
Visualdefault, hover, focus, active, disabled, invalid, loading, error, success, dark

Do / Don't

Do
Use buttonVariants on a plain anchor when the action navigates to a URL.
Don't
Render an anchor through Button — role=button overrides the link role.

Row and inline actions

The filled default (primary) button is for the page's one main action — the button at the end of the app's top bar — and a dialog's confirm. An action that sits on a row, a card or an inline block (an Inbox item's "Catch me up" or "Download", an approval's Approve, a list row's quick action) is never primary: its main action takes secondary, and every further action outline. A screen with twenty rows then has one filled button, not twenty.

Do
Give a row's main action variant='secondary' and the rest variant='outline'.
Don't
Fill a row, card or inline action with the default (primary) variant.

Deviations

Upstream's file plus packages/ui/upstream/patches/button.patch. Every hunk:

  • API-29 — the root writes data-variant, so a tinted container (a status Alert) can give its non-primary buttons a variant-aware hover.
  • FOC-1, FOC-6 — the focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 glow and the outline-none that suppressed the global focus treatment are removed.
  • FOC-14 — the invalid tint stays upstream's aria-invalid:border-destructive, held in every state, focused or not (FOC-14 retired FOC-5's not-focus: guard); the aria-invalid glow rings go with FOC-6.
  • FRM-4 — disabled:pointer-events-none is dropped and focusableWhenDisabled defaults to true, so a disabled button stays hoverable and focusable.
  • API-5, A11Y-12 — the loading prop, the out-of-flow spinner and the opacity-0 label.
  • A11Y-13 — the destructive variant's tint takes text-destructive-text; upstream's text-destructive on its own /10 measures 3.98:1 in light, under the AA floor A11Y-1 enforces.
  • A11Y-24 — on a coarse pointer every size grows an invisible ::after hit area to 44×44 (a per-size inset from the padding box: 11px on xs/icon-xs, 9px on sm/icon-sm, 7px on default/icon, 5px on lg/icon-lg); nothing moves visually. Beside a sibling control (a button, link or field) that side stays flush, so neighbouring areas never overlap. MK, 2026-10-14.
  • DOC-2 — cn is imported from @vegastack/design.

On this page