Skip to main content
Graphite UI

Button

Starts an action: saving, sending, opening a dialog. When the result is going somewhere else, use a link, even if it looks like a button.

Contract 2.5.0Kit · 1 sets · 258 variantsOpen in Figma

Live preview

Rendered by the component itself from the same generated tokens as the rest of the site.

Installation

Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.

npx shadcn@latest add https://www.graphite-ui.com/r/button.json
import { Button, buttonVariants } from '@/components/ui/button'

Anatomy

One slot. The label and any icons are children, so a caller composes them instead of picking from a fixed leading and trailing pair. The wide right inset is the kit’s: it reserves room for a trailing icon.

  1. ChildrenRequiredThe label, and any icons, as children. Icons are children rather than named slots, so a caller composes them instead of choosing from a fixed pair.

Variants

Two props carry the kit’s axes. Variant says how much the action matters; size sets the height, and the icon sizes make the button square, at each size the kit draws one. On the filled styles a trailing icon sits in the slot the wide right inset reserves, 16px from the edge; on the ghost styles it follows the label.

Variant: Primary
Variant: Secondary (default)
Variant: Ghost
Variant: Danger
Variant: Danger ghost
Size: Small
Size: Medium (default)
Size: Large
Size: Extra large
Size: 2x large
Size: Expressive
Icon only: Small · Medium · Large · Extra large · Expressive

States

These are variant axes in the kit and pseudo-classes in the code. A State=Hover variant is a picture of a behaviour, not an instruction to add a hover prop, so the page shows them as a matrix rather than as props.

Enabled
Hover
Active
Focus
Disabled

API reference

Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.

Button props
PropTypeDefaultNotes
variant'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-ghost'—Defaults to secondary. A primary default would make breaking the one-primary rule the path of least resistance.
size'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'expressive' | 'icon-sm' | 'icon' | 'icon-lg' | 'icon-xl' | 'icon-expressive'—The kit's Size axis, plus Icon only at each size it draws one. Text sizes are 32/42/50/66/82 and Expressive 50 (16/24 type); icon-only squares are 32/40/48/64 and Expressive 44, on the grid where the text sizes sit 2px over it. Extra large and 2x large top-align the label and icon, 16px down.
asChildboolean—Render the button's props onto its single child instead of emitting a button element.
className——Merged after the variant recipe, so a caller can extend without forking.
type'button' | 'submit' | 'reset'—Defaults to button, and only when this component owns the element.

Design tokens

Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.

  • primaryFill on the primary variant, and the label colour on ghost — the kit binds Style=Ghost's text to `primary`, not to a neutral. Its hover step is ghost's label on hover and press; its focus step is the ring on primary and ghost; its disabled step is the fill on every filled style and the label on the ghost styles, which stay unfilled.
  • on-primaryLabel on primary, secondary and danger, and on danger-ghost once hover or press fills it. One label colour across the filled styles, as the kit has it.
  • secondaryFill on the secondary variant, which the kit renders filled rather than outlined. Its focus step is secondary's ring.
  • surface-variantHover and pressed background on ghost.
  • dangerFill on the destructive variant, and the tone steps it takes on hover and press. The label on danger-ghost, which takes those same steps as its fill when touched. Its focus step is the ring on both danger styles.
  • backgroundThe 1px line inside the focus ring on the filled styles, so the ring reads against its own fill.
  • spacingPadding, gap to icons, and the minimum touch target.
  • motionTransition duration and easing, shared with the pages so a button moves the way its surroundings do.
  • radiusCorner. The kit is square-cornered, so this resolves to `radius-none`.

Usage

The contract's prohibitions, written as the choices you will actually face.

Do

  • Give each group one primary action, and make it the one the screen exists for.
  • Put a destructive action on danger, even when it is the main action in a confirmation dialog.
  • Use asChild to render onto a link when the button navigates, so it keeps link semantics.
  • Give an icon-only button an aria-label that names the action, not the glyph.

Don’t

  • Place two primary buttons side by side. Decide which one the group is for.
  • Style a delete as primary because it is the expected next step. Danger is what tells a reader it cannot be undone.
  • Shrink a button below its touch target with a className. The minimum holds at every size on purpose.
  • Invent a hover colour. Hover and press are tone steps on the fill’s own ramp.

Accessibility

What the component does for you, and what it leaves to you.

Keyboard
A native button element, so Enter and Space both activate it. The type defaults to button, so it will not submit a form unless you ask it to.
Focus
The kit’s ring, on :focus-visible: 2px inside the edge in the style’s own focus colour (--graphite-primary-focus, -secondary-focus or -danger-focus), with a 1px line of the page background inside it on the filled styles. A mouse click does not show it; a keyboard does. In forced-colours mode a system ring replaces it.
Labels
The icon size changes the shape, never the naming requirement. The component does not check for an aria-label, so the caller must pass one.
Disabled
Uses the native disabled attribute, which removes the button from the tab order. If a reader needs to learn why an action is unavailable, say so in text nearby.
Contrast
Every filled style puts on-primary on its fill, and those pairs are measured at the theme’s target, AA or AAA.
Motion
Colour transitions and the 1px press displacement both switch off under prefers-reduced-motion.

Figma parity

The kit's Button page ships 258 variants in 1 set. The code exposes 5 props. This table is where those two facts are reconciled instead of quietly diverging.

Kit axes and how they map to code
Kit axisValuesCodeHow it maps
StylePrimary · Secondary · Ghost · Danger primary · Danger ghostvariantAll five. Danger primary is danger; Danger ghost is danger-ghost. The kit binds Secondary’s focus ring to success’s, which the code reads as a slip and draws in secondary’s.
SizeSmall · Medium · Large · Extra large · 2x large · ExpressivesizeAll six: sm, md, lg, xl, 2xl and expressive at the kit’s 32, 42, 50, 66, 82 and 50px. Extra large and 2x large top-align the label and icon; Expressive sets its label at 16/24. The kit draws 2x large without the danger styles; the code allows them.
TypeText + Icon · Icon onlysize="icon-*"Icon only is a size: icon-sm, icon, icon-lg, icon-xl and icon-expressive, the kit’s 32, 40, 48, 64 and 44px squares. The kit draws no danger icon-only button; the code allows one.
StateEnabled · Hover · Active · Focus · Disabled · Skeleton—Pseudo-classes in code, per governance rule 7. Disabled is the native attribute. Skeleton has no counterpart.
IconBoolean + swapchildrenComposition. The caller passes the icon as a child instead of choosing one from the kit’s list.