Stepper (+/−)
Two buttons and a number in between, for small exact quantities. Learn limits and steps, typing into the value, long-press, the quirks of the web's number input, and the network and accessibility bugs testers find in cart steppers.
Two bunches of bananas. Three guests at the table. One adult, two children on the flight. Every time an app asks “how many?”, there’s a good chance it answers with a stepper: a minus, a number, a plus.
It’s the simplest control on the screen, and one of the most expensive to get wrong: the number in a cart stepper becomes money. Let’s see where it breaks.
What is a stepper?
A stepper is a control that changes a number by a fixed step (usually 1) with a decrease (−) and an increase (+) button. The value stays between a minimum and a maximum.
You’ll find steppers in:
- Shopping carts: quantity of each product.
- Bookings: guests, rooms, adults/children, nights.
- Recipes and fitness: servings, sets, reps.
- Settings: number of copies to print, reminder minutes.
Anatomy
Oat milk 1 L
Only 4 left
15- 1
Label
What is being counted: "Quantity", "Guests", "Adults". Also the accessible name.
- 2
Decrease button (−)
Lowers the value by one step. Disabled at the minimum, or turns into a trash icon in carts.
- 3
Value
The current number. Sometimes editable, so users can type 12 instead of tapping 11 times.
- 4
Increase button (+)
Raises the value by one step. Disabled at the maximum (stock limit, max tickets).
- 5
Limit hint
Explains why + stopped working: "Only 4 left", "Max 8 guests".
What to know about the value
- Min and max. At the limit, the matching button must be disabled (and look it). Common limits: 1 for a cart line (0 means “remove”), the stock reported by the server, “max 8 guests”, “max 9 travellers in total”.
- Step size. Usually 1, but not always: 0.5 kg of cheese, packs of 6, 15-minute slots. Check the value can’t land between steps.
- Typing. Some steppers let you tap the number and type “12”. Then everything from text fields applies: validation, the keyboard type, what happens to “05”, “1e2”, “-3” or an empty field.
- Long-press. Many steppers repeat while you hold the button and speed up after a while. It must stop at the limit and stop when you let go.
Dev note
iOSUIStepperrepeats by default.
Stepper vs slider vs text field
| Component | Best for | Weak at |
|---|---|---|
| Stepper | Small exact numbers, small changes (1–10) | Big jumps: 1 → 50 needs 49 taps |
| Slider | Rough values in a range (volume, price range) | Exact values |
| Text field (number) | Any exact number, big ranges | Quick +1 changes, limits are invisible |
A good rule: if users usually change the value by one or two, use a stepper. If the value can be large, let users type (an editable stepper or a plain field).
Designs & variants
All of these work. Hold + or − to repeat, and try the editable ones with the keyboard (↑ ↓ Home End).
UIStepper
iOSApple's native stepper: two buttons in a grey capsule, no value inside. The app must show the value in a label next to it.
Oat milk 1 L
€2.49 · 4 in stock
Cart quantity
iOS · Android · WebThe shopping classic. At 1, the − turns into a trash icon, so removing is a clear, deliberate action. + stops at the stock limit.
Outlined buttons
Android · Material 3Material has no stepper component, so Android apps build one from icon buttons and a text. Each team's version behaves a little differently.
Editable quantity
WebUsers can type 12 instead of clicking 11 times. Typed values are validated: digits only, clamped to the limits, the old value is kept if it's invalid.
Adults
Age 16+
Children
Age 2–15 · max 9 travellers
Several counters
AllTravel and ticket apps stack counters. Each needs its own limits (0 children is fine, 0 adults isn't) and its own name for screen readers.
input.value = "2"
Native number input
Web<input type=number> gives you tiny spin arrows for free, plus quirks: 'e' is allowed, the mouse wheel can change it, and an invalid value reads as empty.
Dev note: One name, many implementations
iOS has a native UIStepper (SwiftUI: Stepper). Material Design has no stepper component, so Android apps build their own from icon buttons, and Android’s older NumberPicker is a scroll wheel instead. On the web you get <input type="number"> or, more often, a custom component. Custom means every team makes slightly different decisions, and that’s where bugs hide.
The web’s number input and its quirks
<input type="number"> looks like the perfect stepper: arrows, min, max, step. But it has surprises every web tester should know:
🔢 Native number input, under the microscope
This is a real <input type="number" min="1" max="10" step="1">. Type the samples below and watch what the browser reports.
- value
- "1"
- valueAsNumber
- 1
- Number(value)
- 1
- parseInt(value)
- 1
- validity
- valid
Click into the field, then scroll the mouse wheel over it. Value changes caused by the wheel: 0Chromium browsers change a focused number input on wheel; others may not. Try several browsers.
05value "05". Number() gives 5, but an app that keeps the string shows "05" and may even do "05" + 1 = "051".1e2Valid scientific notation: value "1e2", which is 100. parseInt("1e2") gives 1. Two parts of the app can disagree.eAllowed to type, but not a number: value is "" (empty) and badInput is true. The app sees an empty field.2.5Valid number, but stepMismatch is true for step="1". Does the app round, reject or send 2.5 items?-3Below min="1": rangeUnderflow. The browser only flags it; nothing stops the app from sending it.99Above max="10": rangeOverflow. Same story: min/max are hints until a form is validated.
- “e” is allowed, because
1e2(= 100) is a valid number in scientific notation. “e” on its own makes the value empty, not an error the app can see. - The mouse wheel can change the value in Chromium browsers when the field is focused. Users scroll the page and silently order 7 instead of 2.
- min, max and step are only hints. The browser flags invalid values (
validity), but nothing stops the app from sending them unless the form is validated. - Leading zeros stay in the text (“05”), and code that treats the value as a string can do
"05" + 1 = "051".
Dev note
type="text" inputmode="numeric" with their own validation instead.When to use it
A stepper is a good choice when:
- The number is small and exact: 1–10 items, guests, tickets.
- Users usually change it by one or two from a sensible default.
- The limits are known and meaningful (stock, max guests), and can be shown.
- The change is easy to see and to undo: the number and the total update right away.
Good and bad uses
Small exact counts
Usually 1–4, rarely more than 8. One or two taps, and you can see the value.
Large values
Getting from €0 to €250 takes 250 taps (or a very long press). Use a text field with a number keyboard.
Show the limit
Oat milk
Max 4 per order
- is greyed out and the text says why. No surprise error later.
A silent wall
Oat milk
−4+- looks active, but tapping it does nothing, or fails only at checkout. Users think the app is broken.
Common bugs
| # | Bug | What you see | Typical severity |
|---|---|---|---|
| 1 | + enabled at the stock limit | Tap + at the limit, the server refuses, the number jumps back (or checkout fails) | Medium–High |
| 2 | Typed value not validated | “05”, “1e2”, “-3” or an empty field break the total or reach the server | High |
| 3 | Shown and saved quantity differ | After quick taps the field says 6, the order is for 3 | Critical (money) |
| 4 | − at 1 removes without warning | One tap too many and the item is gone, no undo | Medium |
| 5 | Long-press doesn’t stop | Value keeps running after release, or runs past the max | Medium |
| 6 | Disabled state lies | − looks disabled at 1 but still works (or the other way round) | Low–Medium |
| 7 | Silent for screen readers | “Button, button”, and the new value is never announced | High (for affected users) |
| 8 | Mouse wheel changes the number | Scrolling the page over a focused field changes the quantity | Medium |
| 9 | Tiny buttons | − and + are 24 px apart; users hit the wrong one | Medium |
Stress-test it
Tap + as fast as you can, or hold it, and then wait. Switch between the three sync strategies and compare what the cart shows with what the server saved.
📶 Rapid taps vs the network
Tap + five times as fast as you can (or hold it), then wait. Does the cart agree with the server?
Bananas (bunch)
€1.99 each · total €1.99
Cart shows
1
Server saved
1
Requests
0
Tap + quickly a few times.
- No requests yet…
Find the bug
This grocery web shop has a basket with six bugs. Explore it like a real tester:
- Push every quantity to its limit, and past it.
- Type into the quantity field: “05”, “1e2”, nothing at all. Then press +.
- Tap + quickly several times, wait, then compare the field with the subtotal. Watch the network log.
- Bring a quantity down to 1, and press − once more.
- Turn on the screen reader captions and Tab through a row, then press Enter on +.
- Click into a quantity field, then scroll the page with the mouse wheel.
🐞 Bug hunt: Grocery basket
This demo has 6 hidden bugs. Explore it like a tester, then check your findings.
Your basket
Organic bananas
€1.99 · in stock
€3.98
Oat milk 1 L
€2.49 · only 4 left
€7.47
Sourdough loaf
€3.80 · in stock
€3.80
Device settings
- Change a quantity to see API requests…
- 1
Hint: Oat milk has only 4 left. Press + until you pass 4.
- 2
Hint: Type 05 into the bananas field and press Enter, then press +. Then try 1e2.
- 3
Hint: Tap + on bananas four times quickly, wait two seconds, then compare the field with "Subtotal (N items)".
- 4
Hint: Bring the sourdough to 1 and press − once more.
- 5
Hint: Turn on the screen reader captions, Tab to a − or + button, then press Enter.
- 6
Hint: Click into a quantity field, keep the mouse over it and scroll the page with the wheel.
How to test it
Checklist
Stepper test checklist
0 of 13 checked
Example test cases
TC-STP-001+ is disabled at the stock limitedgehigh
Preconditions
- Oat milk has stock 4 on the server
Steps
- Add Oat milk to the basket
- Tap + until the value stops increasing
Expected result
The value stops at 4, + looks disabled, a hint says "Max 4" and no request with qty=5 is sent.
TC-STP-002Invalid typed values are rejectednegativehigh
Steps
- Tap the quantity field
- Enter each value and confirm
- Check the field, the line total and the request
Test data
05 · 1e2 · -3 · (empty) · 999
Expected result
"05" becomes 5; "1e2", "-3" and empty are rejected and the previous value stays; 999 is clamped to the max (or rejected with a message).
TC-STP-003Rapid taps end in a consistent quantityedgehigh
Preconditions
- Network throttling: slow 3G or a proxy adding random delay
Steps
- Tap + 6 times as fast as possible
- Wait until all requests have finished
- Reload the basket
Expected result
Field, subtotal and the reloaded basket all show the same quantity (start + 6).
TC-STP-004Removing the last unitnegativemedium
Preconditions
- Sourdough quantity is 1
Steps
- Tap − (or the trash icon)
Expected result
The item is removed only with a clear action (trash icon or confirmation), and an Undo option is offered.
TC-STP-005Long-press repeats and stopspositivelow
Steps
- Press and hold + on a stepper with max 20
- Release after 2 seconds
- Hold again until the value reaches 20
Expected result
The value increases while held and stops instantly on release; it never goes past 20.
TC-STP-006Screen reader names and valuepositivemedium
Preconditions
- VoiceOver or TalkBack is on
Steps
- Swipe to the + of Organic bananas
- Double-tap
Expected result
Hears "Increase quantity of Organic bananas, button", then the new value, e.g. "3".
TC-STP-007Mouse wheel does not change the quantity (web)negativemedium
Steps
- Click into a quantity field
- Move the mouse over it and scroll down 3 notches
Expected result
The page scrolls; the quantity stays the same and no request is sent.
Tip: click a test case to expand it.
Accessibility
A stepper is two tiny icon buttons and a number. For screen reader users, all three need words.
| Key (web, editable value) | What it should do |
|---|---|
| ↑ / ↓ | Increase / decrease by one step |
| Page Up / Page Down | Bigger step (optional) |
| Home / End | Jump to min / max (if they exist) |
| Typing digits | Sets the value directly |
- The ARIAARIAHTML attributes that tell assistive technology what custom elements are and what state they're in.Open in glossary → spinbutton pattern: the value has a name (“Quantity of Oat milk”) and, when the number needs a unit, a text value: “2 guests”, “30 minutes”.
Dev note
The value is an element withrole="spinbutton"(a native<input type="number">has it already), witharia-valuenow,aria-valueminandaria-valuemax. Addaria-valuetextfor the unit. - Named buttons: “Increase quantity” / “Decrease quantity”, ideally with the product name. Icon-only buttons with no name are read as “button”.
- Announce the new value after each press. With a spinbutton that has focus, screen readers do this automatically; when the focus stays on the + button, use a polite live region.
- Disabled at the limits: keyboard focus must not be lost when + becomes disabled under your finger.
Dev note
Usearia-disabled(or keep focus somewhere sensible). - Touch targetsTouch targetThe area of the screen that responds to a tap, which should be large enough for a finger.Open in glossary →: each button needs at least 44×44 pt (iOS) / 48×48 dp (Android), with enough space between − and +. On the web, WCAG 2.5.8 (AA) asks for at least 24×24 CSS px; 44×44 px (2.5.5, AAA) is the comfortable target.
- On iOS, VoiceOver reads the native stepper’s two halves as buttons (“Increment”, “Decrement” by default). Make sure the label with the value next to it is announced too, or give the stepper a meaningful accessibility value.
Dev note
That’sUIStepper.
Mobile vs web
- Ask about the limits, the step, long-press repeat (on by default) and wrapping (going past max back to min, off by default).
Dev note
Native:UIStepper(UIKit) orStepper(SwiftUI). Properties:minimumValue,maximumValue,stepValue,autorepeatandwraps. - The native stepper shows no number itself. Check the label next to it updates on every change, including during long-press.
- Cart steppers in shopping apps are almost always custom. Test them like a web component.
- Test with VoiceOver, Dynamic Type (the number must not be cut off: “10” vs “1”) and external keyboards on iPad.
- No Material stepper: apps build their own. Ask how limits, long-press and disabled states were built.
Dev note
Apps combineIconButtons with aText(Compose) or views. NumberPickeris a different control: a scrolling wheel. It’s still used for timers and old-style forms.- Test TalkBack names (an icon button without a description is read as “Button” or “Unlabelled”), font scale, and configuration changes: rotate the phone and check the quantity survives.
Dev note
AnImageButtonwithoutcontentDescription. - Watch rapid taps on slow networks: the same race conditions apply.
<input type="number">: free arrows and keyboard support, but “e” is allowed, the wheel can change it in Chromium browsers, invalid input reads as empty, and min/max/step are not enforced on their own.- Mobile browsers show the numeric keyboard only with the right
typeorinputmode. Check it on iOS Safari and Chrome on Android. - Custom steppers: check keyboard support, focus after + becomes disabled, and the requests in DevToolsDevToolsDeveloper tools built into browsers for inspecting pages, network traffic and errors.Open in glossary → (one per tap? debounced? what does the server answer?).
- Open the cart in two tabs, change the quantity in one, reload the other: which value wins?
Related pages: the cart math around the stepper (totals, discounts, tax) is in Shopping cart & checkout, limits are a perfect case for boundary value analysis, and for rough values a slider may be the better control.
Check your understanding
Quick quiz
0/5 answered · 0 correct
1.A product has 4 items in stock. The cart quantity is 4. What should the + button do?
2.Which of these can a user type into a native <input type="number"> without the browser blocking it?
3.After tapping + quickly five times, the field shows 7 but the order total is for 4. What is the most likely cause?
4.What is the best behavior when the user presses − on a cart item with quantity 1?
5.A screen reader says only "button" on the + of a quantity stepper. What is missing?
Key takeaways
- A stepper changes a small exact number by a fixed step between a min and a max. For large values, let users type.
- At the limits, the matching button is disabled with a reason. Server limits like stock belong in the UI before the request.
- Typed values need validation: “05”, “1e2”, “-3” and empty. The web’s
<input type="number">allows “e” and can change on mouse wheel. - Rapid taps + network = race conditions: debounce, send absolute values, ignore stale replies, and check that screen, total and server agree.
- Going below the minimum (removing an item) needs confirmation or undo, and screen readers need named buttons and the new value announced.
Finished this page?Nice work, this page is complete!
Mark it as done to track your progress. It's saved in this browser, no account needed.
Its review cards are now in Bites ⚡