Skip to content

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.

Sizes

Small for toolbars and dense tables, medium as the default, large for the one action on an empty state.

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

With an icon

Icons are decorative and optically aligned; the label carries the meaning, so an icon never becomes the only name.

Icon-only

An icon-only action is an `IconButton`, and its `label` is required — that is the whole difference between the two components.

Loading and disabled

Loading keeps the label and the width and sets aria-busy; disabled is for an action that genuinely cannot run.

A block action

`block` fills the available width — for the single action at the foot of a form or a card.

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.

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.

Exported symbols
ExportKindSignature
Button8 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.

Button props
PropTypeDefaultNotes
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.

Luc UI v1.0Buttondark theme