Components
Button
The action a person takes next: six intents, three sizes, and a press you can feel.
Examples
Intents
Primary is the one action a region is about; everything else steps down from it.
<Button variant="primary">Run backtest</Button>
<Button variant="accent">Preview</Button>
<Button variant="secondary">Duplicate</Button>
<Button variant="outline">Export</Button>
<Button variant="ghost">Cancel</Button>
<Button variant="danger">Delete run</Button>Sizes
Small for toolbars and dense tables, medium as the default, large for the one action on an empty state.
<Button size="sm" variant="secondary">Small</Button>
<Button size="md" variant="secondary">Medium</Button>
<Button size="lg" variant="primary">Large</Button>States
Hover, focus and loading are shown as specimens, so the interaction is reviewable in a screenshot as well as by using it.
Focus-visible
A 2px accent ring, offset by 2px
Loading
Width preserved, aria-busy set, still focusable
Disabled
Native disabled, never whole-component opacity
{/* focus-visible: the platform ring, 2px accent, 2px offset */}
<Button variant="secondary" className="ring-2 ring-accent-default ring-offset-2 ring-offset-surface-canvas">
Duplicate
</Button>
<Button variant="primary" loading>Saving</Button>
<Button variant="secondary" disabled>Unavailable</Button>With an icon
Icons are decorative and optically aligned; the label carries the meaning, so an icon never becomes the only name.
<Button variant="primary" leadingIcon={<RiPlayLine />}>Run</Button>
<Button variant="secondary" leadingIcon={<RiDownloadLine />}>Export</Button>
<Button variant="secondary" trailingIcon={<RiArrowRightLine />}>Continue</Button>Icon-only
An icon-only action is an `IconButton`, and its `label` is required — that is the whole difference between the two components.
<IconButton label="Add instrument" icon={<RiAddLine />} />
<IconButton label="Settings" icon={<RiSettings3Line />} variant="ghost" />
<IconButton label="More actions" icon={<RiMoreLine />} variant="solid" showTooltip />Loading and disabled
Loading keeps the label and the width and sets aria-busy; disabled is for an action that genuinely cannot run.
<Button variant="primary" loading loadingLabel="Saving the run">Saving</Button>
<Button variant="secondary" disabled>Unavailable</Button>A block action
`block` fills the available width — for the single action at the foot of a form or a card.
<Button variant="primary" block>Publish revision</Button>A link that looks like a button
An anchor keeps the anchor contract and borrows the button's recipe; never put a link inside a button.
<Link href="/charts" className={buttonVariants({ variant: "secondary" })}>
Open the chart catalogue
</Link>Usage
import { Button, buttonVariants } from "@luc/ui";<Button variant="primary" onClick={runBacktest}>
Run backtest
</Button>`type` defaults to `button` rather than `submit`, so a Button inside a form never submits it by accident.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
Button | 8 documented props | ({ className, variant, size, block, leadingIcon, trailingIcon, loading, loadingLabel, tactile, disabled, children, onClick, type, onPointerDown, onPointerMove, onPointerEnter, onPointerLeave, ref, ..… |
Button props
Type: ButtonProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
block? | boolean | undefined | — | |
leadingIcon? | ReactNode | — | |
loading? | boolean | undefined | — | Busy state. The button keeps its width, stays in the tab order and reports `aria-busy` — it is *not* the same as `disabled`. |
loadingLabel? | string | undefined | — | |
size? | "sm" | "md" | "lg" | null | undefined | — | |
tactile? | boolean | undefined | — | Press ink and pointer light. On by default — a filled control that answers the finger is the family's signature — and switchable off for print, dense toolbars or a surface that al… |
trailingIcon? | ReactNode | — | |
variant? | "danger" | "primary" | "accent" | "secondary" | "outline" | "ghost" | null | undefined | — |
Guidelines
Do
- Reach for `loading` rather than swapping the label to “Loading…”: the label stays readable and the control keeps its width.
- Keep one primary action visible per region, and let everything else step down from it.
- Use `IconButton` for an icon-only action — its `label` is required, which is the whole difference between the two components.
Don't
- Disable a button instead of explaining why the action is unavailable.
- Place two primary buttons side by side: the region loses its single obvious next step.
- Put an anchor inside a button — `buttonVariants` on the anchor is the supported way to make a link look like one.
Accessibility
The control is a real `<button>` with `type="button"` by default, so Enter and Space, focus order and the accessible name come from the browser. `loading` sets `aria-busy` and blocks activation while leaving the button focusable; only `disabled` removes it from the tab order.
Related