Skip to content

Button

Triggers an action or event when clicked. The most fundamental interactive element in any UI.

  • CTA
  • Action Button

Overview

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:

  • To trigger an action (submit, save, delete, confirm)
  • To initiate a process (upload, export, send)
  • To toggle a state when the result isn't instant (use a Switch for instant toggles)

When NOT to use a Button:

Use 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.

Variants

Common Button Variants

VariantPurposeVisual Treatment
PrimaryThe main call-to-action on a page. One per section maximum.Solid fill with brand color, white text. High visual weight.
SecondarySupporting actions that complement the primary.Outlined or muted fill. Lower visual weight than primary.
Ghost / TertiaryLow-emphasis actions like "Cancel" or "Skip".No fill, no border. Text-only with hover background.
DestructiveIrreversible actions (delete, remove, revoke).Red fill or red outline. Signals danger.
Link ButtonActions that look like inline text links but behave as buttons.Underlined text, no background.
Icon-onlyCompact actions in toolbars or tight layouts.Square aspect ratio, icon centered. Always needs aria-label. See Icon Button.

Size Variants

SizeHeightPadding (horizontal)Font SizeUse Case
Small (sm)32px12px13pxInline actions, table rows, dense UIs
Medium (md)40px16px14pxDefault for most interfaces
Large (lg)48px24px16pxHero sections, mobile-first layouts

Shape Variants

Experiment with border-radius values using our Border Radius Generator:

  • Rounded (4–8px): Professional, enterprise feel
  • Pill (9999px): Friendly, modern, consumer-facing
  • Square (0px): Brutalist or highly structured layouts

Properties

Button Properties

PropertyTypeDefaultDescription
variant'primary' | 'secondary' | 'ghost' | 'destructive' | 'link''primary'Visual style of the button
size'sm' | 'md' | 'lg''md'Controls height, padding, and font size
disabledbooleanfalsePrevents interaction and applies disabled styling
loadingbooleanfalseShows a spinner and disables interaction
fullWidthbooleanfalseStretches button to fill container width
type'button' | 'submit' | 'reset''button'HTML button type. Always set explicitly.
leftIconReactNode—Icon rendered before the label
rightIconReactNode—Icon rendered after the label
asElementType'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.

Token Mappings

Design Token Mappings

Buttons touch almost every token category. Here's how they map. For a deeper dive on tokens, read our Design Tokens Complete Guide.

Token CategoryToken ExampleButton Usage
Color – Fill--color-primary-600Primary button background
Color – Text--color-on-primaryPrimary button label color
Color – Border--color-primary-700Secondary button border
Color – Destructive--color-error-600Destructive button background
Spacing – Padding--space-3 / --space-4Horizontal padding per size
Spacing – Gap--space-2Gap between icon and label
Border Radius--radius-md (8px)Corner rounding. Preview with Border Radius Generator.
Typography--font-weight-semibold, --font-size-smLabel text styling
Shadow--shadow-xsSubtle 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.

States

Interactive States

StateVisual ChangeBehavior
DefaultBase styling per variantReady for interaction
HoverSlight background darken (8–12%) or lighten. Cursor changes to pointer.Indicates interactivity. Transition: 150ms ease.
FocusVisible focus ring (2px offset, high-contrast color). Never remove this.Keyboard navigation indicator. Must meet WCAG 2.1 SC 2.4.7.
Active / PressedScale down slightly (transform: scale(0.98)) or further darken.Confirms the click registered.
DisabledReduced opacity (0.4–0.5). Cursor changes to not-allowed.aria-disabled="true" preferred over disabled attribute for screen reader announcements.
LoadingSpinner 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.

Focus Ring Best Practices

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;
}

Accessibility

WCAG Requirements

CriterionLevelRequirement
SC 1.4.3 Contrast (Minimum)AAButton text must have 4.5:1 contrast against the button background. Use our Contrast Checker.
SC 1.4.11 Non-text ContrastAAButton boundary (border or fill) must have 3:1 contrast against the page background.
SC 2.4.7 Focus VisibleAAFocus indicator must be visible when navigating by keyboard.
SC 2.5.8 Target Size (Minimum)AAMinimum 24×24px target size (WCAG 2.2). Aim for 44×44px on touch devices.
SC 4.1.2 Name, Role, ValueAButtons must have an accessible name. Icon-only buttons require aria-label.

ARIA Attributes

  • Use native <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).
  • Icon-only buttons: Add aria-label="Delete item" or wrap with visually-hidden text.
  • Toggle buttons: Use aria-pressed="true|false".
  • Loading state: Add aria-busy="true" and optionally aria-label="Saving, please wait".
  • Disabled: Prefer aria-disabled="true" over the disabled attribute so screen readers can still discover and announce the button.

Keyboard Interaction

KeyAction
EnterActivates the button
SpaceActivates the button (native <button> handles both)
TabMoves focus to the next focusable element
Shift + TabMoves focus to the previous focusable element

For deeper accessibility patterns, see our WCAG Practical Guide and Keyboard Accessibility Guide.

Usage Guidelines

Do's

  • ✅ Use a clear, verb-first label. "Save changes" not "Changes" or "OK". The label should describe the action.
  • ✅ Limit to 2–3 button variants per page. One primary, one secondary, maybe a ghost. More than that creates visual noise.
  • ✅ Place the primary action on the right in button pairs (e.g., "Cancel" left, "Submit" right) — this follows the natural reading flow in LTR languages.
  • ✅ Show a loading state during async operations. Never let users wonder if their click registered.
  • ✅ Keep button widths consistent within a group. Use min-width or full-width in mobile layouts.

Don'ts

  • ❌ Don't use buttons for navigation. Use <a> tags. If it goes to a URL, it's a link.
  • ❌ Don't disable buttons without explanation. If a form isn't valid, show validation errors instead. Disabled buttons with no context frustrate users.
  • ❌ Don't use "Click here" as a label. It's meaningless to screen readers and tells users nothing about the action.
  • ❌ Don't put two primary buttons side by side. If everything is important, nothing is.
  • ❌ Don't rely on color alone to distinguish destructive buttons. Add an icon (trash, warning) or use clear labeling like "Delete permanently".

Content Guidelines

  • Labels should be 2–4 words. If you need more, reconsider your UX.
  • Use sentence case ("Save changes") not title case ("Save Changes") — it reads more naturally.
  • For destructive actions, be specific: "Delete project" not just "Delete".

Learn more about button styling in our CSS Buttons guide and experiment live with our Button Generator.

Code Snippets

html
<!-- 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>
tsx
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>

Design Systems

Cross-System Comparison

FeatureMaterial 3Shadcn/uiRadixAnt Design
VariantsFilled, Outlined, Text, Elevated, Tonaldefault, destructive, outline, secondary, ghost, linkUnstyled primitive — BYO stylesprimary, default, dashed, text, link
SizesNot explicit (density via tokens)default, sm, lg, iconN/A (primitive)large, middle, small
LoadingCircularProgress overlayNo built-in loadingN/ABuilt-in loading prop
Icon supportIcon prop + FAB variantsManual via childrenN/Aicon prop
Ripple effectYes (Material signature)NoNoNo
PolymorphicNoasChild via Radix SlotasChild patternNo
AccessibilityBuilt-in focus managementInherits Radix patternsFully accessible primitivesBasic ARIA support

Notable Approaches

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.

ActionsCTAAction Button