Skip to content
Nizivo UI

QuantityStepper

A whole-number quantity, such as a product page's or a cart line's, in Input's box between a minus and a plus button. The input is a native number input; the buttons step it for the pointer and are disabled at the ends of the range.

Tier: client

Import

import { QuantityStepper } from '@nizivo/react/quantity-stepper';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/input.css
import '@nizivo/react/css/quantity-stepper.css';

Static or interactive

Client. It always ships its client leaf, which reads its Field through FieldScope, so a page with a QuantityStepper also loads FieldScope. The server HTML is a native number input that works before hydration, arrow keys included; the buttons work once the page has hydrated. How to choose.

In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.

The buttons

The minus and plus buttons are for the pointer. They are out of the tab order, a press never moves focus, and each is named for a screen reader's pointer modes: "Decrease quantity" and "Increase quantity" (labels.decrement and labels.increment), with aria-controls pointing at the input. A keyboard user steps with the arrow keys, which a screen reader announces as the input's value. Each button is disabled at its end of the range, and both while the input is read-only or disabled.

A press steps the input as an arrow key would (stepUp() and stepDown(); an empty input starts at min) and fires the native input and change events, so onChange, a form's own listeners and a Field see an ordinary edit. decrementIcon and incrementIcon replace the internal minus and plus glyphs.

Values

min is 0 and step is 1 unless set; max has no default. value and defaultValue are numbers, and value={null} is an empty input. onValueChange(value, details) reports every change, typed or pressed: the number, or null while the input is empty, with details.reason 'increment', 'decrement' or 'input'.

Quantities are whole numbers. A typed fraction fails the input's step, so inside a Field messages.stepMismatch names the problem; a decimal amount, such as a price, is a NumberField's job.

Width

The input is three characters wide, enough for 999. --nz-quantity-stepper-input-width sets another width, for example 5ch.

Examples

Basic

A product page's quantity: 1 to 10, in a Field. The minus button is disabled at 1.

Up to 10 per order.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { QuantityStepper } from '@nizivo/react/quantity-stepper';

export function QuantityStepperExample(): JSX.Element {
  return (
    <Field name="quantity" label="Quantity" description="Up to 10 per order.">
      <QuantityStepper min={1} max={10} defaultValue={1} />
    </Field>
  );
}

States

Sizes, a cart line at its stock limit, read-only, and disabled.

Only 3 left in stock.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { QuantityStepper } from '@nizivo/react/quantity-stepper';

export function QuantityStepperStatesExample(): JSX.Element {
  return (
    <div style={{ display: 'grid', gap: 16, justifyItems: 'start' }}>
      <QuantityStepper size="sm" aria-label="Small" defaultValue={2} />
      <QuantityStepper aria-label="Medium" defaultValue={2} />
      <QuantityStepper size="lg" aria-label="Large" defaultValue={2} />
      <Field label="Quantity" description="Only 3 left in stock.">
        <QuantityStepper min={1} max={3} defaultValue={3} />
      </Field>
      <QuantityStepper aria-label="Read-only" defaultValue={4} readOnly />
      <Field label="Quantity" disabled>
        <QuantityStepper defaultValue={1} />
      </Field>
    </div>
  );
}

Props

Client component Renders data-slot="quantity-stepper" with the class nz-quantity-stepper. Props: QuantityStepperProps. Source: src/quantity-stepper/quantity-stepper.client.tsx.

NameTypeDefaultDescription
decrementIconReactNode–

The minus button's mark. Defaults to the internal minus glyph.

defaultValuenumber–

Uncontrolled starting value.

incrementIconReactNode–

The plus button's mark. Defaults to the internal plus glyph.

invalidboolean–

An error message is presented for this input. Inside a Field, the Field sets it.

labelsPartial<QuantityStepperLabels>–

Overrides the buttons' names, for example labels={{ increment: 'Erhöhen' }}.

maxnumber–

The highest quantity; the plus button is disabled there. No limit by default.

minnumber0

The lowest quantity; the minus button is disabled there.

onValueChange(value: number | null, details: ChangeDetails<QuantityStepperValueReason>) => void–

Called on every change, typed or pressed: the input's number, or null while it is empty or not a number.

renderRenderProp<QuantityStepperState>–

Composition of the root, the bordered box. className and style go on the root too; every other prop, ref included, goes on the <input>.

sizeControlSize
"sm""md""lg"
'md'

32 / 40 / 48 px, as Input.

stepnumber1

A whole number.

valuenumber | null–

Controlled value; null is an empty input. Pair it with onValueChange or onChange.

  • Also takes every attribute of <input> except children, defaultValue, max, min, prefix, size, step, type, value. (Omit<ComponentPropsWithRef<"input">, "type" | "size" | "prefix" | "children" | "min" | "max" | "step" | "value" | "defaultValue">)

render takes an element, or, in client code, a function of the props and this state (QuantityStepperState):

NameTypeDefaultDescription
atMaxrequiredboolean–

The value is at max: the plus button is disabled.

atMinrequiredboolean–

The value is at min: the minus button is disabled.

disabledrequiredboolean–
invalidrequiredboolean–

An error message is presented (the Field's, or the invalid prop).

readOnlyrequiredboolean–
sizerequiredControlSize
"sm""md""lg"
–

Other exports

QuantityStepperLabels type

import type { QuantityStepperLabels } from '@nizivo/react/quantity-stepper'

QuantityStepper's own strings (§16.4): serialisable, English defaults.

NameTypeDefaultDescription
decrementrequiredstring'Decrease quantity'

The minus button's name.

incrementrequiredstring'Increase quantity'

The plus button's name.

QuantityStepperProps type

import type { QuantityStepperProps } from '@nizivo/react/quantity-stepper'

The props of QuantityStepper, listed above.

QuantityStepperState type

import type { QuantityStepperState } from '@nizivo/react/quantity-stepper'

The render state of QuantityStepper, listed above.

QuantityStepperValueReason type

import type { QuantityStepperValueReason } from '@nizivo/react/quantity-stepper'

'decrement' / 'increment': a button was pressed. 'input': typed, or an arrow key.

type QuantityStepperValueReason = "decrement" | "increment" | "input"

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
quantity-stepper
.nz-quantity-stepper
QuantityStepper
root (takes render); interactive, focus: within
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-invalid (present or absent) ← invalid
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-readonly (present or absent)
    Mirrors the readonly attribute
  • data-size: "sm""md""lg" ← size
    The size prop; values in each component manifest
quantity-stepper-control
.nz-quantity-stepper-control
QuantityStepper
inner element (CSS only)
none
quantity-stepper-decrement
.nz-quantity-stepper-decrement
QuantityStepper
inner element (CSS only)
none
quantity-stepper-icon
.nz-quantity-stepper-icon
QuantityStepper
inner element (CSS only)
none
quantity-stepper-increment
.nz-quantity-stepper-increment
QuantityStepper
inner element (CSS only)
none

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-quantity-stepper-button-fg--nz-icon-primary
  • color on .nz-quantity-stepper-button
--nz-quantity-stepper-input-width3.5rem
  • inline-size on .nz-quantity-stepper-control

Layers: nz.components. Classes: .nz-input, .nz-quantity-stepper, .nz-quantity-stepper-button, .nz-quantity-stepper-control, .nz-quantity-stepper-icon.

Tokens this stylesheet reads (12)

--nz-bg-hover, --nz-bg-pressed, --nz-duration-fast, --nz-easing-standard, --nz-font-label-m, --nz-icon-primary, --nz-size-control-lg, --nz-size-control-md, --nz-size-control-sm, --nz-size-icon-sm, --nz-text-disabled, --nz-tracking-label-m

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • quantity-stepper: draws the outline when the field inside it has focus.

Keyboard

KeysAction
TabMoves focus to the input. The buttons are skipped.
Arrow Up, Arrow DownIn the input, adds or takes away one step, within min and max (the browser's own number input).
EnterIn the input, submits its form.

Source: a native <input type="number"> between two <button type="button" tabIndex={-1}> (quantity-stepper.client.tsx). The browser lane (test/browser/quantity-stepper.spec.tsx) checks the buttons by pointer, the arrow keys, Tab reaching only the input, the events a press fires, controlled use and a Field's message for a typed fraction.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page