New Components
Quantity spin button
The quantity spin button allows a numeric value to be increased or decreased using two buttons framing the current value. It is used to adjust a number of items, participants, days or tickets. The value is bounded by a minimum and a maximum, and remains editable directly from the keyboard. This component can be built in two ways, with a native number field or with an ARIA pattern, whose trade-offs differ.
When to use a quantity spin button
- Small bounded quantities: a number of items, travellers or licences, between a known minimum and maximum.
- Frequent adjustment: when the user nudges the value up or down rather than typing a precise figure.
- Whole numbers: the step is 1 and decimals make no sense.
When not to use one
- A wide range of values: beyond a score of steps, typing is faster than hammering a button.
- A non-numeric value: a size or a category belongs in a dropdown list or in radio buttons.
- An approximate value: for a continuous setting with no exact figure, a slider fits better.
Modes of action
- Mouse: the minus and plus buttons change the value by one step in both models; the field, where there is one, accepts direct entry.
- Keyboard: this is precisely where the two models differ. In the first, the keyboard goes through the field and through it alone. In the second, it goes through the two buttons, and there is no field.
- Screen reader: the value and the bounds are carried by the native field in the first model, by the displayed text and the live region in the second.
Content
- A visible label: it says what is being counted. "Quantity" alone is not enough when the page holds several.
- The bounds, announced: the minimum and the maximum belong in the hint, before entry, not only in the error message.
- A useful error message: it states the rule and the expected value, never "invalid value".
Keywords
- Quantity spin button: a control that moves a numeric value one step at a time, using two buttons.
- spinbutton: the role of a control holding a bounded numeric value. The native field already carries it, and it alone.
- tabindex="-1": takes an element out of the tab order without making it unusable with the mouse.
- disabled: makes an element inoperative and removes it from the tab order and from the accessibility tree.
- aria-disabled: reports an element unavailable without taking it out of the tab order. It keeps focus, and only goes to disabled once that focus has left.
Breakdown of the components
Two models, and it is exactly the keyboard path that tells them apart. The first puts the setting in the field, the second in the buttons. Neither exposes both at once: that would impose three tab stops for one single setting.
Model 1: the field is the keyboard path
- The field is an input type="number". It brings the spinbutton role, the current value, the bounds, arrow up and down, Home and End, and direct entry, without a line of code.
- The minus and plus buttons carry tabindex="-1": they are pointer commands duplicating the arrows. The function stays fully keyboard operable through the field (WCAG 2.1.1).
- At the bounds, the button concerned becomes unavailable. If it holds focus at that moment it keeps it: it first goes to aria-disabled, which reports it unavailable without taking it out of the tab sequence, then to disabled only once the user has left it. Without that, focus would drop onto the document mid-action (WCAG 2.4.3).
- The error is announced as soon as it appears through an assertive region, and bound to the field with aria-describedby so it stays available afterwards (WCAG 3.3.1).
Model 2: the buttons are the keyboard path
- There is no field. The two buttons are in the tab order and carry the setting on their own.
- No replacement role is added: a focusable container in role="spinbutton" would promise arrows that do not exist, and would add a third tab stop between the two buttons.
- The displayed value serves as the description of both buttons through aria-describedby, and its text carries its unit: a bare "3" means nothing.
- At the bounds the button becomes unavailable. Since the buttons are the only keyboard path here, it keeps focus: it goes to aria-disabled, and only on the next Tab or Shift+Tab, once the user has left, does it go to disabled and leave the tab sequence (WCAG 2.4.3).
Screen reader
- With a native field, no ARIA role is added: doubling it would produce a contradictory announcement.
- The buttons carry explicit names, "Decrease the quantity" and "Increase the quantity", never a bare minus or plus sign.
- The bound is announced the moment it is reached: once the button is disabled, it can no longer explain it.
- The new value is announced after a 500 ms delay, long enough for the screen reader to finish announcing the button.
The two models do not share the same keyboard path, and that is exactly what tells them apart. In the first, the field is the only tab stop: the buttons carry tabindex="-1" and duplicate the arrows for the pointer. In the second there is no field and the two buttons are the only stops. Neither exposes all three at once: that would be three tab stops for a single setting.
| Key | Function |
|---|---|
| Tab | Model with a field: a single stop, the field. Model without a field: two stops, the Decrease button then the Increase button. A button that has just reached a bound keeps focus until this tab press, and only leaves the sequence afterward. |
| Arrow up | Model with a field only: increases the value by one step, without going past the maximum. |
| Arrow down | Model with a field only: decreases the value by one step, without going below the minimum. |
| Home | Model with a field only: takes the value to its minimum. |
| End | Model with a field only: takes the value to its maximum. |
| Enter or Space | Model without a field: activates the focused button. A button that has reached its bound does nothing, the bound having already been announced the moment it was reached. |
| Editing keys | Direct entry in the field: digits, cursor movement, deletion. The variant without a field does not accept direct entry. |
Note: with the native numeric field, the arrows, Home, End and the bounds are handled by the browser, without a line of code. The model without a field offers none of those shortcuts: that is the price, and the reason to keep it for small ranges.
