On this page
Spin button
Buttons with
command="--step-up"
or
--step-down
point
commandfor
at a native
<input>.
Load
src/behaviors/step.js.
It is not a
pe-*
element.
min,
max,
and
step
stay on the field.
| What runs it | JavaScript | |
|---|---|---|
| Bootstrap 5.3 | No spin-button plugin. Quantity steppers are custom markup plus script. | Not in bootstrap.min.js |
| This repo | `stepUp` / `stepDown` from `command="--step-up"` / `--step-down`. Load `src/behaviors/step.js` | Optional src/behaviors/step.js |
When that API is missing,
this page also loads invoker-commands.js (0.8 KB gzip).
See Polyfills.
| Avoid |
|
|---|
How it works
-
commandforis the input id, with no#— the same id as<label for>. Thecommandevent does not bubble, so the module listens ondocumentin the capture phase. -
The module calls
stepUp()orstepDown(), then firesinputandchange. Those methods do not fire events on their own. -
The same commands work on
number,range,date,month,week,time, anddatetime-local. -
An optional
<output for>is a status live region. The module writes the new value when a button is pressed, so the change is announced while focus stays on the button. -
Layout is Bootstrap’s
.input-groupwhen you want it. The module does not require that class. - Without the script the input still works. The buttons do nothing.
Number
Plus and minus step by 1 between 1 and 8. Arrow keys on the field still work.
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/step.js"></script>
<label class="form-label" for="qty">Quantity</label>
<div class="input-group">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="qty" aria-label="Decrease quantity" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="number" class="form-control" id="qty" name="qty" value="1" min="1" max="8">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="qty" aria-label="Increase quantity" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="qty"></output> Range
The same commands on a slider. Buttons are a fine-step control next to the track.
<label class="form-label" for="volume">Volume</label>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="volume" aria-label="Decrease volume" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="range" class="form-range flex-grow-1" id="volume" name="volume" value="5" min="0" max="10" step="1">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="volume" aria-label="Increase volume" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="volume"></output> Date
One day per press. min and a value keep an empty field from jumping to 1970.
<label class="form-label" for="start-date">Start date</label>
<div class="input-group">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="start-date" aria-label="Previous day" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="date" class="form-control" id="start-date" name="start-date" value="2026-10-06" min="2026-01-01" max="2026-12-31">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="start-date" aria-label="Next day" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="start-date"></output> Month
One month per press. Same min and value rule as date.
<label class="form-label" for="billing-month">Billing month</label>
<div class="input-group">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="billing-month" aria-label="Previous month" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="month" class="form-control" id="billing-month" name="billing-month" value="2026-10" min="2026-01" max="2026-12">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="billing-month" aria-label="Next month" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="billing-month"></output> Week
One week per press. Safari does not ship type=week; the buttons still call stepUp where the type exists.
<label class="form-label" for="sprint-week">Sprint week</label>
<div class="input-group">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="sprint-week" aria-label="Previous week" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="week" class="form-control" id="sprint-week" name="sprint-week" value="2026-W41" min="2026-W01" max="2026-W52">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="sprint-week" aria-label="Next week" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="sprint-week"></output> Time
step is 900 seconds, so each press moves 15 minutes.
<label class="form-label" for="alarm">Alarm</label>
<div class="input-group">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="alarm" aria-label="Earlier time" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="time" class="form-control" id="alarm" name="alarm" value="09:00" min="06:00" max="22:00" step="900">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="alarm" aria-label="Later time" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="alarm"></output> Datetime local
Default step is 60 seconds. Set min and value so the first press is not epoch.
<label class="form-label" for="pickup">Pickup</label>
<div class="input-group">
<button type="button" class="btn btn-outline-secondary" command="--step-down" commandfor="pickup" aria-label="Earlier pickup" tabindex="-1">
<span aria-hidden="true">−</span>
</button>
<input type="datetime-local" class="form-control" id="pickup" name="pickup" value="2026-10-06T09:30" min="2026-10-01T00:00" max="2026-10-31T23:59">
<button type="button" class="btn btn-outline-secondary" command="--step-up" commandfor="pickup" aria-label="Later pickup" tabindex="-1">
<span aria-hidden="true">+</span>
</button>
</div>
<output class="visually-hidden" for="pickup"></output> Accessibility
<input type="number"> is an implicit spinbutton. Arrow keys, Home, and End, and min / max as valuemin / valuemax, come from the browser. Do not restyle a text input into that role.
Name the buttons (aria-label). Hide the + / − glyphs with aria-hidden. tabindex="-1" follows the APG: the keys on the input already step, and touch and voice can still activate the button.
Focus stays on the button. A visually hidden <output for> announces the new value. The module writes it only on a button press.
The buttons stay enabled at min and max. stepUp() already clamps. disabled would drop focus.
Browser support
| No script | Polyfill | No fallback |
|---|---|---|
The input still accepts a typed or native-stepped value. Without step.js the plus and minus buttons do nothing. | command / commandfor when that API is missing. step.js is still required to change the value. | stepUp() throws on step="any" and on types that have no step. An empty date, time, week, or month field without min or value jumps to 1970-01-01 or 00:00. |