quantity_stepper 0.1.0
quantity_stepper: ^0.1.0 copied to clipboard
Accessible quantity stepper for Flutter: screen-reader adjust actions, 48dp targets, long-press repeat, keyboard support, a real FormField. No dependencies.
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.
![]()
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,maxandstep, clamped everywhere: the initial value, taps, keys, typing and screen-reader actions. - Controlled or uncontrolled. Pass
valueandonChangedto own the state, orinitialValueto let the widget keep it. - Long-press repeat with acceleration. After
repeatDelay(400 ms) the value moves once, then everyrepeatInterval(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: falseturns it off. - Keyboard. The control is one Tab stop. ArrowUp/ArrowDown move one
step, PageUp/PageDown movelargeStep(defaultstep * 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 whenminis 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, aFormField<int>withvalidator,onSaved,autovalidateMode,enabled, andForm.resetsupport. The error text is shown under the control and outlines it in the error colour.- Theming. A
QuantityStepperThemeThemeExtensionfor the whole app, or astyle:on one widget.compactshrinks the painted circles, never the hit areas. - Layout.
Axis.horizontalorAxis.vertical; custom icons; mirrors correctly in a right-to-leftDirectionality. - Reduced motion. With
MediaQuery.disableAnimationsthe 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(yourlabel),value,increasedValue,decreasedValue, andincrease/decreaseactions. Running the actions throughtester.semantics.increase/decreasechanges the value by onestepand callsonChanged. - At the maximum the node has no
increaseaction and noincreasedValue; at the minimum, nodecreaseaction and nodecreasedValue. - 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,decreasedValueand announcements.- The node reports keyboard focus, and the text direction of the surrounding
Directionality. announceChanges: true(off by default, only whereMediaQuery.supportsAnnounceOfis true) sends oneSemanticsService.sendAnnouncementper 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: truethe 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
QuantityStepperFormFieldthe error text is its own node, a live region where announcements are not supported, the same arrangement Flutter'sInputDecoratoruses; the enclosingFormannounces 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
