Forms & Input
Spinbutton
AvailableA numeric input that increments and decrements via Up/Down keys or buttons, with range announcements.
What is a spin button?
A spin button is a UI control that lets users increment or decrement a value one step at a time using + / − (or ↑ / ↓), selecting a number within a given range. It is commonly used for specifying quantities, counts, or floor numbers.
It is similar to a slider, but a spin button excels at selecting precise, discrete values. The information it needs to convey is the same: the current value, the range (minimum and maximum), and keyboard interaction.
Why does accessibility matter?
- People who rely on the keyboard. If the + and − buttons are
<div>elements, they cannot receive focus or be activated, making it impossible to change the value. Users also expect to increment and decrement with ↑ and ↓. - People who use screen readers. Without the role (spinbutton), current value, and range being communicated, users cannot tell "what is the current value" or "how much further can I go."
The easiest way to meet all of these requirements is to use the native <input type="number">. The browser provides the increment/decrement buttons, keyboard interaction, and screen reader announcements automatically.
Live demo (recommended implementation)
Below is a native <input type="number">.Without using a mouse, focus the input and try changing the value with ↑ and ↓.
Try it: Tab to the input → ↑ ↓ to increment/decrement by 1, or type a number directly. Range: 1–10.
Tip
When a screen reader focuses the input, it announces something like "Quantity, spin button, 1," conveying the role and current value. As you change the value with ↑↓, the new value is announced. All of this is provided automatically by the native element.
Keyboard interaction
| Key | Action | Priority |
|---|---|---|
| ↑ | Increase the value by one step | Required |
| ↓ | Decrease the value by one step | Required |
| Home / End | Set to minimum / maximum value | Recommended |
| Number keys | Enter a value directly | Optional |
Note
Incrementing/decrementing with ↑ ↓ and direct input are automatic with <input type="number">. For custom implementations (role="spinbutton"), you need to handle ↑↓ yourself via a keydown listener.
Required WAI-ARIA roles and properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Native number input | <label for> association | Give the input a label. Role, value, range, and increment/decrement are handled automatically. |
| Custom spinbutton element | role="spinbutton" | Convey to assistive technologies that this is a spinbutton. |
| Custom spinbutton element | tabindex="0" | Make the element keyboard-focusable so it can receive Up/Down arrow key events. |
| Custom spinbutton element | aria-valuemin / aria-valuemax | Defines the selectable range (minimum and maximum). |
| Custom spinbutton element | aria-valuenow | Current value. Must be updated on every increment or decrement. |
| Custom spinbutton element | aria-label / aria-labelledby | A label describing what value the spinbutton controls. |
| Increment/decrement buttons | <button> + aria-label="Increase/Decrease" | Use real button elements for + and -; add a label if using icon-only buttons. |
Implementation: recommended pattern (Good)
Good / Recommended
Start with <input type="number">. Just associating a label gives you the role, value, range, and keyboard interaction for free.
Markup:
<label for="qty">Quantity</label>
<input
type="number"
id="qty"
name="qty"
min="1"
max="10"
step="1"
value="1" />Only when design requirements absolutely prevent using the native element should you build a custom one with role="spinbutton". Make the + and − real <button> elements, and handle ↑↓ on the spinbutton itself.
<!-- Only build a custom one when native doesn't meet design requirements -->
<span id="ppl-label">Number of adults</span>
<div class="spin">
<button type="button" data-dec aria-label="Decrease">−</button>
<div role="spinbutton"
tabindex="0"
aria-labelledby="ppl-label"
aria-valuemin="1"
aria-valuemax="9"
aria-valuenow="2"
id="ppl-spin">2</div>
<button type="button" data-inc aria-label="Increase">+</button>
</div>const spin = document.getElementById('ppl-spin');
const MIN = 1, MAX = 9, STEP = 1;
function setValue(next) {
const v = Math.min(MAX, Math.max(MIN, next));
spin.setAttribute('aria-valuenow', String(v));
spin.textContent = String(v); // Keep the visual display in sync
}
document.querySelector('[data-inc]').addEventListener('click', () =>
setValue(Number(spin.getAttribute('aria-valuenow')) + STEP)
);
document.querySelector('[data-dec]').addEventListener('click', () =>
setValue(Number(spin.getAttribute('aria-valuenow')) - STEP)
);
// Allow incrementing/decrementing with ↑↓ when the spinbutton itself has focus
spin.addEventListener('keydown', (e) => {
const now = Number(spin.getAttribute('aria-valuenow'));
if (e.key === 'ArrowUp') { e.preventDefault(); setValue(now + STEP); }
if (e.key === 'ArrowDown') { e.preventDefault(); setValue(now - STEP); }
if (e.key === 'Home') { e.preventDefault(); setValue(MIN); }
if (e.key === 'End') { e.preventDefault(); setValue(MAX); }
});Anti-pattern (Bad)
Below is a spin button built with a text input and <div>-based + and − buttons.You can click them with a mouse, but you cannot increment/decrement with the keyboard, and neither the value nor the range is announced.
Try it: Pressing Tab does not focus the + or − buttons. A screen reader cannot identify this as a 'spin button' or announce the current value.
<!-- ❌ Anti-pattern: text input + div-based + and − buttons -->
<span>Quantity</span>
<div class="spin">
<div class="btn" onclick="dec()">−</div> <!-- div: not focusable -->
<input type="text" id="qty" value="1"> <!-- No label, not a number input -->
<div class="btn" onclick="inc()">+</div>
</div>
<!--
No role="spinbutton" or aria-valuenow/min/max.
The + and − are divs, so they can't be reached by keyboard,
and the value and range are not announced to assistive technologies.
-->Bad / Avoid
Problems with this implementation:
- Cannot increment/decrement via keyboard — The + and − are
divelements that cannot receive focus or be activated. - Role is not communicated — Without
role="spinbutton", it is not recognized as a spin button. - Value and range are not communicated — Missing
aria-valuenow / valuemin / valuemax. - No accessible name — The
labelis not associated, so the user cannot tell what this number represents.
Implementation checklist
- Considered using <input type="number"> first (preferred approach)
- The input/element has a label (<label for> or aria-label/aria-labelledby)
- + and - are real <button> elements and are keyboard-operable
- Up/Down arrow keys can increment and decrement the value
- Custom implementation includes role="spinbutton" with aria-valuemin, aria-valuemax, and aria-valuenow
- aria-valuenow is updated on every increment or decrement
- Value does not exceed the defined range (stops at minimum and maximum)
- Focus is visible (focus ring is not suppressed)
Source (English):Spinbutton Pattern — W3C APG(opens in a new tab)