Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Add custom increment and decrement buttons to number inputs. Respects min, max, and step attributes with automatic button disabling at boundaries.
The data-stepper attribute enhances a native <input type="number"> with visible increment and decrement buttons. Just add the attribute — the script reads min, max, and step from the input and builds the controls automatically.
<form-field> <label for="qty">Quantity</label> <input type="number" id="qty" min="0" max="50" step="1" value="1" data-stepper></form-field>
Add data-stepper to any <input type="number">. The init script:
.number-wrapper containeraria-label="Decrease") before the inputaria-label="Increase") after the inputtabindex="-1" so the input itself keeps keyboard focusmin, max, and step attributes for boundary logicmin, the increase button when at maxdata-stepper-init to prevent double-bindingThe underlying input remains a real form control. It submits with the form, supports validation, and native keyboard arrows still increment and decrement as expected.
| Attribute | Values | Description |
|---|---|---|
data-stepper |
boolean | Enables the stepper enhancement on a number input. |
data-stepper-init |
boolean | Set automatically to prevent double-binding. Do not set manually. |
min |
number | Standard HTML attribute. The decrease button is disabled at this value. |
max |
number | Standard HTML attribute. The increase button is disabled at this value. |
step |
number | Standard HTML attribute. Controls the increment/decrement amount per click. |
When step is a decimal value, the stepper uses toFixed() to maintain precision. No floating-point drift — values stay clean.
<form-field> <label for="weight">Weight (kg)</label> <input type="number" id="weight" min="0" max="10" step="0.5" value="1.0" data-stepper></form-field>
Combine min, max, and step for any numeric range. Buttons disable automatically at boundaries.
<form-field> <label for="temp">Temperature (°C)</label> <input type="number" id="temp" min="-20" max="45" step="5" value="20" data-stepper></form-field>
Without min or max, neither button ever disables. The stepper allows unbounded incrementing and decrementing.
<form-field> <label for="offset">Offset</label> <input type="number" id="offset" step="1" value="0" data-stepper></form-field>
The stepper automatically manages button states based on the current value:
min, the decrease button gets disabledmax, the increase button gets disabledcursor: not-allowedClicking a stepper button fires both input and change events on the underlying input, matching the behavior of native keyboard arrows.
| Event | Target | Description |
|---|---|---|
input |
The <input> |
Fired immediately when a stepper button is clicked. |
change |
The <input> |
Fired after the value changes, matching native behavior. |
const input = document.querySelector('[data-stepper]'); input.addEventListener('change', (e) => { console.log('New value:', e.target.value);}); input.addEventListener('input', (e) => { console.log('Stepping to:', e.target.value);});
Steppers work naturally inside forms. Combine multiple steppers for booking or quantity selection interfaces.
<form class="stacked"> <form-field> <label for="adults">Adults</label> <input type="number" id="adults" min="1" max="10" step="1" value="2" data-stepper> </form-field> <form-field> <label for="children">Children</label> <input type="number" id="children" min="0" max="8" step="1" value="0" data-stepper> </form-field> <button type="submit">Search</button></form>
The .number-wrapper container and its buttons are styled via CSS. All styles are gated on [data-stepper-init] so the input renders normally without JavaScript.
/* Wrapper around input and buttons */.number-wrapper { display: inline-flex; align-items: center; border: 1px solid var(--color-border); border-radius: var(--radius-m);} /* Stepper buttons */.number-wrapper button { background: var(--color-surface-raised); border: none; cursor: pointer; padding: var(--size-xs) var(--size-s);} /* Disabled state at boundaries */.number-wrapper button:disabled { opacity: 0.4; cursor: not-allowed;}
The wrapper uses inline-flex for alignment and rounds the outer corners. Button disabled states reduce opacity for clear visual feedback.
Inputs added to the DOM after page load are automatically enhanced via a MutationObserver. No manual initialization is needed.
<section> <h2>Accessibility</h2> <ul> <li>Stepper buttons have <code>aria-label="Decrease"</code> and <code>aria-label="Increase"</code> for screen readers</li> <li>Buttons use <code>tabindex="-1"</code> — the input itself retains keyboard focus, avoiding extra tab stops</li> <li>Native keyboard arrows (Up/Down) still work for stepping, preserving expected input behavior</li> <li>The input remains a real <code><input type="number"></code>, so screen readers announce it correctly</li> <li>Disabled buttons are conveyed to assistive technology via the native <code>disabled</code> attribute</li> <li>Without JavaScript, the input renders as a standard number field (progressive enhancement)</li> </ul> </section>