Button
The foundational interactive element. Eight semantic variants covering every action hierarchy from primary CTA to link-style navigation.
Overview
Button is the most-used component in the system. Its eight variants map to distinct interaction hierarchies: primary actions, secondary alternatives, tertiary supporting actions, destructive operations, link-style navigation, and social sign-in.
The loading prop replaces the label with a spinner without changing the button's dimensions, preventing layout shift. When neither label nor aria-label is provided, a console warning fires to catch accessibility gaps early.
Import
import { Button } from '@unflow/ui/components/Button';Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Button text |
variant | 'primary' | 'secondary' | 'tertiary' | 'link' | 'linkHighlight' | 'destructive' | 'destructiveSecondary' | 'social' | 'primary' | Visual and semantic variant |
size | 'xs' | 'sm' | 'md' | 'lg' | 'sm' | Button size |
loading | boolean | false | Shows a spinner loading state |
disabled | boolean | false | Disables the button |
fullWidth | boolean | false | Stretches the button to fill its container |
startIcon | ReactNode | — | Icon before the label |
endIcon | ReactNode | — | Icon after the label |
onClick | MouseEventHandler | — | Click handler |
All standard HTMLButtonElement props are forwarded to the underlying <button>.
Variants
<Button label="Primary" variant="primary" />
<Button label="Secondary" variant="secondary" />
<Button label="Tertiary" variant="tertiary" />
<Button label="Destructive" variant="destructive" />
<Button label="Destructive Secondary" variant="destructiveSecondary" />
<Button label="Link" variant="link" />
<Button label="Link Highlight" variant="linkHighlight" />Primary — Highest-emphasis action. Use for the single most important action on a surface. Background #14141A, hover bg-grey-700.
Secondary — Supporting action. bg-grey-50 with border-grey-300.
Tertiary — Low-emphasis. No background, hover bg-grey-100.
Destructive / Destructive Secondary — Irreversible operations. Full red vs. light red for two levels of emphasis.
Link / Link Highlight — Inline text-link style rendered as <button>. Underline is a CSS ::after pseudo-element at -bottom-0.5 to avoid descender clipping. linkHighlight uses text-indigo-500.
Social — OAuth provider buttons. Same visual as Secondary — pair with a provider logo in startIcon.
Sizes
<Button label="XS" size="xs" /> {/* 28px min-height */}
<Button label="SM" size="sm" /> {/* 32px */}
<Button label="MD" size="md" /> {/* 40px */}
<Button label="LG" size="lg" /> {/* 48px */}Loading state
<Button label="Save changes" loading />The button's dimensions are preserved — no layout shift. The loading label is i18n-aware and defaults to the button.loading translation key.
Customize the loading slot:
<Button
label="Save changes"
loading
slotProps={{
loading: {
label: { content: 'Saving your work…' },
disableLabel: false,
disableSpinner: false,
spinner: { size: 16, thickness: 2 },
},
}}
/>Disabled
<Button label="Disabled" disabled />Sets both disabled and aria-disabled on the native <button>. Opacity drops to 30% via disabled:opacity-30.
Full width
<Button label="Full width" fullWidth />With icons
import { Bell } from '@unflow-ui/icons/communication/bell';
<Button label="Notifications" startIcon={<Bell />} />
<Button label="Continue" endIcon={<ArrowRight />} />Customization (slotProps)
| Slot | Props | Description |
|---|---|---|
loading.label | { content?: string; className?: ClassName } | Custom loading text and styling |
loading.spinner | Pick<ISpinner, 'className' | 'size' | 'speed' | 'thickness'> | Spinner config during loading |
loading.disableLabel | boolean | Hide the loading text |
loading.disableSpinner | boolean | Hide the spinner icon |
label | { className?: ClassName } | Override label text styles |
startIconRoot | BaseHTMLProps<HTMLDivElement> | Override start icon wrapper |
endIconRoot | BaseHTMLProps<HTMLDivElement> | Override end icon wrapper |
Accessibility
- Renders a native
<button>— keyboard and screen-reader accessible by default. aria-labeldefaults to thelabelprop value.- Dev warning fires when neither
labelnoraria-labelis provided. disabledsets both HTMLdisabledandaria-disabled="true".- Loading state retains
aria-labelso screen readers still announce the button's purpose.