Skip to content

Documentation

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.

Sizes are minified and gzip. A plugin figure includes the Bootstrap runtime that plugin imports. Shipping every plugin is bootstrap.bundle.min.js at 23.3 KB gzip, not the sum of these rows.
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.

Markup this component accepts. The full inventory is /components.json.
Avoid
  • commandfor is the input id, with no #. Do not point it at a wrapper.
  • Do not add a pe-step or pe-spin element. The buttons already invoke the input.
  • Do not add role="spinbutton" or aria-valuemin / aria-valuemax / aria-valuenow on the input. type="number" already has that role.
  • step="any" is ignored. stepUp() throws InvalidStateError; the command does nothing.
  • Give date, time, week, month, and datetime-local a min or a value. An empty field steps from epoch.

How it works

  • commandfor is the input id, with no # — the same id as <label for>. The command event does not bubble, so the module listens on document in the capture phase.
  • The module calls stepUp() or stepDown(), then fires input and change. Those methods do not fire events on their own.
  • The same commands work on number, range, date, month, week, time, and datetime-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-group when 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.

HTML
<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.

HTML
<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.

HTML
<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.

HTML
<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.

HTML
<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.

HTML
<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.

HTML
<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

Three columns, not one baseline. The same row is on the overview.
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.