quantity_stepper

An accessible quantity stepper for Flutter: minus and plus buttons around an integer, with screen-reader adjust actions, 48 dp tap targets, long-press repeat with acceleration, keyboard support and a real FormField<int>, and no dependencies beyond the Flutter SDK.

The example app: basic, form, vertical, editable and custom-styled steppers

Screenshot: the example/ app built for the web and rendered in headless Chrome, in its default state.

Install

dependencies:
  quantity_stepper: ^0.1.0

Requires Dart 3.9 or newer (Flutter 3.35 or newer). The lower bound comes from SemanticsService.sendAnnouncement, which the Flutter API docs mark as the replacement for the deprecated announce after v3.35; it has not been tried on Flutter 3.35 itself. The tests were run on Flutter 3.47.5 only.

Use

import 'package:quantity_stepper/quantity_stepper.dart';

QuantityStepper(
  label: 'Quantity', // read by screen readers; required
  min: 1,
  max: 10,
  initialValue: 2,
  onChanged: (int quantity) => setState(() => _quantity = quantity),
)

To own the state yourself, pass value (controlled):

QuantityStepper(
  label: 'Quantity',
  min: 1,
  max: 10,
  value: _quantity,
  onChanged: (int q) => setState(() => _quantity = q),
)

In a form:

QuantityStepperFormField(
  label: 'Guests',
  initialValue: 1,
  validator: (int? n) => (n ?? 0) < 2 ? 'Add at least two guests.' : null,
  onSaved: (int? n) => _guests = n ?? 1,
)

Run the demo: cd example && flutter run -d chrome.

Features

Each item below is covered by a widget test in test/.

  • Integer value with min, max and step, clamped everywhere: the initial value, taps, keys, typing and screen-reader actions.
  • Controlled or uncontrolled. Pass value and onChanged to own the state, or initialValue to let the widget keep it.
  • Long-press repeat with acceleration. After repeatDelay (400 ms) the value moves once, then every repeatInterval (160 ms); the gap halves after five repeats and again after ten. A quick tap moves exactly one step, releasing a hold does not add a step, and the repeat stops at a limit, on cancel, and when the widget is disabled or removed. autoRepeat: false turns it off.
  • Keyboard. The control is one Tab stop. ArrowUp/ArrowDown move one step, PageUp/PageDown move largeStep (default step * 10). The mouse wheel is opt-in (enableMouseWheel) and only works while the control has focus, so scrolling a page never changes a value by accident.
  • Typing (editable: true). A numeric field that accepts digits, and a leading minus when min is negative, up to nine digits. A number in range applies as you type and is never rewritten under your cursor; out-of-range or empty text is fixed when you submit or leave the field. Pressing a button while text is pending applies the text first, and does not close the keyboard.
  • QuantityStepperFormField, a FormField<int> with validator, onSaved, autovalidateMode, enabled, and Form.reset support. The error text is shown under the control and outlines it in the error colour.
  • Theming. A QuantityStepperTheme ThemeExtension for the whole app, or a style: on one widget. compact shrinks the painted circles, never the hit areas.
  • Layout. Axis.horizontal or Axis.vertical; custom icons; mirrors correctly in a right-to-left Directionality.
  • Reduced motion. With MediaQuery.disableAnimations the outline transition and the ink splash are switched off.
  • No dependencies besides the Flutter SDK. Null-safe. Everything public has a doc comment.

How it compares

Read on 2026-09-25 from the pub.dev pages, changelogs and GitHub source of the two packages that come up first when you search for a Flutter quantity widget. "Not found" means it is not in the README, the changelog or the source files that were read; it was checked by reading, not by running those packages. If something here is wrong, please open an issue.

quantity_stepper 0.1.0 input_quantity 2.6.0 quantity_input 1.0.2
Status new, not yet published published 2026-04-23 discontinued on pub.dev; repo archived 2024-10-03
Licence MIT MIT BSD-3-Clause
pub.dev likes none yet 76 8
Runtime dependencies none test (listed under dependencies) intl
Value types int int, double, num int, double
Decimals, locale separator, thousands separators no yes yes
Min / max / step yes yes yes
Typing into a text field yes (editable, off by default) yes yes
Custom validation messages yes, through the FormField validator yes (validator, messageBuilder) not found
FormField integration FormField<int>: validator, onSaved, autovalidateMode, reset wraps a TextFormField; has validator; no onSaved or reset in the constructor not found
Long-press repeat yes, accelerating yes, fixed 80 ms interval not found
Keyboard shortcuts (arrows, PageUp/PageDown) yes; opt-in mouse wheel not found not found
Screen-reader semantics (label, value, increase/decrease actions, announcements) yes, verified in widget tests (not on devices, see below) not found not found
48 dp tap targets enforced and tested not enforced (button constraints default to none; 22 px icons) not specified
Right-to-left mirrored, tested not found not found
Theming ThemeExtension plus per-widget style decoration props colour parameters
Tests in the repo yes yes none found
Button layout options horizontal, vertical horizontal, vertical, left/right/classic styles no layout option found

input_quantity does things this package does not: decimals, output types, locale decimal separator, several button-placement styles, and a longer maintained history. If you need decimals, use it. This package is for the case where an integer control has to be operable with a screen reader, a keyboard, and thumbs.

Accessibility

What the widget tests check against the semantics tree (SemanticsHandle, tester.getSemantics, matchesSemantics):

  • The control is one node in reading order (Before, Quantity, After, nothing else). The painted buttons and the number are excluded, so nothing is exposed twice.
  • That node has label (your label), value, increasedValue, decreasedValue, and increase / decrease actions. Running the actions through tester.semantics.increase/decrease changes the value by one step and calls onChanged.
  • At the maximum the node has no increase action and no increasedValue; at the minimum, no decrease action and no decreasedValue.
  • A disabled control (or a controlled one with no onChanged) is flagged disabled and has no actions.
  • semanticFormatter (for example (n) => '$n items') shapes the value, increasedValue, decreasedValue and announcements.
  • The node reports keyboard focus, and the text direction of the surrounding Directionality.
  • announceChanges: true (off by default, only where MediaQuery.supportsAnnounceOf is true) sends one SemanticsService.sendAnnouncement per burst of button, repeat, keyboard or wheel changes, 400 ms after the last change, containing the value. It sends nothing for the screen reader's own increase/decrease actions, and nothing for typing. The tests capture the messages on the accessibility channel to prove this.
  • With editable: true the text field's semantics are merged into the same node: label, one value, text-field flag and the adjust actions. The reading order is still one stop.
  • In QuantityStepperFormField the error text is its own node, a live region where announcements are not supported, the same arrangement Flutter's InputDecorator uses; the enclosing Form announces the first error where they are.
  • Every button and the editable field is at least 48x48 logical pixels, in both axes, compact or not, and a theme cannot make them smaller.

Not verified yet. Hands-on testing with TalkBack and VoiceOver on real devices has not been done, and the example was only built for the web and rendered headlessly. The tests prove what the widget tells Flutter's semantics tree; they cannot prove what a platform screen reader does with it. In particular:

  • That TalkBack and VoiceOver present the node as adjustable and change the value on the platform's increase/decrease gesture. [VERIFY ON DEVICE]
  • That the platform announces the new value itself after an increase/decrease action, which is why the package does not announce those changes a second time. [VERIFY ON DEVICE]
  • How each platform presents the merged node in editable mode (a text field that is also adjustable). [VERIFY ON DEVICE]
  • Behaviour with browser screen readers on Flutter web, and on desktop. [VERIFY ON DEVICE]

Reports from real devices are the most useful contribution you can make: please open an issue with the platform, OS version, screen reader and what was spoken.

API overview

QuantityStepper

Parameter Default Purpose
label required Spoken name of the control (not painted). Must not be empty.
value / initialValue null / 0 Controlled value, or starting value when uncontrolled. Clamped into range.
onChanged null Called with the new value; never with the same value twice in a row.
min / max 0 / null Limits; max: null means no upper limit. min <= max is asserted.
step / largeStep 1 / step * 10 Size of one move / of a PageUp or PageDown move. Both must be positive.
enabled true Disables input, focus and semantic actions.
editable false Show a numeric text field instead of plain text.
autoRepeat, repeatDelay, repeatInterval true, 400 ms, 160 ms Long-press repeat.
announceChanges false Speak changes the screen reader would not speak itself.
enableMouseWheel false Wheel changes the value while focused.
hasError false Error-coloured outline.
compact false Smaller painted circles; targets stay 48x48.
axis Axis.horizontal Axis.vertical puts plus on top.
decrementIcon / incrementIcon Icons.remove / Icons.add Button icons.
semanticFormatter null String Function(int) for what is spoken.
style null A QuantityStepperTheme for this widget.

QuantityStepperFormField takes the same configuration plus validator, onSaved, autovalidateMode, enabled and onChanged. Form.reset restores initialValue (clamped) and calls onChanged with it.

QuantityStepperTheme (ThemeExtension): buttonColor, iconColor, disabledColor, borderColor, focusColor, textStyle, buttonSize, iconSize, valueWidth. Anything left null comes from your ColorScheme and TextTheme.

Known limits of 0.1.0: integers only; typing accepts at most nine digits, and only ASCII digits; use it under a MaterialApp as the tests and the example do (it adds its own transparent Material, but it has not been tried without a MaterialApp).

Contributing

Issues and pull requests are welcome.

flutter pub get
dart format .
flutter analyze
flutter test

Please keep the package free of dependencies, add a widget test for any behaviour you change, and only claim accessibility behaviour that a test (or a documented device test) demonstrates.

Licence

MIT. See LICENSE.

Made by Daniotech — https://daniotech.com

Libraries

quantity_stepper
An accessible quantity stepper for Flutter.