Skip to content

QuantityStepper

Prop-driven quantity stepper: a typeable number field between two step buttons, clamped to [min, max] and stepping by step. A typed value is reported on Enter or blur, snapped to the step; a step at once. With onRemove, the decrement at the minimum is a remove button.

import { QuantityStepper } from "@evolve-storefront/ui/quantity-stepper";
Design notes

The three labels are required, with no English default. The buttons are icon-only and the field has no visible label, so each name is the whole accessible name, and an omitted one is a control announced in the wrong language.

Preview

DefaultOpen in Storybook ↗

Usage

<QuantityStepper
value={line.quantity}
onValueChange={(quantity) => updateLine({ id: line.id, quantity })}
min={1}
label={t("quantityFor", { name: line.name })}
decrementLabel={t("decreaseQuantityFor", { name: line.name })}
incrementLabel={t("increaseQuantityFor", { name: line.name })}
/>;

Props

PropTypeDescription
valuenumber
onValueChange(value: number) => void
labelstring

Accessible name of the quantity field.

decrementLabelstring

Accessible label for the decrement button.

incrementLabelstring

Accessible label for the increment button.

min?number
max?number
step?number

The pack size: steps go by it, and a typed value snaps to a multiple of it, with min and max rounded onto multiples too. Defaults to 1.

onRemove?(() => void)

At the minimum, the decrement becomes a remove button that calls this, such as removing a cart line. Without it the decrement is disabled there.

removeLabel?string

Accessible label for the remove button; required with onRemove.

disabled?boolean

Locks the value, e.g. while the previous change saves: the buttons disable and the field turns read-only but keeps focus, so a pending save cannot blur it.

Any other props go to the root element, like on a plain HTML element.

Examples

Pack SizeA pack size of 6: the buttons step by 6, a typed quantity snaps to the nearest pack, and the minimum of 1 becomes one pack.
// Sold per 6: steps and typed quantities keep to multiples of 6.
<QuantityStepper value={quantity} onValueChange={setQuantity} min={1} step={6} {...labels} />;
Open in Storybook ↗
Remove At MinimumWith onRemove, the decrement at the minimum is a bin that removes the line, so the last step down does what the shopper meant instead of doing nothing.
<QuantityStepper
value={line.quantity}
onValueChange={setQuantity}
min={1}
onRemove={() => remove(line.id)}
removeLabel={t("removeProduct", { name: line.name })}
{...labels}
/>;
Open in Storybook ↗
Locked While SavingLocked while the change saves, as a cart line does, and then left: the step is reported once.
<QuantityStepper
value={line.quantity}
onValueChange={(quantity) => updateLine.mutate({ id: line.id, quantity })}
disabled={updateLine.isPending}
{...labels}
/>;
Open in Storybook ↗