Use Flutter's Slider for one value and RangeSlider for a start/end interval. Both are controlled widgets: your state owns value or RangeValues, and onChanged updates that state before Flutter rebuilds. Use divisions for discrete steps, labels/value indicators when they help, SliderTheme for consistent visual customization, and semantics/widget tests for accessibility and behavior. This article targets the current Flutter 3.47.2 documentation.
Stateful Slider
class VolumeSlider extends StatefulWidget {
const VolumeSlider({super.key});
@override
State<VolumeSlider> createState() => _VolumeSliderState();
}
class _VolumeSliderState extends State<VolumeSlider> {
double volume = 50;
@override
Widget build(BuildContext context) {
return Slider(
value: volume,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
volume = value;
});
},
);
}
}
Flutter's API states that Slider does not maintain the selected value itself. The callback reports a proposed value; the application decides whether to accept and store it.
Discrete steps and labels
Slider(
value: volume,
min: 0,
max: 100,
divisions: 10,
label: volume.round().toString(),
onChanged: (value) {
setState(() => volume = value);
},
)
With a 0–100 range and 10 divisions, selectable values are spaced by 10. The label provides value-indicator text; theme behavior controls how the indicator is displayed.
Keep value valid when min/max changes
If the allowed range comes from configuration or API state, normalize the stored value when the bounds change. Do not keep an old value outside the newly declared range.
Disabled state
Slider(
value: volume,
min: 0,
max: 100,
onChanged: canEdit
? (value) {
setState(() => volume = value);
}
: null,
)
A null onChanged disables interaction. A range with min == max also has no usable interval.
Use onChangeEnd for expensive persistence
Slider(
value: volume,
min: 0,
max: 100,
onChanged: (value) {
setState(() => volume = value);
},
onChangeEnd: (value) {
saveVolume(value);
},
)
Dragging can produce many onChanged callbacks. Keep immediate UI feedback local, and when the product allows it, defer network/storage work until onChangeEnd.
RangeSlider for one interval
class PriceRange extends StatefulWidget {
const PriceRange({super.key});
@override
State<PriceRange> createState() => _PriceRangeState();
}
class _PriceRangeState extends State<PriceRange> {
RangeValues values = const RangeValues(20, 80);
@override
Widget build(BuildContext context) {
return RangeSlider(
values: values,
min: 0,
max: 100,
divisions: 20,
labels: RangeLabels(
values.start.round().toString(),
values.end.round().toString(),
),
onChanged: (next) {
setState(() => values = next);
},
);
}
}
Use RangeSlider instead of two unrelated sliders when the UI represents one bounded interval.
Accessibility semantics
Slider(
value: volume,
min: 0,
max: 100,
divisions: 10,
semanticFormatterCallback: (value) {
return '${value.round()} percent volume';
},
onChanged: (value) {
setState(() => volume = value);
},
)
A semantic formatter adds domain meaning to a raw number. Also keep a visible nearby label such as “Volume” or “Minimum price”; semantics do not replace understandable visual UI.
Style with SliderTheme
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 6,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 12,
),
),
child: Slider(
value: volume,
min: 0,
max: 100,
onChanged: (value) {
setState(() => volume = value);
},
),
)
Starting from the active theme and using copyWith preserves defaults you are not changing. For app-wide styling, prefer ThemeData.sliderTheme to repeated one-off local settings.
Material 3 and the deprecated year2023 flag
Current Slider/RangeSlider API pages still expose a year2023 compatibility flag, but mark it deprecated. Do not build new long-lived styling around that flag. Use SliderThemeData and check the breaking-change notes for the exact Flutter SDK your app pins.
Desktop/web focus
For desktop and web, test keyboard/focus interaction as well as touch dragging. The widget exposes focus configuration when needed, but prefer normal focus traversal until the product requires custom focus behavior.
Widget test
testWidgets('updates volume when the slider changes', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: VolumeSlider(),
),
),
);
final before = tester.widget<Slider>(find.byType(Slider));
expect(before.value, 50);
before.onChanged?.call(70);
await tester.pump();
final after = tester.widget<Slider>(find.byType(Slider));
expect(after.value, 70);
});
This checks the state transition directly. Add drag, disabled-state, range, persistence, and semantics tests for behaviors your product actually depends on.
Common mistakes
- Changing an external value without rebuilding the slider.
- Leaving state outside a newly changed min/max range.
- Writing to the network on every drag tick when only the final value matters.
- Using two regular sliders for one interval without ordering logic.
- Scattering style constants instead of using a SliderTheme.
- Adding new code around deprecated
year2023. - Showing a number without a meaningful label or units.