Buttons

Buttons allow users to take actions and make choices with a single tap. They communicate actions that users can take and are typically placed throughout your UI.


Usage

The base .Button class provides core button styling. Add variant classes to change appearance.


Variants

All Variants

Primary

The primary button is for the main action on a page. Use sparingly — one primary action per section.

Secondary

Secondary buttons are for alternative actions. They pair well with primary buttons.

Tertiary

Tertiary buttons are for less prominent actions. Use for actions like “Cancel” or “Back”.

Ghost

Ghost buttons are minimal, blending into the background until hovered.

Outline

Outline buttons have a border but no fill. Good for secondary actions with more visual weight than ghost.

Danger

Danger buttons indicate destructive actions like delete or remove.


Sizes

Buttons come in three sizes: small, medium (default), and large.


With Icons

Buttons can include icons for visual reinforcement. Icons can be leading or trailing.

Leading Icon

Trailing Icon


Icon-Only Buttons

For actions where the icon is self-explanatory. Always include an aria-label.


States

Loading

Show a loading state when an action is in progress. Disable interaction during loading.

Disabled

Disabled buttons indicate an action is unavailable.


Button Groups

Group related buttons together.

Icon Button Group


Full Width

Buttons can expand to fill their container.


Common Patterns

Destructive Confirmation

Form Submit

Toolbar


Customization

Override button styles using CSS custom properties:

/* Custom primary color */
.Button--primary {
  --button-bg: oklch(55% 0.2 150);
  --button-hover: oklch(50% 0.2 150);
  background-color: var(--button-bg);
}

.Button--primary:hover {
  background-color: var(--button-hover);
}

/* Custom size */
.Button--xl {
  padding: var(--space-4) var(--space-8);
  font-size: 1.125rem;
}

/* Custom border radius */
.Button--rounded {
  border-radius: var(--space-6);
}

API Reference

Base Classes

Class Description
.Button Base button styles (required)

Variant Classes

Class Description
.Button--primary Primary action button (solid accent color)
.Button--secondary Secondary action button (subtle background)
.Button--tertiary Tertiary action button (text only)
.Button--ghost Minimal button (transparent until hover)
.Button--outline Outlined button (border, no fill)
.Button--danger Destructive action button (red)

Size Classes

Class Description
.Button--small Small button (28px height)
.Button--large Large button (44px height)

Modifier Classes

Class Description
.Button--icon Icon-only button (square aspect ratio)
.Button--block Full-width button
.Button--loading Loading state (use with disabled)

Icon Classes

Class Description
.Button-icon Icon inside button
.Button-icon--trailing Place icon after text
.Button-spinner Loading spinner element

Button Group Classes

Class Description
.ButtonGroup Container for grouped buttons
.ButtonGroup-item--active Active state for segmented control

CSS Reference

/* Base Button */
.Button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: 1px solid transparent;
  border-radius: var(--r-s);
  font-family: var(--font-sans);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.2;
  cursor: pointer;
  transition: background-color 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s;
  white-space: nowrap;
  text-decoration: none;
  background-color: var(--bg-s);
  color: var(--fg);
}

.Button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Variants */
.Button--primary {
  background-color: var(--accent);
  color: var(--fg-on-accent);
  border-color: transparent;
}

.Button--primary:hover {
  filter: brightness(1.1);
}

.Button--secondary {
  background-color: var(--bg-s);
  color: var(--fg);
  border-color: var(--bd);
}

.Button--secondary:hover {
  background-color: var(--bd);
}

.Button--tertiary {
  background-color: transparent;
  color: var(--fg-3);
  border-color: transparent;
}

.Button--tertiary:hover {
  background-color: var(--bg-s);
  color: var(--fg);
}

.Button--ghost {
  background-color: transparent;
  color: var(--fg);
  border-color: transparent;
}

.Button--ghost:hover {
  background-color: var(--bg-s);
}

.Button--outline {
  background-color: transparent;
  color: var(--accent);
  border-color: var(--accent);
}

.Button--outline:hover {
  background-color: oklch(from var(--accent) l c h / 0.1);
}

.Button--danger {
  background-color: oklch(55% 0.2 25);
  color: white;
  border-color: transparent;
}

.Button--danger:hover {
  background-color: oklch(50% 0.2 25);
}

/* Sizes */
.Button--small {
  padding: var(--space-1) var(--space-3);
  font-size: 0.75rem;
  height: 28px;
}

.Button--large {
  padding: var(--space-3) var(--space-6);
  font-size: 1rem;
  height: 44px;
}

/* Icon-Only */
.Button--icon {
  width: 36px;
  height: 36px;
  padding: 0;
}

.Button--icon.Button--small {
  width: 28px;
  height: 28px;
}

.Button--icon.Button--large {
  width: 44px;
  height: 44px;
}

/* Full Width */
.Button--block {
  display: flex;
  width: 100%;
}

/* Loading */
.Button--loading {
  position: relative;
  color: transparent;
  pointer-events: none;
}

.Button-spinner {
  width: 16px;
  height: 16px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: button-spin 0.6s linear infinite;
}

@keyframes button-spin {
  to { transform: rotate(360deg); }
}

/* Disabled */
.Button:disabled,
.Button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Icon inside button */
.Button-icon {
  font-size: 1.1em;
  flex-shrink: 0;
}

.Button-icon--trailing {
  order: 1;
}

/* Button Group */
.ButtonGroup {
  display: inline-flex;
  gap: 0;
}

.ButtonGroup .Button {
  border-radius: 0;
}

.ButtonGroup .Button:first-child {
  border-radius: var(--r-s) 0 0 var(--r-s);
}

.ButtonGroup .Button:last-child {
  border-radius: 0 var(--r-s) var(--r-s) 0;
}

.ButtonGroup .Button + .Button {
  margin-left: -1px;
}

.ButtonGroup-item--active {
  background-color: var(--accent);
  color: var(--fg-on-accent);
  border-color: var(--accent);
  z-index: 1;
}

Accessibility

Keyboard Support

Key Action
Enter Activates the button
Space Activates the button
Tab Moves focus to the button

Screen Readers

<!-- Standard button — text is read automatically -->
<button class="Button Button--primary">Submit Form</button>

<!-- Icon-only — requires aria-label -->
<button class="Button Button--icon" aria-label="Close dialog">
    <i class="ph ph-x"></i>
</button>

<!-- Loading state — announce status -->
<button class="Button Button--primary" aria-busy="true" disabled>
    <span class="Button-spinner"></span>
    Saving...
</button>

<!-- Toggle button -->
<button class="Button Button--secondary" aria-pressed="true">
    <i class="ph ph-star-fill"></i>
    Starred
</button>

Disabled vs aria-disabled

<!-- Native disabled — removes from tab order -->
<button class="Button Button--primary" disabled>Cannot Click</button>

<!-- aria-disabled — keeps in tab order (for tooltips) -->
<button class="Button Button--primary" aria-disabled="true">
    Upgrade Required
</button>

Best Practices

Do

  • Use clear, action-oriented labels — “Save Changes” not “Submit”
  • Lead with a verb — “Create Project”, “Delete File”
  • Limit primary buttons — One per section/view
  • Show loading states — Feedback during async actions
  • Size touch targets — Minimum 44px for mobile

Don’t

  • Use vague labels — “Click Here”, “OK”, “Yes”
  • Disable without explanation — Provide tooltip or helper text
  • Use too many styles — Stick to 2-3 variants per view
  • Hide important actions — Primary actions should be visible
  • Forget hover/focus states — Essential for accessibility