Loading…
Loading…
Triggers an action or event when clicked. The most fundamental interactive element in any UI.
The Button is the most fundamental interactive element in any user interface. It communicates that an action will occur when the user clicks or taps it — submitting a form, opening a dialog, navigating to a new page, or triggering a process.
Despite its simplicity, the button is where most design systems either shine or fall apart. A well-designed button system handles hierarchy (primary vs. secondary vs. tertiary), communicates state clearly (loading, disabled), and remains accessible to every user regardless of input method.
When to use a Button:
When NOT to use a Button:
<a>) styled as a link or a Navigation BarUse our Button Generator to experiment with padding, border-radius, shadow, and color values in real time. Check your text-to-background contrast with the Contrast Checker before shipping.
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Primary | The main call-to-action on a page. One per section maximum. | Solid fill with brand color, white text. High visual weight. |
| Secondary | Supporting actions that complement the primary. | Outlined or muted fill. Lower visual weight than primary. |
| Ghost / Tertiary | Low-emphasis actions like "Cancel" or "Skip". | No fill, no border. Text-only with hover background. |
| Destructive | Irreversible actions (delete, remove, revoke). | Red fill or red outline. Signals danger. |
| Link Button | Actions that look like inline text links but behave as buttons. | Underlined text, no background. |
| Icon-only | Compact actions in toolbars or tight layouts. | Square aspect ratio, icon centered. Always needs aria-label. See Icon Button. |
| Size | Height | Padding (horizontal) | Font Size | Use Case |
|---|---|---|---|---|
| Small (sm) | 32px | 12px | 13px | Inline actions, table rows, dense UIs |
| Medium (md) | 40px | 16px | 14px | Default for most interfaces |
| Large (lg) | 48px | 24px | 16px | Hero sections, mobile-first layouts |
Experiment with border-radius values using our Border Radius Generator:
| Property | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'ghost' | 'destructive' | 'link' | 'primary' | Visual style of the button |
size | 'sm' | 'md' | 'lg' | 'md' | Controls height, padding, and font size |
disabled | boolean | false | Prevents interaction and applies disabled styling |
loading | boolean | false | Shows a spinner and disables interaction |
fullWidth | boolean | false | Stretches button to fill container width |
type | 'button' | 'submit' | 'reset' | 'button' | HTML button type. Always set explicitly. |
leftIcon | ReactNode | — | Icon rendered before the label |
rightIcon | ReactNode | — | Icon rendered after the label |
as | ElementType | 'button' | Polymorphic element override (e.g., render as <a>) |
Important: Always set type="button" explicitly. Browsers default to type="submit" inside forms, which causes accidental form submissions — one of the most common button bugs in production.
Buttons touch almost every token category. Here's how they map. For a deeper dive on tokens, read our Design Tokens Complete Guide.
| Token Category | Token Example | Button Usage |
|---|---|---|
| Color – Fill | --color-primary-600 | Primary button background |
| Color – Text | --color-on-primary | Primary button label color |
| Color – Border | --color-primary-700 | Secondary button border |
| Color – Destructive | --color-error-600 | Destructive button background |
| Spacing – Padding | --space-3 / --space-4 | Horizontal padding per size |
| Spacing – Gap | --space-2 | Gap between icon and label |
| Border Radius | --radius-md (8px) | Corner rounding. Preview with Border Radius Generator. |
| Typography | --font-weight-semibold, --font-size-sm | Label text styling |
| Shadow | --shadow-xs | Subtle elevation on primary buttons |
| Transition | --duration-fast (150ms) | Hover/active state transitions |
Use our Color Palette Generator to create cohesive button color sets, and the Shadow Generator for elevation values.
| State | Visual Change | Behavior |
|---|---|---|
| Default | Base styling per variant | Ready for interaction |
| Hover | Slight background darken (8–12%) or lighten. Cursor changes to pointer. | Indicates interactivity. Transition: 150ms ease. |
| Focus | Visible focus ring (2px offset, high-contrast color). Never remove this. | Keyboard navigation indicator. Must meet WCAG 2.1 SC 2.4.7. |
| Active / Pressed | Scale down slightly (transform: scale(0.98)) or further darken. | Confirms the click registered. |
| Disabled | Reduced opacity (0.4–0.5). Cursor changes to not-allowed. | aria-disabled="true" preferred over disabled attribute for screen reader announcements. |
| Loading | Spinner replaces or sits beside label. Button is non-interactive. | Prevents double-submission. Keep the button width stable — don't let it collapse when the label disappears. |
Use a 2px solid ring with a 2px offset from the button edge. The ring color should have at least 3:1 contrast against the surrounding background (WCAG 2.1 SC 1.4.11). Test with our Contrast Checker.
button:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
| Criterion | Level | Requirement |
|---|---|---|
| SC 1.4.3 Contrast (Minimum) | AA | Button text must have 4.5:1 contrast against the button background. Use our Contrast Checker. |
| SC 1.4.11 Non-text Contrast | AA | Button boundary (border or fill) must have 3:1 contrast against the page background. |
| SC 2.4.7 Focus Visible | AA | Focus indicator must be visible when navigating by keyboard. |
| SC 2.5.8 Target Size (Minimum) | AA | Minimum 24×24px target size (WCAG 2.2). Aim for 44×44px on touch devices. |
| SC 4.1.2 Name, Role, Value | A | Buttons must have an accessible name. Icon-only buttons require aria-label. |
<button> — it has the button role built in. Avoid <div role="button"> unless absolutely necessary (you then need to handle Enter, Space, and focus yourself).aria-label="Delete item" or wrap with visually-hidden text.aria-pressed="true|false".aria-busy="true" and optionally aria-label="Saving, please wait".aria-disabled="true" over the disabled attribute so screen readers can still discover and announce the button.| Key | Action |
|---|---|
| Enter | Activates the button |
| Space | Activates the button (native <button> handles both) |
| Tab | Moves focus to the next focusable element |
| Shift + Tab | Moves focus to the previous focusable element |
For deeper accessibility patterns, see our WCAG Practical Guide and Keyboard Accessibility Guide.
min-width or full-width in mobile layouts.<a> tags. If it goes to a URL, it's a link.Learn more about button styling in our CSS Buttons guide and experiment live with our Button Generator.
<!-- Primary Button -->
<button type="button" class="btn btn-primary">
Save changes
</button>
<!-- Secondary Button -->
<button type="button" class="btn btn-secondary">
Cancel
</button>
<!-- Destructive Button -->
<button type="button" class="btn btn-destructive">
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Z"/>
</svg>
Delete project
</button>
<!-- Icon-only Button -->
<button type="button" class="btn btn-ghost btn-icon" aria-label="Close dialog">
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"/>
</svg>
</button>
<!-- Loading Button -->
<button type="button" class="btn btn-primary" aria-busy="true" disabled>
<span class="spinner" aria-hidden="true"></span>
Saving…
</button>import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
type Variant = "primary" | "secondary" | "ghost" | "destructive";
type Size = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
loading?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "primary",
size = "md",
loading = false,
leftIcon,
rightIcon,
disabled,
children,
className = "",
type = "button",
...props
},
ref,
) => {
return (
<button
ref={ref}
type={type}
disabled={disabled || loading}
aria-busy={loading || undefined}
aria-disabled={disabled || undefined}
className={`btn btn-${variant} btn-${size} ${className}`}
{...props}
>
{loading && <span className="spinner" aria-hidden="true" />}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
</button>
);
},
);
Button.displayName = "Button";
export default Button;
// Usage
<Button variant="primary" size="md" onClick={handleSave}>
Save changes
</Button>
<Button variant="destructive" loading={isDeleting}>
Delete project
</Button>| Feature | Material 3 | Shadcn/ui | Radix | Ant Design |
|---|---|---|---|---|
| Variants | Filled, Outlined, Text, Elevated, Tonal | default, destructive, outline, secondary, ghost, link | Unstyled primitive — BYO styles | primary, default, dashed, text, link |
| Sizes | Not explicit (density via tokens) | default, sm, lg, icon | N/A (primitive) | large, middle, small |
| Loading | CircularProgress overlay | No built-in loading | N/A | Built-in loading prop |
| Icon support | Icon prop + FAB variants | Manual via children | N/A | icon prop |
| Ripple effect | Yes (Material signature) | No | No | No |
| Polymorphic | No | asChild via Radix Slot | asChild pattern | No |
| Accessibility | Built-in focus management | Inherits Radix patterns | Fully accessible primitives | Basic ARIA support |
Material 3 introduced "Tonal" buttons — a middle ground between filled and outlined that uses a lighter shade of the primary color. This is excellent for secondary actions that need more emphasis than an outline but less than a filled button. Consider adopting this in your design system if you find the primary/secondary/ghost trio isn't granular enough.
Shadcn/ui leverages the Radix Slot component via asChild, allowing you to render a button's styles on any element — perfect for when you need a button that's actually a <Link> under the hood.
Radix Primitives doesn't provide a button primitive because native <button> already has the right semantics. Radix focuses on complex widgets (Dialog, Popover, etc.) where native elements fall short.
Ant Design bundles a loading prop directly into the button component, including an automatic spinner — a pragmatic approach that reduces boilerplate in enterprise apps.
For more on building component libraries, read our Building Component Libraries guide.