Stepper
GlassStepper is a minus and a plus in one glass capsule, as UIStepper: a held half repeats, the end at a limit is disabled, and a press costs no capture.
GlassStepper · kGlassStepperSize · kGlassStepperRepeatDelay · kGlassStepperRepeatInterval · Source
GlassStepper is iOS's UIStepper on glass: one capsule split by a hairline into a minus and a plus. A press steps the
value at once; held, the half repeats, half a second after the press and then ten times a second, until the finger lifts
or the value reaches a limit. At a limit the half that would pass it is disabled and its glyph dims, unless the stepper
wraps.
The stepper shows no number. As on iOS, the value goes in a label beside it, which you rebuild from onChanged.
When to use
- A small count changed one step at a time: copies to print, guests, a quantity in a cart, a font size.
- Not for a wide range, where tapping a hundred times is no way to get there. Use a slider,
with
divisionsif the value is whole. - Not as two GlassButtons side by side. Two buttons are two surfaces; the stepper is one, and draws the divider and the pressed half inside it.
Usage
Row(
children: <Widget>[
Expanded(child: Text('Copies: $copies')),
GlassStepper(
value: copies.toDouble(),
min: 1,
max: 10,
semanticLabel: 'Copies',
onChanged: (double v) => setState(() => copies = v.round()),
),
],
)
The value is a double, so a fractional step works too: step: 0.5 for a font size. onChanged: null disables both
halves.
Behaviour
autorepeat(on by default) repeats a held half after kGlassStepperRepeatDelay (500 ms), every kGlassStepperRepeatInterval (100 ms). The repeat stops at a limit.wrapsgoes frommaxtominand back instead of stopping, and keeps both halves enabled.- A held half brightens its own cell by the finish's rim colour, clipped to the capsule, as a
toolbar cell does.
pressedOverlayreplaces that colour.
Size
The capsule is kGlassStepperSize,
94 × 32: UIStepper's width and the segmented control's track. The control is laid
out 44 tall around it, so a tap a little above or below still lands. Layout, not a reading: the iOS 26 stepper was
not among the controls measured for the package.
Cost
One surface, whatever is held. A press, the repeat and a glyph dimming at a limit are drawn inside the glass, so none of them is a capture. Keep the label you change beside the stepper on glass too, as the demo does on a card, and a press repaints nothing under any glass: the count under the stage stays where it was.
Accessibility
A screen reader hears one adjustable control: its semanticLabel and the value, which it increases or decreases by
step. semanticFormatterCallback says the value your way, "12.5 points" rather than "12.5".
A focused stepper steps with the arrow keys: up and the arrow toward the plus increase, down and the other decrease. The
focus ring is drawn inside the glass, so it takes no capture, and a disabled stepper takes no focus. Under a
right-to-left Directionality the stepper is mirrored, with the minus at the end, and so are the left and right arrows.
Code
import 'package:flutter/material.dart';
import 'package:g1455/g1455.dart';
/// A print dialog's options on a glass card.
/// Assumes a GlassHost above the navigator (MaterialApp.builder).
class PrintOptions extends StatefulWidget {
const PrintOptions({super.key});
@override
State<PrintOptions> createState() => _PrintOptionsState();
}
class _PrintOptionsState extends State<PrintOptions> {
int _copies = 1;
double _scale = 100;
@override
Widget build(BuildContext context) => SizedBox(
width: 340,
child: GlassCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(child: Text('Copies: $_copies')),
GlassStepper(
value: _copies.toDouble(),
min: 1,
max: 99,
semanticLabel: 'Copies',
onChanged: (double v) => setState(() => _copies = v.round()),
),
],
),
Row(
children: <Widget>[
Expanded(child: Text('Scale: ${_scale.round()}%')),
GlassStepper(
value: _scale,
min: 25,
max: 400,
step: 25,
semanticLabel: 'Scale',
semanticFormatterCallback: (double v) => '${v.round()} percent',
onChanged: (double v) => setState(() => _scale = v),
),
],
),
],
),
),
);
}API
| Parameter | Type | Default | Description |
|---|---|---|---|
value |
double |
required | The value shown. The stepper holds none of its own. |
onChanged |
ValueChanged<double>? |
required | Called with the new value. Null disables both halves. |
min |
double |
0 |
The lowest value. At most max. |
max |
double |
100 |
The highest value. |
step |
double |
1 |
How far one press moves the value. Above 0. |
wraps |
bool |
false |
Past max goes to min and back, instead of stopping. |
autorepeat |
bool |
true |
A held half repeats until the finger lifts or a limit is reached. |
finish |
GlassFinish? |
null |
The glass. Null takes the theme's. |
pressedOverlay |
Color? |
null |
Added over the held half. Null takes the finish's rim colour. |
semanticLabel |
String? |
null |
What a screen reader calls the control. |
semanticFormatterCallback |
String Function(double)? |
null |
How a screen reader says the value. |
focusNode |
FocusNode? |
null |
The stepper's focus. Null makes one the stepper owns. |
autofocus |
bool |
false |
Take the focus as soon as the stepper is built. |
key |
Key? |
null |
Constants
| Name | Value | Description |
|---|---|---|
kGlassStepperSize |
Size(94, 32) |
The capsule. Laid out at least 44 tall. Layout, not a reading. |
kGlassStepperRepeatDelay |
Duration(milliseconds: 500) |
How long a held half waits before it repeats. |
kGlassStepperRepeatInterval |
Duration(milliseconds: 100) |
How often it repeats after that. |