Skip to content
Bugwise
Menu
UI Components

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.

35 min Inputs & selection iOS · Android · Web Also called: Quantity stepper, Number stepper, Spin button, Counter

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
4234
  1. 1

    Label

    What is being counted: "Quantity", "Guests", "Adults". Also the accessible name.

  2. 2

    Decrease button (−)

    Lowers the value by one step. Disabled at the minimum, or turns into a trash icon in carts.

  3. 3

    Value

    The current number. Sometimes editable, so users can type 12 instead of tapping 11 times.

  4. 4

    Increase button (+)

    Raises the value by one step. Disabled at the maximum (stock limit, max tickets).

  5. 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
    iOS UIStepper repeats by default.

Stepper vs slider vs text field

ComponentBest forWeak at
StepperSmall exact numbers, small changes (1–10)Big jumps: 1 → 50 needs 49 taps
SliderRough values in a range (volume, price range)Exact values
Text field (number)Any exact number, big rangesQuick +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).

Guests: 2

UIStepper

iOS

Apple'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

2

Cart quantity

iOS · Android · Web

The shopping classic. At 1, the − turns into a trash icon, so removing is a clear, deliberate action. + stops at the stock limit.

Servings4

Outlined buttons

Android · Material 3

Material has no stepper component, so Android apps build one from icon buttons and a text. Each team's version behaves a little differently.

Quantity

Editable quantity

Web

Users 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+

2

Children

Age 2–15 · max 9 travellers

0

Several counters

All

Travel 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
That’s why many shops use 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

Guests−2+

Usually 1–4, rarely more than 8. One or two taps, and you can see the value.

Large values

Amount to transfer−€250+

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

−4+
  • 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

#BugWhat you seeTypical severity
1+ enabled at the stock limitTap + at the limit, the server refuses, the number jumps back (or checkout fails)Medium–High
2Typed value not validated“05”, “1e2”, “-3” or an empty field break the total or reach the serverHigh
3Shown and saved quantity differAfter quick taps the field says 6, the order is for 3Critical (money)
4− at 1 removes without warningOne tap too many and the item is gone, no undoMedium
5Long-press doesn’t stopValue keeps running after release, or runs past the maxMedium
6Disabled state lies− looks disabled at 1 but still works (or the other way round)Low–Medium
7Silent for screen readers“Button, button”, and the new value is never announcedHigh (for affected users)
8Mouse wheel changes the numberScrolling the page over a focused field changes the quantityMedium
9Tiny buttons− and + are 24 px apart; users hit the wrong oneMedium

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?

How the app syncs
Network

Bananas (bunch)

€1.99 each · total €1.99

1

Cart shows

1

Server saved

1

Requests

0

Tap + quickly a few times.

Network lognewest first
  • 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.

Found0/6
grocer.example.com

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

Subtotal (6 items)€15.25
Delivery €2.99
Total€18.24

Device settings

Network log0 requests
  • Change a quantity to see API requests…
  1. 1

    Hint: Oat milk has only 4 left. Press + until you pass 4.

  2. 2

    Hint: Type 05 into the bananas field and press Enter, then press +. Then try 1e2.

  3. 3

    Hint: Tap + on bananas four times quickly, wait two seconds, then compare the field with "Subtotal (N items)".

  4. 4

    Hint: Bring the sourdough to 1 and press − once more.

  5. 5

    Hint: Turn on the screen reader captions, Tab to a − or + button, then press Enter.

  6. 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 limit

Preconditions

  • Oat milk has stock 4 on the server

Steps

  1. Add Oat milk to the basket
  2. 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 rejected

Steps

  1. Tap the quantity field
  2. Enter each value and confirm
  3. 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 quantity

Preconditions

  • Network throttling: slow 3G or a proxy adding random delay

Steps

  1. Tap + 6 times as fast as possible
  2. Wait until all requests have finished
  3. Reload the basket

Expected result

Field, subtotal and the reloaded basket all show the same quantity (start + 6).

TC-STP-004Removing the last unit

Preconditions

  • Sourdough quantity is 1

Steps

  1. 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 stops

Steps

  1. Press and hold + on a stepper with max 20
  2. Release after 2 seconds
  3. 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 value

Preconditions

  • VoiceOver or TalkBack is on

Steps

  1. Swipe to the + of Organic bananas
  2. 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)

Steps

  1. Click into a quantity field
  2. 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 DownBigger step (optional)
Home / EndJump to min / max (if they exist)
Typing digitsSets 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 with role="spinbutton" (a native <input type="number"> has it already), with aria-valuenow, aria-valuemin and aria-valuemax. Add aria-valuetext for 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
    Use aria-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’s UIStepper.

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) or Stepper (SwiftUI). Properties: minimumValue, maximumValue, stepValue, autorepeat and wraps.
  • 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 combine IconButtons with a Text (Compose) or views.
  • NumberPicker is 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
    An ImageButton without contentDescription.
  • 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 type or inputmode. 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. 1.A product has 4 items in stock. The cart quantity is 4. What should the + button do?

  2. 2.Which of these can a user type into a native <input type="number"> without the browser blocking it?

  3. 3.After tapping + quickly five times, the field shows 7 but the order total is for 4. What is the most likely cause?

  4. 4.What is the best behavior when the user presses − on a cart item with quantity 1?

  5. 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 ⚡
Esc

Type to search the whole site.