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', flexDirection: 'column', gap: 24 }}>
  <div style={{ display: 'flex', gap: 32, alignItems: 'center' }}>
    <div style={{ width: 120 }}>
      <span style={{ fontSize: 12, color: '#666', fontWeight: 600 }}>State</span>
    </div>
    <div style={{ display: 'flex', gap: 32 }}>
      {['primary', 'secondary', 'tertiary', 'quaternary', 'destructive', 'destructive-outline', 'link-color', 'link-gray'].map((variant) => (
        <div key={variant} style={{ width: 80, textAlign: 'center' }}>
          <span style={{ fontSize: 12, color: '#666', fontWeight: 600, textTransform: 'capitalize' }}>
            {variant.replace('-', ' ')}
          </span>
        </div>
      ))}
    </div>
  </div>
  {['Default', 'Hover', 'Clicked', 'Loading', 'Disabled'].map((state) => (
    <div key={state} style={{ display: 'flex', gap: 32, alignItems: 'center' }}>
      <div style={{ width: 120 }}>
        <span style={{ fontSize: 12, color: '#999' }}>{state}</span>
      </div>
      <div style={{ display: 'flex', gap: 32 }}>
        {['primary', 'secondary', 'tertiary', 'quaternary', 'destructive', 'destructive-outline', 'link-color', 'link-gray'].map((variant) => (
          <div key={variant} style={{ width: 80, display: 'flex', justifyContent: 'center' }}>
            {state === 'Default' && <Button variant={variant}>Button</Button>}
            {state === 'Hover' && (
              <>
                {variant === 'primary' && <Button variant={variant} className="bg-(--Labels-Secondary)">Button</Button>}
                {variant === 'secondary' && <Button variant={variant} className="bg-(--Grays-Gray-1)">Button</Button>}
                {variant === 'tertiary' && <Button variant={variant} className="bg-(--Grays-Gray-1)">Button</Button>}
                {variant === 'quaternary' && <Button variant={variant} className="bg-(--Grays-Tinted-Default)">Button</Button>}
                {variant === 'destructive' && <Button variant={variant} className="bg-[color-mix(in_srgb,var(--Labels-Error)_90%,white)]">Button</Button>}
                {variant === 'destructive-outline' && <Button variant={variant} className="bg-(--Labels-Error)/10">Button</Button>}
                {variant === 'link-color' && <Button variant={variant} className="underline">Button</Button>}
                {variant === 'link-gray' && <Button variant={variant} className="underline text-(--Labels-Primary)">Button</Button>}
              </>
            )}
            {state === 'Clicked' && (
              <>
                {variant === 'primary' && <Button variant={variant} className="bg-(--Labels-Secondary) ring-1 ring-inset ring-(--Labels-Primary)">Button</Button>}
                {variant === 'secondary' && <Button variant={variant} className="bg-(--Grays-Gray-2)">Button</Button>}
                {variant === 'tertiary' && <Button variant={variant} className="bg-(--Grays-Gray-2)">Button</Button>}
                {variant === 'quaternary' && <Button variant={variant} className="bg-(--Grays-Tinted-Emphasized)">Button</Button>}
                {variant === 'destructive' && <Button variant={variant} className="bg-[color-mix(in_srgb,var(--Labels-Error)_90%,black)]">Button</Button>}
                {variant === 'destructive-outline' && <Button variant={variant} className="bg-(--Labels-Error)/20">Button</Button>}
                {variant === 'link-color' && <Button variant={variant} className="underline">Button</Button>}
                {variant === 'link-gray' && <Button variant={variant} className="underline text-(--Labels-Primary)">Button</Button>}
              </>
            )}
            {state === 'Loading' && <Button variant={variant} loading>Button</Button>}
            {state === 'Disabled' && <Button variant={variant} disabled>Button</Button>}
          </div>
        ))}
      </div>
    </div>
  ))}
</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"