Skip to content
← Inventory

Button

Button styled by a tone × emphasis grammar: tone sets the semantic colour (neutral / brand / danger) and emphasis sets the visual weight (solid / soft / outline / ghost / transparent / link). Any pairing is valid — there is no fixed "primary/secondary" list; those looks are expressed as tone × emphasis pairs. An icon-only button (an icon-* size, or icon with no children) requires an aria-label; a dev-only console warning fires when it is missing. With asChild, styling and icon/dot decorations are merged onto the single child via Slot.

Usage

@desk/ui/components/button

<Flex align="center" gap="sm" justify="space-between" wrap="wrap">
  <Group gap="sm">
    <Button icon={Package} tone="brand">
      Request sample
    </Button>
    <Button emphasis="outline" icon={Heart} tone="neutral">
      Save to palette
    </Button>
    <Button
      aria-label="Share"
      emphasis="ghost"
      icon={Share2}
      tone="neutral"
    />
  </Group>
  <Button emphasis="ghost" icon={Trash2} tone="danger">
    Remove
  </Button>
</Flex>

Best practices

  • Express weight with tone×emphasis, not a variant prop — there is no "primary/secondary"; e.g. the destructive confirm is tone="danger".

  • Icon-only buttons MUST have an aria-label (a dev warning fires otherwise).

  • Use asChild to render a link/Slot child (e.g. Next <Link>) rather than nesting a button inside an anchor.

  • Reach for emphasis="ghost"/"link" in toolbars/headers — they carry data-edge-comp so containers can pull them flush to the edge.

Props

PropTypeDefaultDescription
tone"neutral" | "brand" | "danger" | null | undefined
radius"rounded" | "square" | "full" | null | undefined
squareboolean | null | undefined
emphasis"link" | "outline" | "solid" | "ghost" | "soft" | "transparent" | null | undefined
asInputboolean | null | undefined
align"center" | "end" | "start" | null | undefined
pressedboolean | null | undefined
sizeButtonSizeProp"base"

Size token; icon-* variants produce a square icon-only button. Also accepts a responsive object keyed by viewport breakpoints and/or @-prefixed container queries — e.g. { base: "sm", md: "base", "@sm": "lg" }.

asChildboolean | undefinedfalse

Merge props and styling onto the single child element via Slot.

loadingboolean | undefinedfalse

Show a spinner and disable interaction.

loadingLabelstring | undefined

Label shown in place of children while loading.

iconIconComponent | undefined

Leading icon (Lucide or SVG component); with no children it becomes an icon-only button.

iconClassNamestring | undefined

Extra classes for the leading icon.

suffixIconIconComponent | undefined

Trailing icon (Lucide or SVG component) rendered at the end.

suffixIconClassNamestring | undefined

Extra classes for the trailing icon.

dotstring | undefined

CSS colour for a status dot; empty/undefined renders no dot.

dotPlacement"end" | "start" | undefined"start"

Dot position relative to content.

expandableboolean | undefinedfalse

Icon-only at rest; expands on hover/focus-visible to reveal the label to the right of the icon. Collapsed geometry equals the matching icon-* size, expanded equals the text button of that size — so pass a text size (sm/base/lg) OR its icon twin (icon-sm …); both resolve identically. Requires both icon and children; ignored with asChild or square. The label stays in the DOM in both states (stable accessible name).

focusedboolean | undefined

Force the focus ring on — for documentation/testing only.

hoveredboolean | undefined

Force the hover state on — for documentation/testing only.

testIdstring

Also exports

  • constBUTTON_EMPHASES
  • constBUTTON_TONES
  • typeButtonEmphasis
  • typeButtonProps
  • typeButtonSize
  • typeButtonTone
  • constbuttonVariants