Skip to main content

Button

A button triggers an action. It is Headless UI's Button with the ZUI variants and sizes on top: the box comes from the control tokens, the colours from the semantic pairs, so a retheme moves every button at once.

import Button from '@zuilib/components/button'

Variants

Loading example

Sizes

sm, md and lg take their height from --control-height-* and their padding from --button-padding-x-*. icon is a square the height of an md control for a lone glyph; give it an aria-label.

Loading example

Icons

leadingIcon and trailingIcon are wrapped in an aria-hidden span and sized 1em unless the SVG carries a size-* class.

Loading example

Loading and disabled

loading swaps the leading icon for a spinner, announces loadingText, sets aria-busy and swallows clicks, but keeps the button focusable so keyboard focus is not lost when a click flips it to loading. disabled uses the native attribute on a <button> and aria-disabled on any other tag.

Loading example

as renders another tag or component and types its props: href on an anchor, to on a router Link. A disabled non-button gets aria-disabled and leaves the tab order.

Loading example

Full width

Loading example

Props

ButtonProps<TTag> extends Headless UI ButtonProps<TTag> (onClick, disabled, type, autoFocus, …) with:

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link''primary'
size'sm' | 'md' | 'lg' | 'icon''md'icon is a square the height of an md control
fullWidthbooleanfalsew-full on the root
loadingbooleanfalseSpinner, aria-busy, clicks swallowed; stays focusable
loadingTextstring'Loading'Visually hidden text announced with the spinner
leadingIconReactNodeBefore the children; replaced by the spinner while loading
trailingIconReactNodeAfter the children
disabledbooleanfalseNative attribute on <button>; aria-disabled + tabIndex -1 on any other tag
asElementType'button'Polymorphic tag or component; its props are typed
classNamestringMerged last, after the variant and size classes

Slots

SlotElementNotes
[data-slot="button"]button (or as)Root. Also data-variant, data-size, data-loading
[data-slot="button-spinner"]svgPresent while loading
[data-slot="button-leading-icon"]spanWraps leadingIcon; aria-hidden
[data-slot="button-trailing-icon"]spanWraps trailingIcon; aria-hidden
[data-slot="button-content"]spanThe children, visually hidden, on an icon-size button while loading
/* Consumer CSS: square primary buttons only */
[data-slot="button"][data-variant="primary"] {
border-radius: 0;
}

Tokens

TokenUsed for
--control-height-sm/md/lgHeight per size; icon is --control-height-md square
--button-padding-x-sm/md/lgHorizontal padding per size
--radius-mdCorner radius (from --radius)
--primary, --secondary, --destructive, --accent, --input, --background and their -foreground pairsVariant colours
--ringKeyboard focus ring
--duration-fastColour transition

Accessibility

  • Renders a real <button type="button"> by default; pass type="submit" in forms.
  • Keyboard focus shows a 2px --ring ring offset by 2px of --background (focus-visible only; no ring on mouse click).
  • loading sets aria-busy and aria-disabled and keeps the element focusable; loadingText is read by screen readers.
  • Icon-only buttons need an aria-label. Decorative icons are aria-hidden by the wrapper.
  • A disabled anchor or Link gets aria-disabled="true" and tabIndex={-1}.
  • AIButton: the same button with a sparkle and a generating state.
  • Menu and Popover: buttons that open panels.
  • Theming: retheme every button through the control tokens.