Repository navigation
Description
- I have searched the existing issues to make sure this bug has not already been reported.
Describe the Bug
ComponentCommon gives every component an accessibility object with a label and a description, and conformance/core/accessibility.yaml covers it in four cases. Swift reads them: AccessibilityAttributes, Node.accessibilityAttributes, and AccessibilityConformanceTests runs that suite against the model.
The Dart side drops them on the floor. dart/a2ui_core has no mention of accessibility, and genui never builds a Semantics: grep -rn 'Semantics(' packages/genui/lib comes back empty, and no basic catalog widget passes a semanticLabel either.
What makes it quiet is that the attributes are accepted rather than rejected. The component renders exactly as it should, nothing is logged, and the agent has no way to tell that what it asked to be announced was discarded. A screen reader reads the visible text — Mute — where the agent asked for Mute Notifications, and the description is simply gone.
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';
void main() {
testWidgets('accessibility attributes reach the semantics tree', (tester) async {
final catalog = BasicCatalogItems.asCatalog().copyWith(catalogId: 'repro.catalog');
final controller = SurfaceController(catalogs: [catalog]);
controller.handleMessage(
core.CreateSurfaceMessage(surfaceId: 's', catalogId: 'repro.catalog'),
);
controller.handleMessage(
core.UpdateComponentsMessage(
surfaceId: 's',
components: [
{
'id': 'root',
'component': 'Button',
'child': 'label',
'action': {'event': {'name': 'mute'}},
'accessibility': {
'label': 'Mute Notifications',
'description': 'Silences notifications about this conversation',
},
},
{'id': 'label', 'component': 'Text', 'text': 'Mute'},
],
),
);
await tester.pumpWidget(
MaterialApp(home: Surface(surfaceContext: controller.contextFor('s'))),
);
await tester.pumpAndSettle();
// The component renders: the attributes were accepted, not rejected.
expect(find.text('Mute'), findsOneWidget);
// What the agent asked assistive technology to announce.
expect(find.bySemanticsLabel('Mute Notifications'), findsOneWidget);
});
}The first expectation passes. The second fails:
Expected: exactly one matching candidate
Actual: _ElementPredicateWidgetFinder:<Found 0 widgets with a semantics label
named "Mute Notifications": []>
Which: means none were found but one was expected
The component is a Button here only because it is the clearest case to read; the same holds for every component in the basic catalog.
Expected Behavior
accessibility.label becomes the component's semantics label and accessibility.description its hint, so assistive technology announces what the agent asked for.
Every component is built in one place, Catalog.buildWidget, which already wraps each one in a KeyedSubtree. That is the one spot where this can be handled for the whole catalog, including generated and third-party items, rather than component by component:
return KeyedSubtree(
key: ValueKey(itemContext.id),
child: Semantics(
label: ..., // accessibility.label, resolved
hint: ..., // accessibility.description, resolved
child: item.widgetBuilder(...),
),
);Two things worth deciding there:
- Both fields are
DynamicStringin the spec, so a{"path": ...}binding has to resolve throughBoundStringlike any other property rather than be read raw. - v1.0 adds
liveandhidden, which the suite already covers. They map ontoSemanticsProperties.liveRegionandExcludeSemantics, so it is worth shaping the first version so they fit later.
Environment Details
- OS: macOS 26.5.2
- Browser/Platform: Flutter 3.44.6 stable, Dart 3.10
- SDK/Package Name & Version: genui 0.10.2, a2ui_core 0.1.1
- Protocol Version: v0.9.1
Additional Context
As far as I can see no renderer consumes these attributes yet, so this is not genui falling behind the others — it is the one I can measure. Filing it against genui because that is where the semantics tree is built and where a fix is testable today.
Happy to send the PR if the approach above is the one you want.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
josemontespg commentedon Sep 22, 2026
A2UI Triage: Confirmed.
Catalog.buildWidgetin flutter/genuipackages/genui/lib/src/model/catalog.dartwraps every item in aKeyedSubtreeonly, and nothing underpackages/genui/libbuilds aSemanticsor passes asemanticLabel;accessibilityappears only in the embedded validation schema, so the attributes are validated and then dropped. The web renderers in this repo validate the attribute but do not apply it either, so this is a gap shared by the first-party renderers, tracked here for genui.Marked P2. The single-spot approach in
Catalog.buildWidgetis the right shape. Two things to settle in the PR: the explicit label has to replace the child's inferred label rather than merge with it (the v1.0 spec text requiresaccessibility.labelto override visible text), and both fields need to resolve through the data model like any otherDynamicString.cc @andrewkolos and @gspencergoog to decide if we want to own the PR fix for this, or open it for community contribution
gspencergoog commentedon Sep 23, 2026
Let's ask @diegolopezrm to own the fix for this. (that OK, Diego?)
diegolopezrm commentedon Sep 23, 2026
Yes, I will take it 🫡
diegolopezrm commentedon Sep 23, 2026
Plan I have in mind, in case you'd rather it went another way.
The wrapper in
Catalog.buildWidgetstays. For anything that doesn't build its own semantics boundary it replaces the label instead of prepending it, with a small render object that clears the child labels before they merge up. That covers Text, Card, Image and the rest.The interactive ones can't be fixed from outside, so they take the label themselves: Button, CheckBox, ChoicePicker, Slider, TextField, DateTimeInput and the media controls. While I'm in those files I'd fix #2763 too, since several of them have no name at all right now and it's the same line.
Two PRs: the wrapper first, which is flutter/genui#1035, and the components after.
feat(genui): apply a component's accessibility attributes (flutter#1035)