Skip to content

Number stepper

A native <input type="number"> in an input group flanked by two .re-input-group__action buttons marked data-re-number-step="1" / "-1". The input is marked data-re-number. Opt into the behavior:

import { enhanceNumberStepper } from "@relements/core";
enhanceNumberStepper(document);

The native number input is the spinbutton — the behavior adds nothing to its semantics, it only gives pointer users bigger step targets. Pressing a button calls native stepUp()/stepDown() and then dispatches input + change (which those methods don’t fire), so frameworks observe the change like typing. min/max/step, arrow keys, and form value stay native; the buttons are tabindex="-1" and disable at the matching bound and when the input is disabled or read-only. Without JavaScript it’s a normal number input with its native spinner.

Live example
<label class="re-field">
  <span class="re-field__label">Quantity</span>
  <span class="re-input-group">
    <button
      class="re-input-group__action"
      type="button"
      data-re-number-step="-1"
      aria-label="Decrease"
      tabindex="-1"
    >
      &minus;
    </button>
    <input
      class="re-input"
      type="number"
      data-re-number
      value="1"
      min="0"
      max="10"
      step="1"
      aria-label="Quantity"
    />
    <button
      class="re-input-group__action"
      type="button"
      data-re-number-step="1"
      aria-label="Increase"
      tabindex="-1"
    >
      +
    </button>
  </span>
</label>
  • Keyboard — All input lives on the native <input type="number">, the only tab stop: Tab to focus, then type, or use Arrow Up / Arrow Down to step by step (the browser also honours min/max here). The ± buttons are tabindex="-1" and deliberately not in the tab order — they’re pointer shortcuts for the same native stepping that the arrow keys already provide.
  • Focus — The input group owns the visible :focus-within ring, so the whole control lights up when the input is focused; the input’s own ring is suppressed to avoid a double outline. A ± button reached by pointer/script shows an inset :focus-visible ring.
  • Semantics<input type="number"> is an implicit role="spinbutton", and the behavior adds nothing to it — min/max/step, the current value, and validity stay 100% native, announced by assistive tech as a spin button. Each ± button is a <button type="button"> with an aria-label (Decrease / Increase) since its glyph (− / +) isn’t reliable accessible text. Buttons set disabled at the matching bound and when the input is disabled or read-only, so AT and pointer users can’t step past a limit; the field’s aria-invalid / :user-invalid state is reflected on the group.
  • Notes — Pressing a button calls native stepUp() / stepDown() and then dispatches input + change (which those methods don’t fire on their own), so screen-reader value announcements and framework bindings update exactly as if the user typed. Without JavaScript it degrades to a plain number input with its native spinner — fully operable by keyboard. See the accessibility guide for cross-component conventions.