Components

QuantityPicker

A stepper with clamping, per-button loading, a remove mode and a width that does not move.

A quantity stepper driven by seed-design's headless engine, so the clamping, the step arithmetic, the per-button loading state and the removable mode are theirs by construction.

The look is Attio's number field (Deals > New Deal > Deal value): a 32px, 9px-radius field on the same ring Input uses, the value left-aligned, and a vertical pair of 12x10 chevron buttons at the right edge, increase above decrease. size is kept for compatibility and ignored — Attio's field has one height.

Available since@crisp-ui-kit/crisp@0.11.0

Installation

npm install @crisp-ui-kit/crisp

Props

Prop

Type

The width does not move

The value is a grid with a hidden copy of max stacked in the same cell as the number. Without it the control gets wider at 10 and wider again at 100, and a column of them stops lining up. The hidden copy runs through getValueText too, so a formatted value reserves the width of its formatting, and it is aria-hidden so the value is announced once rather than twice.

Direction

dir must be passed, not inherited. The engine writes dir on the root from its own prop, which defaults to "ltr", so a picker inside an RTL app renders left-to-right unless you pass dir="rtl". Here it also decides the ORDER of the value and the stepper — the engine reverses its own children rather than relying on CSS — so an inherited direction would leave the stepper on the wrong side.

Usage

import { QuantityPicker } from "@crisp-ui-kit/crisp";

const [qty, setQty] = React.useState(1);

<QuantityPicker min={1} max={99} value={qty} onValueChange={setQty} />;

Pass name to render a hidden input, so the value submits with a surrounding form.

Examples

Sizes (all render 32px)

Remove mode

With removable, pressing minus at the minimum calls onRemove instead of doing nothing, and the decrease chevron becomes a bin so the button says what it now does. This is the cart-line shape: the last decrement takes the line out.

removeAriaLabel is required when removable is set — the type enforces it, because that label is the only accessible name the button has in this mode.

Loading

loading can be true for both buttons or { increment } / { decrement } for one. The loading button shows a Spinner, reports aria-busy and refuses to step, while its neighbour keeps working.

On this page