Spinbutton
A number field that steps through a range, following the
ARIA spinbutton pattern.
The field carries role="spinbutton" and the value. It takes typing and the
arrow keys, and it is the only part that takes focus.
import {
Spinbutton,
SpinbuttonDecrement,
SpinbuttonIncrement,
SpinbuttonInput,
SpinbuttonLabel,
SpinbuttonStateChild,
useSpinbuttonState,
} from 'terracotta/spinbutton';Anatomy
<Spinbutton> {/* owns the value, limits and step */}
<SpinbuttonLabel/> {/* optional; names the field */}
<SpinbuttonDecrement/> {/* optional; out of the tab sequence */}
<SpinbuttonInput/> {/* role="spinbutton", the only tab stop */}
<SpinbuttonIncrement/> {/* optional; out of the tab sequence */}
</Spinbutton>Examples
Basic
Typing
The field is a normal text field, so editing keys work as the browser provides them.
- Text that is a value in range and on a step sets the value as it is typed.
- Text on its way to a value, such as
1.or-, waits. - Text that is not a number, or is out of range, sets
aria-invalid="true". - On blur and Enter, the text becomes the value: snapped to the step and kept in range. Text that is not a number is dropped, and the field shows the last value again.
Buttons
SpinbuttonIncrement and SpinbuttonDecrement are optional. They are out of
the tab sequence, since the arrow keys do the same, and pressing one leaves
focus in the field. Each is disabled at its limit. Give each a name:
<SpinbuttonIncrement aria-label="More adults">+</SpinbuttonIncrement>Without limits
Leave out min or max for an open range. aria-valuemin and
aria-valuemax are only set for the limits that exist. Without a limit,
Home and End move the caret as usual.
Formatting
formatValue sets the text shown in the field, and parseValue reads it
back. getValueText sets aria-valuetext when the number alone is not clear.
<Spinbutton
defaultValue={5}
min={0}
formatValue={value => `${value} kg`}
parseValue={text => Number.parseFloat(text)}
getValueText={value => `${value} kilograms`}
>
<SpinbuttonInput aria-label="Weight" />
</Spinbutton>Controlled
const [adults, setAdults] = createSignal(2);
<Spinbutton value={adults()} onChange={setAdults} min={1} max={8}>…</Spinbutton>In a form
Pass name to SpinbuttonInput. The form receives the text in the field.
State attributes
| Element | Attribute | Present when |
|---|---|---|
Spinbutton |
tc-spinbutton |
Always |
SpinbuttonLabel |
tc-spinbutton-label |
Always |
SpinbuttonInput |
tc-spinbutton-input |
Always |
SpinbuttonIncrement |
tc-spinbutton-increment |
Always |
SpinbuttonDecrement |
tc-spinbutton-decrement |
Always |
Spinbutton, SpinbuttonInput |
tc-disabled |
The spinbutton is disabled |
SpinbuttonIncrement, SpinbuttonDecrement |
tc-disabled |
The button cannot step, at a limit or while disabled |
Style typed text that is not a value with [aria-invalid="true"].
Reading the state in code
| Member | Type | Description |
|---|---|---|
value() |
number |
The current value. |
min() / max() |
number | undefined |
The limits, if there are any. |
step() |
number |
The step. |
disabled() |
boolean |
Whether the spinbutton is disabled. |
canIncrement() / canDecrement() |
boolean |
Whether the value can move up or down a step. |
setValue(value) |
(number) => void |
Sets the value, snapped to the step and kept in range. |
increment(steps?) / decrement(steps?) |
(number?) => void |
Moves by a number of steps, 1 by default. |
You can reach this state as the render-prop argument on Spinbutton and the
two buttons, through <SpinbuttonStateChild>, or with useSpinbuttonState()
inside any descendant component.
Keyboard
Handled on the field:
| Key | Action |
|---|---|
| ↑ / ↓ | Increases / decreases the value by one step |
| Page Up / Page Down | Changes the value by largeStep steps |
| Home / End | Sets the minimum / maximum, when there is one |
| Enter | Turns the typed text into the value. A form still submits. |
| Text editing keys | Edit the text, as the browser provides them |
API
<Spinbutton>
Renders a <div> by default. Pass exactly one of defaultValue or value.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
ValidConstructor |
'div' |
Element or component to render as. |
defaultValue |
number |
none | Initial value, uncontrolled. |
value |
number |
none | Current value, controlled. |
min |
number |
none | The lowest value. |
max |
number |
none | The highest value. |
step |
number |
1 |
The distance between allowed values, counted from min or 0. |
largeStep |
number |
10 |
Steps moved by Page Up and Page Down. |
disabled |
boolean |
false |
Blocks changes, and disables the field and buttons. |
formatValue |
(value: number) => string |
String |
The text shown in the field. |
parseValue |
(text: string) => number |
Number |
Reads typed text. Return NaN for text that is not a value. |
getValueText |
(value: number) => string |
none | Text for aria-valuetext. |
onChange |
(value: number) => void |
none | Called with the new value. |
children |
JSX.Element | (state: SpinbuttonStateProperties) => JSX.Element |
none | Contents, or a render prop. |
| …rest | props of as |
none | Forwarded to the rendered element. |
<SpinbuttonInput>
Renders an <input> by default.
Rendered attributes: role="spinbutton", type="text",
inputmode="decimal", autocomplete="off", aria-valuenow, aria-valuemin
and aria-valuemax when there are limits, aria-valuetext when
getValueText is set, aria-invalid for typed text that is not a value,
aria-labelledby while a SpinbuttonLabel is mounted, disabled while
disabled, tc-spinbutton-input.
<SpinbuttonLabel>
Renders a <label> by default, with for pointing at the field.
<SpinbuttonIncrement> / <SpinbuttonDecrement>
Render a <button> by default, with type="button", tabindex="-1",
aria-controls pointing at the field, and disabled at the limit.
Every part takes as, ref and children, and forwards the rest of its
props.