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.
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).
| Token | Preview | Responsive Heights | When to Use |
|---|---|---|---|
size="sm" | h-7 / sm:h-8 / lg:h-9 | Compact UI, toolbars, secondary actions on mobile | |
size="default" | h-8 / sm:h-9 / lg:h-10 | Primary actions, forms, navigation | |
size="lg" | h-10 / sm:h-11 / lg:h-12 | Hero CTAs, onboarding, marketing pages | |
size="icon" | size-8 / sm:size-9 / lg:size-10 | Icon-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.
| Size | Mobile (base) | Tablet (sm:) | Desktop (lg:) |
|---|---|---|---|
sm | h-7 px-2 text-xs | h-8 px-2.5 | h-9 px-3 |
default | h-8 px-2.5 | h-9 px-3 | h-10 px-4 |
lg | h-10 px-4 text-base | h-11 px-5 | h-12 px-6 |
icon | size-8 | size-9 | size-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-labelor 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-pxprovides a physical press cue, complementing the visual hover state.aria-invalidtriggers a destructive ring for form validation contexts.disabledsetspointer-events: noneand 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>