Skip to main content
BM

Button

A native button for actions, in three levels of emphasis.

View source on GitHub

ButtonExample.tsx
import { Button } from "@/components/Button";
import { Stack } from "@/components/Stack";

export function ButtonExample() {
  return (
    <Stack direction="horizontal" gap="sm">
      <Button variant="primary">Save changes</Button>
      <Button>Cancel</Button>
    </Stack>
  );
}

Examples

Variants

Sizes

Icons

Icon-only

  • sm · aria-label
  • md · aria-label
  • sm · VisuallyHidden
  • md · VisuallyHidden

Toggles

Announced as: “Show grid, toggle button, pressed”

Announced as: “Wrap lines, toggle button, not pressed”

Announced as: “Mark as done, toggle button, not pressed”

Announced as: “Notify me, toggle button, pressed”

Disabled (still focusable)

Your last note is still sending

Long label

Customized with a component token

CSS
.brandCta {
  --component-button-primary-background: var(--color-accent-2-fill);
  --component-button-primary-background-hover: var(--color-accent-2-fill-hover);
  --component-button-primary-background-active: var(--color-accent-2-fill-hover);
  --component-button-primary-border: var(--color-accent-2-fill);
  --component-button-primary-text: var(--color-accent-2-fill-text);
  --component-button-primary-text-active: var(--color-accent-2-fill-text);
}

Props

Any native prop not listed here goes to the root element unchanged.

Button props
PropTypeDefaultDescription
variant"primary" | "secondary" | "ghost""secondary"Emphasis: one primary action per view, secondary for the rest, ghost for toolbars.
size"sm" | "md""md"32px or 40px tall.
iconStartIconName—Decorative icon before the label.
iconEndIconName—Decorative icon after the label.
type"button" | "submit" | "reset""button"Defaults to "button", not the native "submit".
disabledbooleanfalseSets aria-disabled, not the native attribute: still focusable, announced, and never activated.
aria-pressedboolean | "mixed"—Makes it a toggle button. "mixed" has no look of its own.
disabledReasonReactNode—While disabled: why. It's the button's accessible description, and shows as a tooltip on hover, keyboard focus, and a press or tap.
nameTooltipbooleantrueIcon-only buttons named by aria-label or VisuallyHidden show that name as a tooltip; false turns it off.

Component tokens

Button component tokens
TokenCopyDescriptionAliasesLightDarkPreview
--component-button-radiusCorner radius.radius.control4pxfrom radius.44pxfrom radius.4
--component-button-font-familyLabel font.type.family.body'Inter Variable', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', Roboto, sans-seriffrom type.font-stack.sans'Inter Variable', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', Roboto, sans-seriffrom type.font-stack.sans
--component-button-font-weightLabel weight.type.weight.strong600from type.font-weight.600600from type.font-weight.600
--component-button-gapGap between the label and its icons.space.inline.sm0.5remfrom space.20.5remfrom space.2
--component-button-line-heightLine height of the label.type.leading.heading1.2from type.line-height.tight1.2from type.line-height.tight
--component-button-padding-blockBlock padding. It only shows once a label wraps; a single line stays exactly the height token.space.stack.xs0.25remfrom space.10.25remfrom space.1
--component-button-height-smBlock size of small buttons (32px, above the 24px target minimum).size.control.sm2remfrom size.82remfrom size.8
--component-button-height-mdBlock size of standard buttons (40px).size.control.md2.5remfrom size.102.5remfrom size.10
--component-button-padding-inline-smInline padding of small buttons.space.inline.sm0.5remfrom space.20.5remfrom space.2
--component-button-padding-inline-mdInline padding of standard buttons.space.inline.md1remfrom space.41remfrom space.4
--component-button-font-size-smLabel size of small buttons.type.size.small0.875remfrom type.font-size.sm0.875remfrom type.font-size.sm
--component-button-font-size-mdLabel size of standard buttons.type.size.body1remfrom type.font-size.md1remfrom type.font-size.md
--component-button-border-widthBorder width.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-button-focus-ring-colorFocus indicator. It's drawn outside the button, so it's checked against the surfaces.color.focus.ring#005f7bfrom color.cyan.700#54d8f4from color.cyan.300
--component-button-transition-durationDuration of hover color changes (0ms under reduced motion).motion.duration.instant100msfrom motion.time.100Reduced motion: 0ms100msfrom motion.time.100No preview
--component-button-transition-easingEasing of hover color changes.motion.easing.standardcubic-bezier(0.4, 0, 0.2, 1)from motion.curve.in-outcubic-bezier(0.4, 0, 0.2, 1)from motion.curve.in-outNo preview
--component-button-primary-backgroundPrimary background.color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-button-primary-background-hoverPrimary background on hover.color.action.background-hover#c58800from color.gold.500#fff5e2from color.champagne.50
--component-button-primary-background-activePrimary background while pressed (:active): a darker gold than the resting fill.color.action.background-pressed#795616from color.gold.700#b2a181from color.champagne.600
--component-button-primary-textPrimary text and icon color.color.action.text#0b0c0efrom color.neutral.950#0b0c0efrom color.neutral.950
--component-button-primary-text-activePrimary text and icon color while pressed (:active), on the darker fill.color.action.text-pressed#fbf8f2from color.sand.50#0b0c0efrom color.neutral.950
--component-button-primary-borderPrimary border color.color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-button-primary-pressed-backgroundPrimary toggle background while pressed (aria-pressed="true"): the darker gold of a pressed primary. It differs from the resting fill by lightness only, so a toggle whose state must read clearly also changes its label or shows a check icon.color.action.background-pressed#795616from color.gold.700#b2a181from color.champagne.600
--component-button-primary-pressed-background-hoverPrimary pressed toggle background on hover and while pressing: it stays the darker gold.color.action.background-pressed#795616from color.gold.700#b2a181from color.champagne.600
--component-button-primary-pressed-textPrimary pressed toggle text and icon color.color.action.text-pressed#fbf8f2from color.sand.50#0b0c0efrom color.neutral.950
--component-button-primary-pressed-borderPrimary pressed toggle border color: the pressed fill, which is the toggle's boundary (3:1 against the surfaces).color.action.background-pressed#795616from color.gold.700#b2a181from color.champagne.600
--component-button-secondary-backgroundSecondary background.color.surface.raised#fffffffrom color.gray.0#18202efrom color.slate.900
--component-button-secondary-background-hoverSecondary background on hover.color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-button-secondary-background-activeSecondary background while pressed (:active).color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-button-secondary-textSecondary text and icon color.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-button-secondary-borderSecondary border color.color.border.strong#6d7e90from color.gray.500#818b9efrom color.slate.400
--component-button-secondary-pressed-backgroundSecondary toggle background while pressed (aria-pressed="true").color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-button-secondary-pressed-background-hoverSecondary pressed toggle background on hover and while pressing.color.action.background-hover#c58800from color.gold.500#fff5e2from color.champagne.50
--component-button-secondary-pressed-textSecondary pressed toggle text and icon color.color.action.text#0b0c0efrom color.neutral.950#0b0c0efrom color.neutral.950
--component-button-secondary-pressed-borderSecondary pressed toggle border color.color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-button-ghost-backgroundGhost background.color.fill.nonergba(0, 0, 0, 0)from color.overlay.transparentrgba(0, 0, 0, 0)from color.overlay.transparent
--component-button-ghost-background-hoverGhost background on hover.color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-button-ghost-background-activeGhost background while pressed (:active).color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-button-ghost-textGhost text and icon color.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-button-ghost-borderGhost border color.color.fill.nonergba(0, 0, 0, 0)from color.overlay.transparentrgba(0, 0, 0, 0)from color.overlay.transparent
--component-button-ghost-pressed-backgroundGhost toggle background while pressed (aria-pressed="true").color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-button-ghost-pressed-background-hoverGhost pressed toggle background on hover and while pressing.color.action.background-hover#c58800from color.gold.500#fff5e2from color.champagne.50
--component-button-ghost-pressed-textGhost pressed toggle text and icon color.color.action.text#0b0c0efrom color.neutral.950#0b0c0efrom color.neutral.950
--component-button-ghost-pressed-borderGhost pressed toggle border color.color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-button-disabled-backgroundBackground while disabled.color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-button-disabled-textText while disabled. Exempt from contrast minimums (WCAG 1.4.3), but kept at 4.5:1 anyway.color.text.subtle#5d6876from color.gray.600#d3dce6from color.slate.200
--component-button-disabled-borderBorder while disabled.color.border.subtle#e5dccbfrom color.sand.200#272f3ffrom color.slate.800

Accessibility

  • Always a native <button>, type="button" unless set.
  • Named by its text. Icon-only buttons need an icon plus aria-label or aria-labelledby (a type error otherwise), or a VisuallyHidden label. With no visible text they're square.
  • aria-pressed makes a toggle on any variant: the label stays the same and the pressed look shows the state. A button whose label changes (“Show grid” ↔ “Hide grid”) is a plain button, without aria-pressed.
  • A toggled-on primary is a darker gold, so its on and off looks differ by lightness alone. Where the state must read more clearly, show a check icon while it's on, or use a plain button whose label changes.
  • disabled sets aria-disabled, not the native attribute: it stays focusable and announced, and blocks clicks, keys and form submission.
  • A disabledReason is the disabled button's accessible description, and shows as a tooltip on hover, keyboard focus, and a press or tap (the first Disabled example).
  • Icon-only buttons show their name as a tooltip on hover and keyboard focus, for sighted users who don't know the icon. It isn't announced again. Not for buttons named by aria-labelledby, whose name is already on screen.
  • In forced colors, disabled shows GrayText and pressed shows Highlight, since the forced palette drops their background and border colors.
  • Target size at least 32px tall; focus ring 2px in component.button.focus-ring-color, 3:1 against the surface.
Button keyboard
KeyAction
Enter / SpaceActivates the button, or flips a toggle. While disabled it does neither, and shows the disabledReason if there is one.
TabMoves focus to it, including when disabled.