Button
A button with solid, outline, ghost, and soft variants, plus intent colors, sizes, a loading state, and icon slots.
Import
import { Button } from "@hyzer-labs/ui"Demo
Four looks for one control. solid (the default) fills with the intent
color, outline and ghost lighten the footprint, and soft tints the intent into the surface. Each row shows one variant across a few intents.
<!-- solid (default) -->
<Button>primary</Button>
<Button intent="secondary">secondary</Button>
<Button intent="danger">danger</Button>
<!-- outline -->
<Button variant="outline">primary</Button>
<Button variant="outline" intent="secondary">secondary</Button>
<Button variant="outline" intent="danger">danger</Button>
<!-- ghost -->
<Button variant="ghost">primary</Button>
<Button variant="ghost" intent="secondary">secondary</Button>
<Button variant="ghost" intent="danger">danger</Button>
<!-- soft -->
<Button variant="soft">primary</Button>
<Button variant="soft" intent="secondary">secondary</Button>
<Button variant="soft" intent="danger">danger</Button>Button spans the full intent vocabulary,
not a hand-picked subset. primary is the default, because a button is a call
to action.
<!-- primary (default) -->
<Button>solid</Button>
<Button variant="outline">outline</Button>
<Button variant="ghost">ghost</Button>
<Button variant="soft">soft</Button>
<!-- neutral -->
<Button intent="neutral">solid</Button>
<Button variant="outline" intent="neutral">outline</Button>
<Button variant="ghost" intent="neutral">ghost</Button>
<Button variant="soft" intent="neutral">soft</Button>
<!-- secondary -->
<Button intent="secondary">solid</Button>
<Button variant="outline" intent="secondary">outline</Button>
<Button variant="ghost" intent="secondary">ghost</Button>
<Button variant="soft" intent="secondary">soft</Button>
<!-- danger -->
<Button intent="danger">solid</Button>
<Button variant="outline" intent="danger">outline</Button>
<Button variant="ghost" intent="danger">ghost</Button>
<Button variant="soft" intent="danger">soft</Button>
<!-- warning -->
<Button intent="warning">solid</Button>
<Button variant="outline" intent="warning">outline</Button>
<Button variant="ghost" intent="warning">ghost</Button>
<Button variant="soft" intent="warning">soft</Button>
<!-- success -->
<Button intent="success">solid</Button>
<Button variant="outline" intent="success">outline</Button>
<Button variant="ghost" intent="success">ghost</Button>
<Button variant="soft" intent="success">soft</Button>
<!-- info -->
<Button intent="info">solid</Button>
<Button variant="outline" intent="info">outline</Button>
<Button variant="ghost" intent="info">ghost</Button>
<Button variant="soft" intent="info">soft</Button>Every size, shown across all four variants. size="full" fills its container
at the md height/padding, and it cannot be combined with sm/lg.
<!-- size="sm" -->
<Button size="sm">solid</Button>
<Button size="sm" variant="outline">outline</Button>
<Button size="sm" variant="ghost">ghost</Button>
<Button size="sm" variant="soft">soft</Button>
<!-- size="md" (default) -->
<Button>solid</Button>
<Button variant="outline">outline</Button>
<Button variant="ghost">ghost</Button>
<Button variant="soft">soft</Button>
<!-- size="lg" -->
<Button size="lg">solid</Button>
<Button size="lg" variant="outline">outline</Button>
<Button size="lg" variant="ghost">ghost</Button>
<Button size="lg" variant="soft">soft</Button>
<!-- size="full": fills its container at the md height/padding -->
<Button size="full">full</Button>While loading, the button sets aria-busy="true" and renders a
visually hidden loadingLabel (default "Loading") for screen
readers. The two loading buttons below look identical but announce differently.
Customize the label to describe the in-flight action, e.g. loadingLabel="Saving changes…".
<Button loading>Save</Button>
<!-- loadingLabel is announced to screen readers, not shown visually -->
<Button loading loadingLabel="Saving changes…">Save</Button>
<Button disabled>Disabled</Button><Button>
{#snippet iconEnd()}<IconArrowRight />{/snippet}
Continue
</Button>
<Button variant="outline">
{#snippet iconStart()}<IconSearch />{/snippet}
Search
</Button>
<!-- Icon-only: ariaLabel is required -->
<Button ariaLabel="Search" variant="ghost">
{#snippet iconStart()}<IconSearch />{/snippet}
</Button>Button vs Link
Link. Want it to look like a button,
including one that navigates? Use Button, adding href when it should navigate.href changes the element, not the look. It renders a real <a> that navigates, styled as whatever variant you choose. It stays a
plain semantic anchor with no role="button", because it genuinely does
navigate.
<Button href="/pricing">Link button (renders as <a>)</Button>Props
| Name | Type | Default | Note |
|---|---|---|---|
variant | 'solid' | 'outline' | 'ghost' | 'soft' | 'solid' | |
intent | Intent | 'primary' | The full intent registry, not a hand-picked subset — see Foundation → Colors & Intent. |
size | 'sm' | 'md' | 'lg' | 'full' | 'md' | full fills its container at the md height/padding; not combinable with sm/lg. |
disabled | boolean | false | |
loading | boolean | false | |
loadingLabel | string | 'Loading' | Screen-reader-only text announced while loading; not rendered visually. |
href | string | — | Renders as a plain <a> (no role="button") when set — it genuinely navigates. |
type | 'button' | 'submit' | 'reset' | 'button' | |
ariaLabel | string | — | Required for icon-only buttons. |
onclick | (e: MouseEvent) => void | — | Swallowed while disabled or loading. |
class | string | — | Merged after the hz-button class. |
children | Snippet | — | |
iconStart | Snippet | — | An icon snippet with no children renders the compact circular icon-only form. |
iconEnd | Snippet | — |
Theme hooks
What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.
Root class: .hz-button
Data attributes
| Hook | Values | Styles |
|---|---|---|
data-variant | 'solid' | 'outline' | 'ghost' | 'soft' | Default solid. |
data-intent | 'neutral' | any registered intent | The full vocabulary, not a hand-picked subset — register an intent and Button stamps it. Sets --hz-button-accent. |
data-size | 'sm' | 'md' | 'lg' | 'full' | Default md. full fills its container at the md height/padding — not combinable with sm/lg. |
data-state | 'disabled' | 'loading' | absent | Absent when interactive — every hover and active rule is gated on :not([data-state]), so styling a state cannot be undone by a hover. disabled wins over loading. |
data-icon-only | present when icon-only | Derived: an icon with no children. Renders the circle form. |
Custom properties
| Hook | Values | Styles |
|---|---|---|
--hz-button-accent | <color> — default var(--hz-intent-primary) | The accent every variant derives from. Override it to restyle solid, outline, ghost, and soft at once — and it is how you wire up an intent you registered yourself. |
--hz-button-on-accent | <color> — default var(--hz-color-surface) | Text colour on a solid fill. The surface role rather than white, so it flips with the mode and keeps solid text ≥ 4.5:1 in both. |
--hz-button-tint | <percentage> — 14% light, 28% dark | Soft-variant background tint strength — the same recipe and hook pattern as --hz-badge-tint. |
Accessibility
Use ariaLabel for icon-only buttons (no visible text). The loading state sets aria-busy="true" and renders a screen-reader-only "Loading" label. Disabled state sets aria-disabled="true".
References: APG Button pattern · MDN: <button>