Skip to content
View as Markdown

Inputs

QuantityInput

A count a person adjusts by one: items in a cart, guests, seats. Compose it inside a Field for its label, description and error.

Import

TSX
import { Field, QuantityInput, VisuallyHidden } from "@loamui/core";

Usage

Every example has a CSS tab. That’s the real, complete stylesheet for the component: plain, static CSS, with nothing running in the browser.

In a cart line

The input is a native number field with a name, so the form posts the count like any other value. The buttons step it and the row takes the type and height of the controls around it. In a cart line the product name already says what is counted, so the Field's label is hidden: still the control's name, not painted twice.

Seed tray, 40 cells
£12

With bounds

Fewer disables at min and More at max, so the ends are visible before they are reached. A value typed past a bound is not clamped: it is left invalid for the field to report after a submit attempt.

Between 1 and 5.

Step and button names

step is how much one press changes the count, and the arrow keys follow it. Fewer and More are the buttons' accessible names; labels replaces them when the page's own words say more, or when the page is not in English.

Sold in packs of 10.

Disabled

disabled reaches the input and both buttons, so nothing in the row can be pressed or typed into, and the description says why.

Out of stock.

Controlled

Drive it with value and onChange. A press of either button arrives through onChange like a typed change, so the live count can be shown wherever it helps.

Guidance

When to use it

  • For a small whole count the user nudges rather than composes: items in a cart, guests at a table, seats, copies. The usual move is one more or one fewer, and a press is faster than selecting the figure and retyping it.
  • Where the bounds are part of the question (at least one, at most what is in stock): the buttons show the ends by disabling, before an error has to say so.

When not to

  • For a large or arbitrary number the user already knows, such as an account number, a weight or a year. Pressing a button forty times is not entry; use Input with inputMode="numeric".
  • For a value chosen by feel along a visible scale, such as volume or brightness. Use Range, where the position is the point and the exact figure is not.

Accessibility

  • Inside a Field.Root the input reads its id from the field, so Field.Label is a real <label> tied to it; outside one, pass aria-label or aria-labelledby. In development a count with no name at all is reported to the console. The buttons carry their own names, Fewer and More, which labels replaces when the words of the page differ.
  • Renders a native <input type="number"> with inputMode="numeric": arrow keys step the focused value, touch devices raise a number pad, and constraint validation reports a value outside min and max through the Field, announced with aria-invalid after a submit attempt.
  • The buttons are native <button>s with disabled at the bounds and aria-hidden glyphs, so their state and names come from the platform. Focus stays on the button after a press, so repeated presses need no re-navigation.

How it works

The input is the value of record

Nothing is held in React: the count lives in the native input, so a form posts it under its name, a reset restores its default, and value with onChange controls it exactly as they control an Input. The buttons adjust the value of that same input.

The buttons step the native value

Fewer and More call the input's own stepDown() and stepUp() and then fire the input and change events a browser fires for its spinner. Arrow keys still step the focused input, the step prop governs both, and the native spinner is hidden because it would duplicate the buttons in a corner of the box. This is the one place the library reaches for a number input: elsewhere a number is typed, and Input's guidance stands.

Bounds disable and announce

At min, Fewer disables; at max, More disables. A disabled button is announced as unavailable, so a screen-reader user hears the end where a sighted one sees it. A count typed past a bound is deliberately not clamped: silently changing what someone typed hides the mistake, so the input stays invalid and the field reports it after a submit attempt, in words that say the allowed range.

Error messages

Say what happened and how to fix it, in the words of the question itself. See the writing guidance on the Field page.

SituationMessage
The count is out of range[Label] must be between [min] and [max]
The field is emptyEnter [how many of whatever the label asks for]

Props

Status is not a prop: it comes from the surrounding --loam-context region. See the Contextualism guide.

PropTypeDefaultDescription
minnumber0The smallest count allowed; Fewer disables here.
maxnumber—The largest count allowed; More disables here.
stepnumber1How much one press changes the count.
defaultValuenumber—Initial count for uncontrolled usage.
valuenumber—The count, when controlled; pair it with onChange.
labels{ decrement?: string; increment?: string }{ decrement: "Fewer", increment: "More" }The buttons' accessible names, for another language or the page's own words.
wrapperPropsPartProps<"div">—Props for the row that holds the buttons. className, style, ref and every other prop land on the <input> itself.
...othersInputHTMLAttributes—All native <input type="number"> props, and ref, are forwarded to the <input>, including name, disabled, required and onChange.