Repository navigation
Description
- I have searched the existing issues to make sure this bug has not already been reported.
Describe the Bug
slider.dart derives the number of stops from the range itself:
divisions: (sliderData.max - sliderData.min).toInt(),The schema documents min as defaulting to 0.0 and max to 1.0. That range gives divisions: 1, which means the thumb can only rest at the two ends. Every value between them snaps, and a 0.5 is announced by assistive technology as 100%. A brightness, volume or opacity slider built on the documented defaults is a switch.
A range narrower than 1 is worse: min: 0, max: 0.5 gives divisions: 0 and Flutter asserts.
'package:flutter/src/material/slider.dart': Failed assertion: line 209 pos 15:
'divisions == null || divisions > 0': is not true.
So an agent can take the surface down with a message the catalog schema accepts.
Two smaller things in the same widget:
- The number shown next to the thumb is
value?.toStringAsFixed(0), read from the data model only. With a literalvalueit shows the minimum instead, andtoStringAsFixed(0)means a 0-to-1 slider can only ever read0or1. - Because of the snapping, the value the user leaves behind is not the value the agent sees: the write goes through
onChanged, so the data model receives the snapped number.
Steps to Reproduce
import 'package:a2ui_core/a2ui_core.dart' as core;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:genui/genui.dart';
Future<SurfaceController> show(
WidgetTester tester, List<Map<String, dynamic>> components) async {
final catalog = BasicCatalogItems.asCatalog().copyWith(catalogId: 'repro');
final controller = SurfaceController(catalogs: [catalog]);
controller.handleMessage(
core.UpdateComponentsMessage(surfaceId: 's', components: components));
controller.handleMessage(
core.CreateSurfaceMessage(surfaceId: 's', catalogId: 'repro'));
await tester.pumpWidget(MaterialApp(
home: Scaffold(body: Surface(surfaceContext: controller.contextFor('s')))));
await tester.pumpAndSettle();
return controller;
}
void main() {
testWidgets('the default range is not a two-position switch', (tester) async {
await show(tester, [
{'id': 'root', 'component': 'Slider', 'value': 0.5, 'min': 0, 'max': 1},
]);
expect(tester.widget<Slider>(find.byType(Slider)).divisions, isNot(1));
});
testWidgets('a sub-unit range does not crash', (tester) async {
await show(tester, [
{'id': 'root', 'component': 'Slider', 'value': 0.2, 'min': 0, 'max': 0.5},
]);
expect(tester.takeException(), isNull);
});
}The first fails with Expected: not <1> / Actual: <1>; the second with the assertion above.
Expected Behavior
A slider is continuous unless something says otherwise: divisions: null by default, and a declared step — a step property on the component, or divisions derived from it — when the catalog wants discrete stops. Whatever the rule ends up being, it cannot produce 0, and it should not turn the documented default range into two positions.
Environment Details
- OS: macOS 26.5.2
- Browser/Platform: Flutter 3.44.6 stable, Dart 3.10
- SDK/Package Name & Version: genui 0.10.3 (
main, d88aa76), a2ui_core 0.1.1 - Protocol Version: v0.9.1
Additional Context
Found while prototyping a rendering conformance suite that states what a surface must expose as semantics — role, name, value, state, actions — so the same cases can run against Flutter, SwiftUI, Compose and the web. The case declared value: "0.5" and this renderer announced 100%, which is what led back to the divisions.
Worth saying that a golden or screenshot test would not have caught this one: the slider looks right. What is wrong is where the thumb is allowed to stop and what it tells a screen reader.
Happy to send the PR.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
diegolopezrm commentedon Sep 22, 2026
The prototype this came out of is written up in #2738, in case the shape of the suite is worth discussing separately from the fix.
josemontespg commentedon Sep 22, 2026
A2UI Triage: Confirmed on genui main (d88aa76):
slider.dartderivesdivisionsfrommax - min, so the documented default range renders two stops and a range narrower than 1 trips Flutter'sdivisions > 0assertion in debug and profile builds. The spec matches your expectation: v0.9.1 has no snapping and v1.0 snaps only whensteps(integer, minimum 1) is set. The spec's own music player example (max: 1) is affected.Marked P2 and assigned to @gspencergoog. A PR is welcome; @gspencergoog, please say here whether you want to take it or review one from @diegolopezrm, since the fix also decides how v1.0
stepsmaps todivisions(#1959).fix: make Slider continuous by default and support steps
fix: make Slider continuous by default and support steps
fix: make Slider continuous by default (#1039)
Add commit history to RESUME.cs.md (#2)
Rename type values in RESUME.cs.md to hyphenated form (#3)
Update RESUME metadata and repository type (#4)