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
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 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.
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 | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'primary' | |
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 |
loadingText | string | 'Loading' | Visually hidden text announced with the spinner |
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 |
className | string | — | Merged last, after the variant and size classes |
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="button"] | button (or as) | Root. Also data-variant, 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 buttons only */
[data-slot="button"][data-variant="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-md | Corner radius (from --radius) |
--primary, --secondary, --destructive, --accent, --input, --background and their -foreground pairs | Variant colours |
--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;loadingTextis 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}.