Skip to content

[a11y] Add semantics to the Expansible widget. #175907

Description

@ksokolovskyi

Use case

After #174981 lands, we will have onExpand and onCollapse actions available, which will enable expansion and collapse via a11y.

As discussed here:
#174981 (review)
#174981 (comment)

Proposal

Add expanded semantics flag alongside onExpand and onCollapse semantics actions to the Expansible widget.

Activity

  1. added
    a: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)
    team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)
    on Sep 24, 2025
  2. srivats22 commented on Sep 24, 2025

    @srivats22
    Contributor

    Once it lands, can I contribute the changes?

    I read the other PR comments, and my understanding isto update the Expansible widget to support the new onExpand and onCollapse functions?

  3. ksokolovskyi commented on Sep 24, 2025

    @ksokolovskyi
    ContributorAuthor

    Once it lands, can I contribute the changes?

    Hi @srivats22, of course you can contribute to this issue.

    I read the other PR comments, and my understanding isto update the Expansible widget to support the new onExpand and onCollapse functions?

    Mainly yes, but additionally, you will have to verify that the semantics of the CupertinoExpansionTile and ExpansionTile still work as expected on all platforms, and there are no duplicates in the labels and announcements.

  4. added
    P2Important issues not at the top of the work list
    on Sep 24, 2025
  5. srivats22 commented on Oct 8, 2025

    @srivats22
    Contributor

    Hi,
    I am getting started on this issue and wanted to know if what I am doing is correct:

    1. In the following file: packages/flutter/lib/src/widgets/expansible.dart in the build method:
    return AnimatedBuilder(
          animation: _animationController.view,
          builder: (BuildContext context, Widget? child) {
            final Widget header = widget.headerBuilder(context, _animationController);
            final Widget body = ClipRect(
              child: Align(heightFactor: _heightFactor.value, child: child),
            );
            return widget.expansibleBuilder(context, header, body, _animationController);
          },
          child: shouldRemoveBody ? null : result,
        );

    I should wrap it with a Semantic widget and pass the onExpand and onCollapse callback?
    2. Looking at the ExpansionTile, both Material and Cupertino, there is already a Semantic widget. Should I be removing that since it uses the Expansible widget?

  6. ksokolovskyi commented on Nov 5, 2025

    @ksokolovskyi
    ContributorAuthor

    Hi @srivats22, sorry for the late reply.

    1. In general, yes, you need to wrap the AnimatedBuilder with the Semantic widget and set expanded, onExpand, and onCollapse properties according to the expansion state. The expanded/collapsed state is announced only on Android. On iOS, you need to specify the expansion state explicitly in hint.
    2. I am not sure how you need to proceed after the changes to the Expansible widget. What I can suggest is to try removing the semantic properties that define the expansible state in ExpansionTile and then verify how VoiceOver and TalkBack behave when you focus on or use ExpansionTile.

    Please take a look at this PR #176428, which added pretty much the same semantics to the DropdownMenu.

    P.S. I am not an a11y expert. In case you still have questions, you can write them in the #hackers-accessibility chat on Flutter Discord.

  7. srivats22 commented on Nov 5, 2025

    @srivats22
    Contributor

    Thank you for the response @ksokolovskyi I will get started with the AnimatedBuilder and any question will ask on discord.

  8. srivats22 commented on Nov 11, 2025

    @srivats22
    Contributor

    I someone assign this to me please? I have started working on it 😀

  9. srivats22 commented on Nov 17, 2025

    @srivats22
    Contributor
    Screen.Recording.2025-11-17.at.5.17.34.PM.mov

    Hi Can someone confirm if this is how it should be working post the change?
    Changes at a high level:

    1. Expansible.dart
    return Semantics(
          hint: semanticsHint,
          onTapHint: onTapHint,
          expanded: widget.controller.isExpanded,
          onExpand: widget.controller.isExpanded
              ? null
              : () {
                  widget.controller.expand();
                },
          onCollapse: !widget.controller.isExpanded
              ? null
              : () {
                  widget.controller.collapse();
                },
          child: AnimatedBuilder(
            animation: _animationController.view,
            builder: (BuildContext context, Widget? child) {
              final Widget header = widget.headerBuilder(context, _animationController);
              final Widget body = ClipRect(
                child: Align(heightFactor: _heightFactor.value, child: child),
              );
              return widget.expansibleBuilder(context, header, body, _animationController);
            },
            child: shouldRemoveBody ? null : result,
          ),
        );
    1. expansion_tile (Material)
    return MergeSemantics(
          // hint: semanticsHint,
          // onTapHint: onTapHint,
          child: ListTileTheme.merge(
            iconColor: _iconColor.value ?? _expansionTileTheme.iconColor,
            textColor: _headerColor.value,
            child: ListTile(
              enabled: widget.enabled,
              onTap: _tileController.isExpanded ? _tileController.collapse : _tileController.expand,
              dense: widget.dense,
              splashColor: widget.splashColor,
              visualDensity: widget.visualDensity,
              enableFeedback: widget.enableFeedback,
              contentPadding: widget.tilePadding ?? _expansionTileTheme.tilePadding,
              leading: widget.leading ?? _buildLeadingIcon(context, animation),
              title: widget.title,
              subtitle: widget.subtitle,
              trailing: widget.showTrailingIcon
                  ? widget.trailing ?? _buildTrailingIcon(context, animation)
                  : null,
              minTileHeight: widget.minTileHeight,
              internalAddSemanticForOnTap: widget.internalAddSemanticForOnTap,
            ),
          ),
        );
  10. srivats22 commented on Nov 19, 2025

    @srivats22
    Contributor

    I was able to fix it... will try and have the PR out by this week

  11. srivats22 commented on Nov 21, 2025

    @srivats22
    Contributor

    Raised following pr: #178922 as a first attempt... seems right to me but looking forward to PR comment feedbacks 😀

  12. srivats22 commented on Jan 22, 2026

    @srivats22
    Contributor

    Hi @ksokolovskyi

    I have opened the PR for this and got a review comment, see below:

    Image

    So based on that, having the new states on Expansible might not make sense.

    Can you also please weigh in if this is required or not?

    cc: @victorsanni and @chunhtai

  13. victorsanni commented on Feb 4, 2026

    @victorsanni
    Contributor

    So based on that, having the new states on Expansible might not make sense.

    I'm not entirely sure.

    @ksokolovskyi @chunhtai is onCollapse and onExpand callbacks in a Semantics widget required for Android announcements? I saw this PR that suggests setting liveRegion also allows for Android announcements.

    Either way, the end goal is Expansible should be able to trigger a semantics announcement on expansion state changes. The API to do this is SemanticsService.sendAnnouncement on non-Android platforms or liveRegion/onCollapse/onExpand on Android. This work might also involve adding a new string property containing the message to announce etc. But overall these should live in Expansible.

  14. chunhtai commented on Feb 4, 2026

    @chunhtai
    Contributor

    is onCollapse and onExpand callbacks in a Semantics widget required for Android announcements

    I am not sure if i understand the question, can you explain a bit more?

    liveRegion also allows for Android announcements.

    this is the only way to trigger android announcement now, we are deprecating send announcement support for android.

    Otherwise, agree with victor, these accessibility property should live in Expansible.

  15. added
    ⏳Bot is counting down the days until it unassigns the issue
    on Jun 10, 2026
  16. flutter-triage-bot commented on Jun 10, 2026

    @flutter-triage-bot

    This issue is assigned to @srivats22 but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!

  17. flutter-triage-bot commented on Aug 26, 2026

    @flutter-triage-bot

    This issue was assigned to @srivats22 but has had no status updates in a long time. To remove any ambiguity about whether the issue is being worked on, the assignee was removed.

  18. removed
    ⏳Bot is counting down the days until it unassigns the issue
    on Aug 26, 2026
  19. srivats22 commented on Aug 27, 2026

    @srivats22
    Contributor

    Hi,

    I am currently busy with other work items in my day to day job and not sure when I will be able to get back to working on this. If someone else wants to pick this up they can.

    Thanks

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)team-accessibilityOwned by Framework Accessibility team (i.e. responsible for accessibility code in flutter/flutter)triaged-accessibilityTriaged by Framework Accessibility team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions