Skip to content

[BUG]: genui drops AccessibilityAttributes: label and description never reach the semantics tree #2697

Closed
flutter/genui
#1035

Description

@diegolopezrm
  • 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 DynamicString in the spec, so a {"path": ...} binding has to resolve through BoundString like any other property rather than be read raw.
  • v1.0 adds live and hidden, which the suite already covers. They map onto SemanticsProperties.liveRegion and ExcludeSemantics, 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.

Activity

  1. added
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Sep 18, 2026
  2. moved this from Todo to In Progress in A2UIon Sep 18, 2026
  3. josemontespg commented on Sep 22, 2026

    @josemontespg
    Collaborator

    A2UI Triage: Confirmed. Catalog.buildWidget in flutter/genui packages/genui/lib/src/model/catalog.dart wraps every item in a KeyedSubtree only, and nothing under packages/genui/lib builds a Semantics or passes a semanticLabel; accessibility appears 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.buildWidget is 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 requires accessibility.label to override visible text), and both fields need to resolve through the data model like any other DynamicString.

    cc @andrewkolos and @gspencergoog to decide if we want to own the PR fix for this, or open it for community contribution

  4. removed
    status: needs-triageauto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs
    on Sep 22, 2026
  5. gspencergoog commented on Sep 23, 2026

    @gspencergoog
    Collaborator

    Let's ask @diegolopezrm to own the fix for this. (that OK, Diego?)

  6. diegolopezrm commented on Sep 23, 2026

    @diegolopezrm
    ContributorAuthor

    Let's ask @diegolopezrm to own the fix for this. (that OK, Diego?)

    Yes, I will take it 🫡

  7. diegolopezrm commented on Sep 23, 2026

    @diegolopezrm
    ContributorAuthor

    Plan I have in mind, in case you'd rather it went another way.

    The wrapper in Catalog.buildWidget stays. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions