Button

Interactive controls for actions and navigation. Built on headless primitives with responsive sizing, semantic variants, and composable button groups.

Anatomy

Every button is composed of a single <Button> primitive from @base-ui/react, styled through class-variance-authority (CVA). The component exposes two variant axes: variant controls visual style and size controls dimensions. Both produce fully responsive output using Tailwind CSS media queries.

inline-start iconlabelinline-end icon

Variants

Six semantic variants map to levels of visual emphasis. Use higher emphasis sparingly — a screen should rarely have more than one primary action.

variant="default"

High emphasis. Use for the single most important action on screen.

variant="secondary"

Medium emphasis. Pair with primary to create visual hierarchy.

variant="outline"

Medium-low emphasis. Works well for cancel, back, or filter actions.

variant="ghost"

Low emphasis. For tertiary actions, icon buttons in dense UI.

variant="destructive"

Danger signal. Reserve for irreversible actions like delete or remove.

variant="link"

Inline navigation. Renders as underlined text, no background.

Responsive Sizes

Three text-button sizes and one icon-only size. Each scales automatically across breakpoints using Tailwind media queries — no JavaScript, no layout shift. The base (mobile-first) value grows at sm: (640px) and again at lg: (1024px).

TokenPreviewResponsive HeightsWhen to Use
size="sm"h-7 / sm:h-8 / lg:h-9Compact UI, toolbars, secondary actions on mobile
size="default"h-8 / sm:h-9 / lg:h-10Primary actions, forms, navigation
size="lg"h-10 / sm:h-11 / lg:h-12Hero CTAs, onboarding, marketing pages
size="icon"size-8 / sm:size-9 / lg:size-10Icon-only actions, close buttons, toggles

Side-by-side comparison

Icons in Buttons

Mark icons with data-icon="inline-start" or data-icon="inline-end" to automatically adjust padding. The button detects icon placement via has-data-[icon=*] selectors and reduces padding on the icon side for optical balance.

Leading icon

data-icon="inline-start"

Trailing icon

data-icon="inline-end"

Icon only

size="icon"

Button Groups

Wrap related actions in <ButtonGroup> to merge borders and radii automatically. Supports horizontal and vertical orientation with an optional <ButtonGroupSeparator> between items. See the ButtonGroup page for full documentation.

Horizontal

Vertical

With separator

Icon group (toolbar)

States

Buttons respond to focus, hover, active press, and disabled states. The focus ring uses the --ring token (brand-300) for accessibility. Active buttons translate 1px downward for a subtle press effect.

Resting

focus-visible

disabled, 50% opacity

Loading pattern

Hierarchy Patterns

Common pairings that create clear visual hierarchy. The primary action should always stand out, with supporting actions progressively de-emphasized.

Dialog footer

Danger zone

Empty state CTA

No projects yet

Toolbar

Token Map

Quick reference mapping each size token to its generated Tailwind classes across the three breakpoint tiers.

SizeMobile (base)Tablet (sm:)Desktop (lg:)
smh-7 px-2 text-xsh-8 px-2.5h-9 px-3
defaulth-8 px-2.5h-9 px-3h-10 px-4
lgh-10 px-4 text-baseh-11 px-5h-12 px-6
iconsize-8size-9size-10

Design Guidelines

Do

  • One primary per visible area. Multiple primary buttons dilute the visual hierarchy and leave users uncertain which action matters most.
  • Use verb-first labels. "Save changes", "Delete account", "Send invite" — the label should describe the outcome, not the input.
  • Pair destructive with a confirmation. Always gate irreversible actions behind a dialog or undo mechanism.
  • Let the size scale handle responsive. Rely on the built-in breakpoint scaling instead of conditionally swapping size props per viewport.
  • Maintain 44px minimum touch target. Even when the visual size is smaller, ensure the tappable area meets WCAG 2.5.8 on mobile.

Don't

  • Don't use icon-only without a tooltip or sr-only label. Icon buttons must have an accessible name via aria-label or a visually hidden <span>.
  • Don't override responsive sizes inline. Adding manual h-* classes breaks the design token chain and will drift from the system on future updates.
  • Don't nest buttons or wrap interactive elements. A button inside a link (or vice versa) violates HTML semantics and creates unpredictable behavior for screen readers.
  • Don't rely on color alone for destructive intent. Combine the destructive variant with a clear label ("Delete", "Remove") so the meaning is communicated even without color perception.
  • Don't disable without explanation. If a button is disabled, surface a tooltip or inline hint explaining why the action is unavailable.

Developer Reference

Accessibility

  • The focus ring uses focus-visible — it only appears on keyboard navigation, not mouse clicks.
  • active:translate-y-px provides a physical press cue, complementing the visual hover state.
  • aria-invalid triggers a destructive ring for form validation contexts.
  • disabled sets pointer-events: none and 50% opacity. The element remains in the tab order; screen readers announce it as disabled.
  • Always add <span className="sr-only">Label</span> inside icon-only buttons.

Usage

import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"

// Variants
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>

// Responsive sizes — each scales via Tailwind media queries
// Mobile (base) → Tablet (sm:) → Desktop (lg:)
<Button size="sm">Small</Button>     {/* h-7 → h-8 → h-9 */}
<Button>Default</Button>              {/* h-8 → h-9 → h-10 */}
<Button size="lg">Large</Button>     {/* h-10 → h-11 → h-12 */}
<Button size="icon"><Plus /></Button> {/* size-8 → size-9 → size-10 */}

// Icons with data attributes for optical padding
<Button>
  <Mail data-icon="inline-start" />
  Send Email
</Button>
<Button variant="outline">
  Continue
  <ChevronRight data-icon="inline-end" />
</Button>

// Button groups
<ButtonGroup>
  <Button variant="outline">Save</Button>
  <ButtonGroupSeparator />
  <Button variant="outline" size="icon">
    <ChevronDown />
  </Button>
</ButtonGroup>