Button renders a <button> element (or a delegated child, via asChild)
with the product’s visual language baked in: variant, color, size, loading,
and an optional bound keyboard shortcut. All native button semantics are
preserved — form submission, type, disabled, focus handling.
Reach for a Button when the user’s action can be expressed in two or three
words. For navigation within the app, use a link styled like a button
(asChild over an <a>) so middle-click and right-click behave as expected.
Import
import { Button } from "@unkey/ui";
Variants
variant chooses the visual weight. A single screen should generally hold
one primary button (the action the user most likely came for), several
outline buttons (secondary actions), and ghost for tertiary affordances
that should fade into the surrounding UI.
Colors
color layers semantic intent on top of the variant. Use danger for
destructive actions (delete, revoke, force-disconnect), success for
positive confirmations that need emphasis, warning for cautionary ones,
and info for neutral informational actions.
The variant × color matrix is intentional: a primary danger is a
solid red destructive action, outline danger is a bordered-only
destructive action for lower-weight contexts (table row actions, settings
pages), and ghost danger is the lightest treatment, appropriate when the
destructive button sits inline with plain text.
Sizes
Five sizes are available. Use sm (the default) for most in-flow actions,
md/lg when the button anchors a form, and xlg/2xlg for
marketing-style hero CTAs.
States
A button can be disabled (non-interactive, dimmed) or loading
(non-interactive, spinner overlay). The two states have different semantics:
disabledmeans “you cannot do this right now” — usually because a precondition has not been met. The nativedisabledattribute is set, so the browser skips it in tab order.loadingmeans “we’re doing this right now, don’t click again”. The label stays visible, the button’s width is pinned so layout doesn’t shift, and clicks are ignored. Thearia-busy="true"attribute is set so assistive tech announces progress.
Keyboard shortcut
Pass a keyboard prop to bind a global shortcut. The display string is
rendered inside the button as a <kbd>, so users learn the binding by
seeing it. trigger is evaluated on every keydown and should return
true when the callback should fire — this lets you match complex
combinations like ⌘K without the library prescribing a key-parsing DSL.
Because the handler is registered at the document level, avoid putting more than one Button with the same trigger on the same page — the last one mounted wins.
Rendering as another element
Set asChild to render the Button as its direct child (via Radix’s Slot)
instead of a <button>. Useful for links that should look like primary
CTAs, while preserving href, middle-click, and right-click menu semantics.
<Button asChild variant="primary">
<a href="/dashboard">Open dashboard</a>
</Button>
When asChild is true, the Button contributes only the className and
click-handler wiring. The wrapping DOM node is whatever you pass in.
Accessibility
Button renders a native <button>, so all standard keyboard and assistive
tech behavior works: Enter and Space activate it, it participates in
tab order, and it exposes a button role automatically.
- When
loadingistrue, the button setsaria-busy="true"and the spinner includes ansr-onlytext announcement (loadingLabel, default: “Loading, please wait”). OverrideloadingLabelwhen the action is specific enough that “loading” is vague — e.g."Saving changes". - When
disabledistrue, the nativedisabledattribute is applied (which suppresses focus and clicks). For buttons that are temporarily unusable but should still receive focus so screen readers can explain why (e.g. with a tooltip), mark themaria-disabledvia className rather than usingdisabled. - Icon-only buttons must still carry an accessible name. Pass
aria-label="..."so screen readers can announce the action.
Props
variant "primary" | "outline" | "ghost" Optional
Default "primary" Visual weight of the button. primary is solid and high-emphasis,
outline uses a border, and ghost has no chrome until hover.
color "default" | "success" | "warning" | "danger" | "info" Optional
Default "default" Semantic intent. Combines with variant via compound-variant rules to
produce colored variants (e.g. primary + danger is a solid red
destructive action, ghost + danger is an inline, low-weight one).
size "sm" | "md" | "lg" | "xlg" | "2xlg" Optional
Default "sm" Height of the button. sm is 28px, md 32px, lg 36px, xlg 40px,
2xlg 48px.
loading boolean Optional
Default false When true, shows a spinner, sets aria-busy, pins the button’s width
to prevent reflow, and ignores clicks. Pair with loadingLabel.
loadingLabel string Optional
Default "Loading, please wait" Screen-reader text announced while loading is true. Override to
describe the specific action (“Saving changes”, “Deleting key”).
disabled boolean Optional
Default false Native disabled attribute — suppresses focus, clicks, and any bound
keyboard shortcut. For “disabled with a reason” states, prefer
aria-disabled so the button can still receive focus.
keyboard { display: string; trigger: (e: KeyboardEvent) => boolean; callback: (e: KeyboardEvent) => void } Optional Binds a global keyboard shortcut. display is rendered as a <kbd>
inside the button, trigger evaluates each keydown, callback runs
when it returns true. The handler is registered at the document level.
asChild boolean Optional
Default false Render the button as its single child instead of a <button>, via
Radix’s Slot. Useful for wrapping an <a> while keeping button
styling and preserving browser-native link semantics.
className string Optional Additional Tailwind classes. Merged with variant classes via cn;
later classes win.
...rest ButtonHTMLAttributes<HTMLButtonElement> Optional All standard button attributes (onClick, type, form,
aria-label, etc.) pass through to the underlying <button>.
Related
- Badge — for passive, non-interactive labels.
- Alert — when the context needs its own block of space, not an action.