Skip to main content

Button

  • Component overview: The Primary button is used for a single primary action on a page — used standalone, never placed side by side with other buttons.
  • Size baseline: Small=32px, Medium=40px, Large=48px, Icon Only=28px.
  • Implementation note: ui/button retains only the structural skeleton; the design layer owns all visual tokens.
  • Figma spec

Variants

8 visual variants to cover different semantic levels.

Result
Loading...
Live Editor
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
  <Button variant="primary">Primary</Button>
  <Button variant="secondary">Secondary</Button>
  <Button variant="tertiary">Tertiary</Button>
  <Button variant="quaternary">Quaternary</Button>
  <Button variant="destructive">Destructive</Button>
  <Button variant="destructive-outline">Destructive Outline</Button>
  <Button variant="link-color">Link Color</Button>
  <Button variant="link-gray">Link Gray</Button>
</div>
VariantDescription
PrimarySolid dark background with white text — emphasizes the primary action
SecondaryWhite background with a border — secondary to Primary
TertiaryTransparent, no border — shows a secondary background color on hover
QuaternaryTransparent, no border — shows a tinted gray background on hover, lightest
DestructiveSolid error-color background — emphasizes dangerous actions
Destructive OutlineError-color border and text — softer emphasis for dangerous actions
Link ColorBrand-color text link style
Link GrayGray text link style — transitions to primary color on hover

States

5 interaction states: Default / Hover / Clicked / Loading / Disabled.

Result
Loading...
Live Editor
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <span style={{ fontSize: 12, color: '#999' }}>Default</span>
    <Button>Button</Button>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <span style={{ fontSize: 12, color: '#999' }}>Hover</span>
    <Button className="bg-[var(--Labels-Secondary)]">Button</Button>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <span style={{ fontSize: 12, color: '#999' }}>Clicked</span>
    <Button className="bg-[var(--Labels-Secondary)]">Button</Button>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <span style={{ fontSize: 12, color: '#999' }}>Loading</span>
    <Button loading>Button</Button>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <span style={{ fontSize: 12, color: '#999' }}>Disabled</span>
    <Button disabled>Button</Button>
  </div>
</div>

Loading State

The loading prop automatically disables the button and displays the built-in Spinner. Supported by all variants.

Result
Loading...
Live Editor
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
  <Button variant="primary" loading>
    Primary
  </Button>
  <Button variant="secondary" loading>
    Secondary
  </Button>
  <Button variant="tertiary" loading>
    Tertiary
  </Button>
  <Button variant="quaternary" loading>
    Quaternary
  </Button>
  <Button variant="destructive" loading>
    Destructive
  </Button>
  <Button variant="destructive-outline" loading>
    Outline
  </Button>
  <Button variant="link-color" loading>
    Link Color
  </Button>
  <Button variant="link-gray" loading>
    Link Gray
  </Button>
</div>

Icon Button

When size="icon", the button renders as a 28×28 square. aria-label is required to ensure accessibility.

Result
Loading...
Live Editor
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
  <Button size="icon" variant="primary" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="secondary" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="tertiary" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="quaternary" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="destructive" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="destructive-outline" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="link-color" aria-label="Add">
    <Plus size={16} />
  </Button>
  <Button size="icon" variant="link-gray" aria-label="Add">
    <Plus size={16} />
  </Button>
</div>

Icon-Only Tertiary / Quaternary buttons give different interaction feedback from their text counterparts:

  • Tertiary: Hover / Clicked use the Tinted background scale (Grays/Tinted/Default, Grays/Tinted/Emphasized) instead of the Gray scale used by text buttons.
  • Quaternary: drops background feedback entirely and conveys interaction through icon color only — Default Labels/Tertiary → Hover Labels/Primary → Clicked Labels/Secondary.

Size Spec

Result
Loading...
Live Editor
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 24 }}>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <Button variant="secondary" size="lg">
      Large
    </Button>
    <span style={{ fontSize: 12, color: '#999' }}>48px</span>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <Button variant="secondary" size="default">
      Medium
    </Button>
    <span style={{ fontSize: 12, color: '#999' }}>40px</span>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <Button variant="secondary" size="sm">
      Small
    </Button>
    <span style={{ fontSize: 12, color: '#999' }}>32px</span>
  </div>
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
    <Button variant="secondary" size="icon" aria-label="Add">
      <Plus size={16} />
    </Button>
    <span style={{ fontSize: 12, color: '#999' }}>28px</span>
  </div>
</div>
DimensionLargeMediumSmallIcon Only
Height48px40px32px28px
Min width100px100px70px28px
Horizontal padding24px24px16px0
Border radius5px5px5px5px

Props

PropTypeDefaultDescription
childrenReactNode-Button content
variant'primary' | 'secondary' | 'tertiary' | 'quaternary' | 'destructive' | 'destructive-outline' | 'link-color' | 'link-gray''primary'Visual variant
size'default' | 'sm' | 'lg' | 'icon''default'Size: sm=32px, default=40px, lg=48px, icon=28px
loadingbooleanfalseLoading state — shows Spinner and disables interaction
disabledbooleanfalseDisabled state
onClickMouseEventHandler-Click callback
aria-labelstring-Accessibility label — required when size="icon"