{"slug":"stepper","name":"Stepper","packageName":"@liquidify/react","category":"controls","maturity":"experimental","summary":"A user-tappable control for incrementing or decrementing a numeric value by a fixed step — the canonical glass stepper.","importPath":"@liquidify/react/stepper","sourcePath":"packages/@liquidify/react/src/components/stepper/stepper.tsx","specPath":"packages/@liquidify/react/src/components/stepper/spec.md","docsPath":"../liquidify-page/content/component-source/stepper.md","documentationStatus":"draft","storyIds":["controls-stepper--default"],"additionalExports":["ControlProps","StepperCallbackProps","StepperValueProps"],"anatomy":[],"props":[{"name":"children","type":"ReactNode","required":false,"description":"Optional visible label content, rendered next to the capsule in a row — the SwiftUI `label:` closure / `init(_ titleKey:)` analogue (`<Stepper aria-label=\"Quantity\">Quantity</Stepper>`). The accessible name is **not** sourced from here; supply `aria-label` / `aria-labelledby` for the screen-reader name (the segment buttons derive theirs from it). children role: `label`.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"className","type":"string | undefined","required":false,"description":"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.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"defaultValue","type":"number | undefined","required":false,"description":"Uncontrolled seed for the internal value (ignored once {@link StepperValueProps.value} is supplied). Clamped into `[min, max]`. Absent in callback mode — no value binding exists.","defaultValue":"`min` (when bounded), else `0`","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"disabled","type":"boolean | undefined","required":false,"description":"When `true`, both segments are inert; `aria-disabled` + `data-disabled` on the root; `disabled` on each segment button; `tabIndex={-1}` on the root.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"labelProps","type":"LabelProps | undefined","required":false,"description":"Per-instance typography override for the visible label, forwarded to the internal {@link Text} (ADR-0028). Token-backed axes only (`variant`, `weight`, `color`, `tint`, `tracking`, …); the HIG default is `variant=\"body\"` / `color=\"primary\"`. Applies only when {@link StepperCommonProps.children} is a string/number — a label element the consumer passes is rendered as-is.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"max","type":"number | undefined","required":false,"description":"Upper bound of the value range. Absent in callback mode — there is no range to bound.","defaultValue":"`undefined` (unbounded)","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"min","type":"number | undefined","required":false,"description":"Lower bound of the value range. Absent in callback mode — there is no range to bound.","defaultValue":"`undefined` (unbounded)","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"onChange","type":"((next: number) => void) | undefined","required":false,"description":"Fires with the next clamped value whenever the value changes. Called in both controlled and uncontrolled modes. Absent in callback mode — no value binding exists.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"onDecrement","type":"(() => void) | undefined","required":false,"description":"See {@link StepperValueProps.onIncrement}. Fires on each `−` press.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"onEditingChanged","type":"((editing: boolean) => void) | undefined","required":false,"description":"Fires `true` on segment `pointerdown`, `false` on `pointerup` / `pointercancel` — the SwiftUI `onEditingChanged` analogue.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"onIncrement","type":"(() => void) | undefined","required":false,"description":"Not on the value branch — callback-only mode is mutually exclusive with a value binding (charter §6). Fires on each `+` press.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"ref","type":"Ref<HTMLDivElement> | undefined","required":false,"description":"Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"step","type":"number | undefined","required":false,"description":"Increment / decrement magnitude. A zero, `NaN`, non-finite, or negative `step` coerces to the default `1` (a negative `step` no longer reverses direction). Absent in callback mode — there is no value to step.","defaultValue":"`1`","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"},{"name":"value","type":"number | undefined","required":false,"description":"Controlled value. Passing this switches Stepper to controlled mode: the rendered value always reflects this (clamped) and the consumer owns it via {@link StepperValueProps.onChange}. Absent in callback mode — no value binding exists.","declaredIn":"packages/@liquidify/react/src/components/stepper/stepper.tsx"}],"swiftUIMap":[{"swiftUI":"`value: Binding<V>`","react":"`value` (controlled) / `defaultValue` (uncontrolled seed)","notes":"The controlled-numeric triad (ADR-0015), on the `StepperValueProps` arm. A `Binding` splits into controlled `value` + `onChange`, or uncontrolled `defaultValue`. Presence selects the value arm — the default when neither `onIncrement` nor `onDecrement` is supplied.","relationship":"direct"},{"swiftUI":"`in: ClosedRange<V>`","react":"`min` / `max`","notes":"Inclusive range bounds, `StepperValueProps` only. Demo uses `0...10`, `0...5`. Omitting both defaults to unbounded (`-Infinity` / `Infinity`).","relationship":"direct"},{"swiftUI":"`step: V.Stride`","react":"`step`","notes":"Increment/decrement magnitude, `StepperValueProps` only. SwiftUI default `1`; demo never sets it explicitly. React default `1` — a zero, `NaN`, non-finite, or **negative** `step` also coerces to `1` (a negative step no longer reverses direction).","relationship":"direct"},{"swiftUI":"`init(_ titleKey:)` / `label: () -> Label`","react":"`children` (ReactNode / string)","notes":"Leading visible label content rendered next to the capsule **through the `Text` primitive** (default `variant=\"body\"` / `color=\"primary\"`) when `children` is a string/number; a label *element* the consumer passes is rendered as-is (no double-wrap, ADR-0028). The accessible name is sourced from `aria-label` / `aria-labelledby`, not `children`. Demo: `\"Quantity: \\(qty)\"`, `Text(\"Bounded 0–10\")`, `\"Custom actions\"`.","relationship":"direct"},{"swiftUI":"`.font(_:)` / `.foregroundStyle(_:)` on the label","react":"`labelProps` (`Pick<TextProps, …>`)","notes":"Per-instance typography override forwarded to the internal `Text` (token-backed axes only — `variant`/`weight`/`color`/`tint`/`tracking`/…; ADR-0028). Spread last, so an explicit value wins over the `body`/`primary` default. Applies only to string/number `children`.","relationship":"direct"},{"swiftUI":"`onIncrement: (() -> Void)?`","react":"`onIncrement?`","notes":"Callback mode (`StepperCallbackProps`), the discriminant. Fires on each `+` press.","relationship":"direct"},{"swiftUI":"`onDecrement: (() -> Void)?`","react":"`onDecrement?`","notes":"Callback mode (`StepperCallbackProps`), the discriminant. Fires on each `−` press.","relationship":"direct"},{"swiftUI":"`onEditingChanged: (Bool) -> Void`","react":"`onEditingChanged?(editing: boolean)`","notes":"Fires `true` on segment press-start, `false` on release. Optional; shared by both arms; mirrors Slider's adoption of this pattern.","relationship":"direct"},{"swiftUI":"`.disabled(true)`","react":"`disabled` (from `ControlProps`)","notes":"Whole-control dim; both segments inert. Shared by both arms.","relationship":"direct"},{"swiftUI":"`format:` (FormatStyle variants)","react":"(out of scope v1)","notes":"Deferred number-formatting forms.","relationship":"direct"},{"swiftUI":"`.tint(_:)`","react":"(not present)","notes":"No tint axis for Stepper (demo renders no accent).","relationship":"direct"}],"tokens":["--lq-color-accent","--lq-color-bg-primary","--lq-color-label-primary","--lq-color-label-secondary","--lq-color-label-tertiary","--lq-color-separator","--lq-glass-","--lq-hit-target-touch","--lq-opacity-faint","--lq-opacity-muted","--lq-radius-pill","--lq-space-","--lq-stepper-capsule-h","--lq-stepper-disabled-opacity","--lq-stepper-divider-w","--lq-stepper-pad","--lq-stepper-segment-w"],"keyboard":[{"key":"Keyboard","action":"Keyboard (WAI-ARIA APG spinbutton pattern, ADR-0017): on the role=\"spinbutton\" root, ↑ increments by step; ↓ decrements by step; Home sets to the COERCED-FINITE lower bound (if bounded — a no-op when min is undefined, NaN, or non-finite); End sets to the coerced-finite upper bound (if bounded, same no-op rule); PageUp increments by step × 10 (the macOS Shift-click quick-change analogue); PageDown decrements by step × 10. Each key press preventDefaults, clamps the result, fires onChange, and updates aria-valuenow. Home/End resolving against the coerced-finite bound (rather than the raw min/max prop) preserves the invariant that every resolved value is finite — an unbounded axis's Home/End never jumps to ±Infinity."}],"variants":[],"variantPages":[]}