Button
A button triggers an action. It is Headless UI's Button with the ZUI
variants, tones 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/primitives/button'
Variants
variant is the style only: solid is filled, outline bordered,
ghost flat, link inline text with no control box.
Tones
tone is the colour. solid defaults to primary (the brand action);
the flat variants default to neutral (quiet chrome). danger marks the
irreversible action.
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.
Icons
leadingIcon and trailingIcon are wrapped in an aria-hidden span and
sized 1em unless the SVG carries a size-* class.
Loading and disabled
loading swaps the leading icon for a spinner, announces loadingLabel,
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.
As a link
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.
Full width
Props
ButtonProps<TTag> extends Headless UI ButtonProps<TTag> (onClick,
disabled, type, autoFocus, …) with:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'solid' | 'outline' | 'ghost' | 'link' | 'solid' | Visual style only; the colour comes from tone |
tone | 'primary' | 'neutral' | 'danger' | 'primary' for solid, 'neutral' for outline / ghost / link | Semantic colour |
size | 'sm' | 'md' | 'lg' | 'icon' | 'md' | icon is a square the height of an md control |
fullWidth | boolean | false | w-full on the root |
loading | boolean | false | Spinner, aria-busy, clicks swallowed; stays focusable |
loadingLabel | string | 'Loading' | Visually hidden text announced with the spinner; the default is Labels.loading |
leadingIcon | ReactNode | — | Before the children; replaced by the spinner while loading |
trailingIcon | ReactNode | — | After the children |
disabled | boolean | false | Native attribute on <button>; aria-disabled + tabIndex -1 on any other tag |
as | ElementType | 'button' | Polymorphic tag or component; its props are typed |
track | string | — | Emits { component: 'button', action: 'click', name } to a TelemetryProvider on every click. Nothing is emitted without the prop |
className | string | — | Merged last, after the variant, tone and size classes |
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="button"] | button (or as) | Root. Also data-variant, data-tone, data-size, data-loading |
[data-slot="button-spinner"] | svg | Present while loading |
[data-slot="button-leading-icon"] | span | Wraps leadingIcon; aria-hidden |
[data-slot="button-trailing-icon"] | span | Wraps trailingIcon; aria-hidden |
[data-slot="button-content"] | span | The children, visually hidden, on an icon-size button while loading |
/* Consumer CSS: square primary solid buttons only */
[data-slot="button"][data-variant="solid"][data-tone="primary"] {
border-radius: 0;
}
Tokens
| Token | Used for |
|---|---|
--control-height-sm/md/lg | Height per size; icon is --control-height-md square |
--button-padding-x-sm/md/lg | Horizontal padding per size |
--radius-button | Corner radius (defaults to --radius) |
--primary, --secondary, --danger, --accent, --input, --background and their -foreground pairs | Variant and tone colours |
--primary-text, --danger-text | Toned outline / ghost / link text |
--ring | Keyboard focus ring |
--duration-fast | Colour transition |
Accessibility
- Renders a real
<button type="button">by default; passtype="submit"in forms. - Keyboard focus shows a 2px
--ringring offset by 2px of--background(focus-visibleonly; no ring on mouse click). loadingsetsaria-busyandaria-disabledand keeps the element focusable;loadingLabelis read by screen readers.- Icon-only buttons need an
aria-label. Decorative icons arearia-hiddenby the wrapper. - A disabled anchor or
Linkgetsaria-disabled="true"andtabIndex={-1}.