# Button

**Maturity:** experimental

A user-invokable control that performs an immediate action.

## Import

```tsx
import { Button } from "@liquidify/react/button"
```

## Props

- `aria-label`: `string | undefined`. Accessible name — **REQUIRED** for an icon-only button (no visible text). Sets the native `aria-label` attribute on the rendered element. Alternative to `aria-labelledby` for labelling buttons that have no visible text.
- `aria-labelledby`: `string | undefined`. Accessible name by reference — alternative to `aria-label`. Sets the native `aria-labelledby` attribute on the rendered element. Points to the `id` of another element that describes the button. Useful when the label is already in the DOM and should be reused.
- `asChild`: `false | undefined`; default ``false``. Discriminant (charter §4 rule 3): `false` or omitted renders the native `<button>` arm ({@link ButtonProps}). Set `true` to switch to the {@link ButtonAsChildProps} arm instead — merges the glass surface onto the single element child (D4 / Slot composition) rather than rendering its own root, so a Button can become a link or wrap another library's element while keeping the glass look.
- `children`: `ReactNode`. The visible label and accessible name for the button. Optional. Single line of text (or icon + text via `leadingIcon` / `trailingIcon` slots). children role: `label` (charter §8) — token parity (see spec.md § Tokens); Button exposes no `labelProps` seam, unlike most `TextLabel`-backed components (ADR-0028 rule 3). When {@link SlottableProps.asChildasChild} is set, this must be the single element to merge the glass surface onto (e.g. an `<a>`), not a text node.
- `className`: `string | undefined`. Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.
- `disabled`: `boolean | undefined`. When `true`, sets the native `disabled` attribute on the `<button>` element. The button is removed from the tab order and all interaction (`onClick` / `onPress`) is suppressed. `aria-disabled` is NOT set — the native attribute provides full-suppression semantics per the WAI-ARIA Button pattern (Shape N, `docs/api-conventions.md` §3; see spec.md § Accessibility). With {@link SlottableProps.asChildasChild} the native attribute is unavailable on a non-form child (e.g. an `<a>`), so Button hard-suppresses instead (Shape A, P0.B4): activation is cancelled (`preventDefault`, no navigation, no `onClick` / `onPress` — the Slot's `defaultPrevented` cancellation also silences the child's own handler) and the root carries `aria-disabled="true"`, STAYING focusable. In every mode the root is stamped with `data-disabled` for the CSS fallback (`.lq-button[data-disabled]`).
- `leadingIcon`: `ReactNode`. Optional leading glyph rendered before the label (e.g. an `<Icon>`). Pairs with `trailingIcon` to frame the label on left and right. When present alongside no visible text (icon-only mode), an accessible name **must** be supplied via {@link ButtonProps. "aria-label" | aria-label} or {@link ButtonProps. "aria-labelledby" | aria-labelledby} (or an associated `<label>`). An icon-only button with no accessible name is not perceivable to screen-reader users and violates WCAG 2.1 SC 1.1.1 (ADR-0019).
- `loading`: `boolean | undefined`. When `true`, the button is in a loading state: `aria-busy="true"` is set, a spinner indicator is rendered, and `onClick` / `onPress` are suppressed. The button remains focusable while loading (unlike `disabled`).
- `onClick`: `((event: MouseEvent<HTMLButtonElement>) => void) | undefined`. Low-level native passthrough; prefer `onPress`. Fires FIRST, before `onPress`. Suppressed automatically when `disabled` or `loading` is `true`; consumers do not need to guard against those states themselves. Typed against the native `<button>` element (`asChild` false/omitted — see {@link ButtonAsChildProps.onClick} for the `asChild` arm, whose event stays polymorphic).
- `onPress`: `(() => void) | undefined`. Primary activation callback — SwiftUI `Button(action:)`. Payload-free and semantic: fires at most once per interaction (`docs/api-conventions.md` §1 rule 1). Suppressed automatically when `disabled` or `loading` is `true` — a suppressed interaction emits nothing. Fires AFTER {@link ButtonProps.onClickonClick}; if `onClick` calls `event.preventDefault()`, `onPress` is suppressed for that interaction — and so is the internal glass `data-pop` stamp on the `glass` / `glassProminent` variants (§1 rule 3).
- `ref`: `Ref<HTMLButtonElement> | undefined`. Forwarded ref to the native `<button>` — the charter-documented ref target for Button's native arm (`docs/api-conventions.md` §4).
- `role`: `ButtonRole | undefined`. The button's {@link ButtonRolerole} — its purpose, **orthogonal** to {@link ButtonProps.variantvariant} (SwiftUI `Button(role:)`). Surfaced as `data-role` (a styling hook only — never the DOM `role` attribute, which would clobber the implicit `button` role). Omit for a neutral button. - `destructive` — irreversible / data-loss action; red label (red fill on the prominent variants). - `cancel` — dismisses without committing; rendered like a neutral button.
- `size`: `ButtonSize | undefined`; default ``"regular"``. Size of the button — the `small · regular · large` subset of the canonical {@link SizedControlProps.sizesize scale}. Resolved to geometry via `data-size` attribute selectors in `styles.css`, which compose min-height and padding from the shared `--lq-space-*` scale and the label from `--lq-font-size-*` (see `docs/components/button/token-vocabulary.md`).
- `tint`: `LiquidTintValue | undefined`. System-accent of the control — drawn from the RESTRICTED {@link LIQUID_COLORS} palette (the thirteen Apple system colours). **Orthogonal to a component's `variant`**: it recolours the accent the surface draws from (focus ring, tinted fill, selection) without changing its variant. Resolved via `data-tint` attribute selectors in CSS (tokens-only, ADR-0003).
- `trailingIcon`: `ReactNode`. Optional trailing glyph rendered after the label. Pairs with `leadingIcon` to frame the label on left and right. When present alongside no visible text (icon-only mode), an accessible name **must** be supplied via {@link ButtonProps. "aria-label" | aria-label} or {@link ButtonProps. "aria-labelledby" | aria-labelledby} (or an associated `<label>`). An icon-only button with no accessible name is not perceivable to screen-reader users and violates WCAG 2.1 SC 1.1.1 (ADR-0019).
- `variant`: `ButtonVariant | undefined`; default ``"automatic"``. Visual variant of the button surface — the SwiftUI `buttonStyle`, surfaced as the `data-variant` attribute and resolved to chrome + label colour in CSS. - `automatic` — context default; text-only, system-blue label, no capsule. - `plain` — text-only, no capsule; label is the **default label colour** (black in light / white in dark). - `bordered` — frosted Liquid-Glass capsule that refracts the backdrop; system-blue label. - `borderedProminent` — solid systemBlue fill, white label. - `borderless` — text-only, no capsule; system-blue label. - `glass` — live Liquid-Glass capsule refracting the backdrop; blue label. - `glassProminent` — solid tint fill with a prominent Liquid-Glass specular edge. - `translucent` — no SwiftUI `buttonStyle` analogue (see spec.md § Variants); a tint-accent capsule mixed toward transparent so the engine glass refracts through it, with a matching accent label.

## SwiftUI mapping

- ``Button(action:) { label }`` → ``children`` (direct): children role: `label`, token parity (see Tokens). `action` is payload-free, mirrored by `onPress` below.
- ``action: () -> Void`` → ``onPress`` (direct): The primary activation convention (charter §1) — payload-free, fires at most once per interaction.
- `*(no SwiftUI analogue — DOM passthrough)*` → ``onClick`` (web-only): Low-level native passthrough; prefer `onPress`. Fires first; `event.preventDefault()` here suppresses `onPress` (and, in `asChild` mode, the child's own handler via Slot cancellation, P0.B4).
- ``.buttonStyle(_:)`` → ``variant`` (direct): `ButtonVariant`, 1:1 with Apple's `ButtonStyle` values; `translucent` is the one style with no direct Apple analogue (see above).
- ``Button(role:)`` → ``role`` (direct): Orthogonal to `variant`; recolours the label (and prominent fill) only.
- ``.controlSize(_:)`` → ``size`` (direct): `small | regular | large` — the canonical 3-step button-shaped-control subset (charter §7).
- ``.tint(_:)`` → ``tint`` (direct): Restricted `LiquidColor` (13 system colours); orthogonal to `variant`.
- ``.disabled(_:)`` → ``disabled`` (direct): Shape N (native `<button disabled>`, no `aria-disabled`) by default; Shape A (`aria-disabled` + `data-disabled` + hard suppression, stays focusable) when `asChild` (charter §3).
- `*(web-only, no SwiftUI analogue)*` → ``loading`` (web-only): `aria-busy="true"` + spinner; suppresses activation like `disabled` but stays focusable.
- `*(web-only, no SwiftUI analogue)*` → ``className`` (web-only): Inherited surface axis (`LiquidBaseProps`) — merges into the glass class.
- `*(web-only — D4 Slot composition)*` → ``asChild`` (web-only): Discriminant: `false`/omitted renders `<button>`; `true` merges the surface onto the single element child instead (e.g. an `<a>`).
- `*(no SwiftUI analogue — refs)*` → ``ref`` (web-only): Typed to the charter-declared target: `Ref<HTMLButtonElement>` when `asChild` is `false`/omitted (native `<button>`); the child's own element type via Slot merge when `asChild` is `true` (charter §4).
- ``Label(_:systemImage:)`` → ``leadingIcon` / `trailingIcon`` (direct): Icon placement either side of the label; icon-only mode requires `aria-label`/`aria-labelledby` (ADR-0019).
- `*(web-only, accessibility)*` → ``aria-label` / `aria-labelledby`` (web-only): Accessible-name overrides for icon-only Buttons.
