@magic-spells/quantity-input

Minus, plus, done.

A quantity stepper that owns the clamping, the bounds and the disabled states — and nothing else. You bring the markup.

Install npm i @magic-spells/quantity-input
JS (gzip) 1.3 KB
CSS (gzip) 0.4 KB
Dependencies 0
// install npm i @magic-spells/quantity-input // import (auto-registers the <quantity-input> element) import '@magic-spells/quantity-input' import '@magic-spells/quantity-input/styles' // optional defaults // use it — you provide the buttons and the input <quantity-input min="1" max="10" value="5"> <button data-action-decrement type="button">−</button> <input type="number" /> <button data-action-increment type="button">+</button> </quantity-input>

Bounds it can't leave.

min defaults to 1, max is open-ended, and value starts at the minimum. Each button disables itself at its own bound — typing out of range snaps back on commit.

defaults
min 5 · max 15 · value 10
min 0 · value 0
<quantity-input min="5" max="15" value="10">…</quantity-input>

Count by sixes.

step sets how much each press moves. Clamping still wins at the edges, so a step that would overshoot lands on max instead.

step 6 · case of six
step 5 · max 22 (clamps short)
<quantity-input min="0" max="24" value="0" step="6">…</quantity-input>

Out of stock.

disabled on the host disables both buttons and the input, and every handler no-ops. Toggle it as a property from JavaScript.

disabled
el.disabled = !el.disabled // or the `disabled` attribute

Enter commits. Nothing submits.

This is a real <form>. Type a quantity and press Enter — the value is committed and the keypress is swallowed, so the form never implicitly submits. The button still does.

Discount code
Quantity — press Enter here

form submits: 0

// inside the component if (e.key === 'Enter') { e.preventDefault(); commit(); }

Listen in.

Every committed change fires a bubbling quantity-input:change with { value }. Clamped input and no-op presses stay quiet.

min 1 · max 10 · value 5
el.addEventListener('quantity-input:change', (e) => { console.log(e.detail) // { value: 6 } })

One cart, many steppers.

Instances are fully independent — separate bounds, separate state. The subtotal listens once, on the list, because the event bubbles.

Ceramic mug
$18.00 · max 6
Espresso beans
$22.00 · step 2
Gift box
$9.00 · one per order
Subtotal $0.00

The whole API, on one screen.

Five attributes, all mirrored as properties, and one event. That's everything.

Attributes & Properties

Name
Default
Description
min
1
Lowest allowed value · 0 is valid
max
null
Highest allowed value · omit for unlimited
value
min
Current quantity, clamped to the bounds
step
1
Amount each press adds or subtracts · positive integers only
disabled
false
Disables both buttons and the input; handlers no-op

Markup hooks & Events

Name
Type
Description
[data-action-decrement]
hook
Any element that should subtract a step
[data-action-increment]
hook
Any element that should add a step
input
hook
The first descendant input · typed values commit on change or Enter
quantity-input:change
event
Bubbles on every committed change · detail { value }