Button
Displays a button, or a component that looks like one — six variants, eight sizes, and a loading state.
- Status
- 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 buttonInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/buttonRun 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
Link
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.
As Link
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
| Prop | Type | Default | Description |
|---|---|---|---|
loading | boolean | — | 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-visiblethe global background tint inbase.cssapplies (FOC-1). The component carries no focus ring, noring-3glow and nooutline-noneof its own (FOC-6). disabledrendersaria-disabledrather 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.loadingsetsaria-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 anaria-label.
| Contract | States tested |
|---|---|
| Behaviour | default, active, disabled, error, invalid, loading, success |
| Accessibility | busy, disabled, focus-visible, invalid, labeled |
| Visual | default, hover, focus, active, disabled, invalid, loading, error, success, dark |
Do / Don't
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.
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/50glow and theoutline-nonethat 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'snot-focus:guard); thearia-invalidglow rings go with FOC-6. - FRM-4 —
disabled:pointer-events-noneis dropped andfocusableWhenDisableddefaults to true, so a disabled button stays hoverable and focusable. - API-5, A11Y-12 — the
loadingprop, the out-of-flow spinner and theopacity-0label. - A11Y-13 — the
destructivevariant's tint takestext-destructive-text; upstream'stext-destructiveon its own/10measures 3.98:1 in light, under the AA floor A11Y-1 enforces. - A11Y-24 — on a coarse pointer every size grows an invisible
::afterhit area to 44×44 (a per-size inset from the padding box: 11px onxs/icon-xs, 9px onsm/icon-sm, 7px ondefault/icon, 5px onlg/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 —
cnis imported from@vegastack/design.