We changed the semantics traverse order of the overlay entries in modal routes. Accessibility talk back or voice over now focuses the scope of a modal route first instead of its modal barrier.
Original behavior
The modal route has two overlay entries, the scope and the modal barrier. The scope is the actual content of the modal route, and the modal barrier is the background of the route if its scope does not cover the entire screen. If the modal route returns true for `barrierDismissible`, the modal barrier becomes accessibility focusable because users can tap the modal barrier to pop the modal route. This change specifically made the accessibility to focus the scope first before the modal barrier.
New behavior
We added additional semantics node above both the overlay entries of modal routes. Those semantics nodes denote the semantics traverse order of these two overlay entries. This also changed the structure of semantics tree.
- Who is affected
- Applications and packages relying on the previous behaviour or signature. Code that uses ModalRoute, OverlayEntry.
- Practical impact
- Behaviour or API differences that can break builds or change runtime behaviour.
- What action should I take?
- Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.
Before and after (from the official guide)
| 1 | import 'dart:ui'; | |
| 2 | ||
| 3 | import 'package:flutter_test/flutter_test.dart'; | |
| 4 | import 'package:flutter/rendering.dart'; | |
| 5 | import 'package:material_ui/material_ui.dart'; | |
| 6 | ||
| 7 | void main() { | |
| 8 | testWidgets('example test', (WidgetTester tester) async { | |
| 9 | final SemanticsHandle handle = | |
| 10 | tester.binding.pipelineOwner.ensureSemantics(); | |
| 11 | ||
| 12 | // Build our app and trigger a frame. | |
| 13 | await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text('test')))); | |
| 14 | ||
| 15 | final SemanticsNode root = | |
| 16 | tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; | |
| 17 | ||
| 18 | final SemanticsNode firstNode = getChild(root); | |
| 19 | expect(firstNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)); | |
| 20 | ||
| 21 | // Fixes the test by expecting an additional node above the scope route. | |
| 22 | final SemanticsNode secondNode = getChild(firstNode); | |
| 23 | expect(secondNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)); | |
| 24 | ||
| 25 | final SemanticsNode thirdNode = getChild(secondNode); | |
| 26 | expect(thirdNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)); | |
| 27 | expect(thirdNode.hasFlag(SemanticsFlag.scopesRoute), true); | |
| 28 | ||
| 29 | final SemanticsNode forthNode = getChild(thirdNode); | |
| 30 | expect(forthNode.rect, Rect.fromLTRB(0.0, 0.0, 56.0, 14.0)); | |
| 31 | expect(forthNode.label, 'test'); | |
| 32 | handle.dispose(); | |
| 33 | }); | |
| 34 | } | |
| 35 | ||
| 36 | SemanticsNode getChild(SemanticsNode node) { | |
| 37 | SemanticsNode child; | |
| 38 | bool visitor(SemanticsNode target) { | |
| 39 | child = target; | |
| 40 | return false; | |
| 41 | } | |
| 42 | ||
| 43 | node.visitChildren(visitor); | |
| 44 | return child; | |
| 45 | } |
| 1 | import 'dart:ui'; | |
| 2 | ||
| 3 | import 'package:flutter_test/flutter_test.dart'; | |
| 4 | import 'package:flutter/rendering.dart'; | |
| 5 | import 'package:material_ui/material_ui.dart'; | |
| 6 | ||
| 7 | void main() { | |
| 8 | testWidgets('example test', (WidgetTester tester) async { | |
| 9 | final SemanticsHandle handle = | |
| 10 | tester.binding.pipelineOwner.ensureSemantics(); | |
| 11 | ||
| 12 | // Build our app and trigger a frame. | |
| 13 | await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text('test')))); | |
| 14 | ||
| 15 | final SemanticsNode root = | |
| 16 | tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; | |
| 17 | ||
| 18 | final SemanticsNode firstNode = getChild(root); | |
| 19 | expect(firstNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)); | |
| 20 | ||
| 21 | // Fixes the test by expecting an additional node above the scope route. | |
| 22 | final SemanticsNode secondNode = getChild(firstNode); | |
| 23 | expect(secondNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)); | |
| 24 | ||
| 25 | final SemanticsNode thirdNode = getChild(secondNode); | |
| 26 | expect(thirdNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)); | |
| 27 | expect(thirdNode.hasFlag(SemanticsFlag.scopesRoute), true); | |
| 28 | ||
| 29 | final SemanticsNode forthNode = getChild(thirdNode); | |
| 30 | expect(forthNode.rect, Rect.fromLTRB(0.0, 0.0, 56.0, 14.0)); | |
| 31 | expect(forthNode.label, 'test'); | |
| 32 | handle.dispose(); | |
| 33 | }); | |
| 34 | } | |
| 35 | ||
| 36 | SemanticsNode getChild(SemanticsNode node) { | |
| 37 | SemanticsNode child; | |
| 38 | bool visitor(SemanticsNode target) { | |
| 39 | child = target; | |
| 40 | return false; | |
| 41 | } | |
| 42 | ||
| 43 | node.visitChildren(visitor); | |
| 44 | return child; | |
| 45 | } |
Raw before / after
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets('example test', (WidgetTester tester) async {
final SemanticsHandle handle =
tester.binding.pipelineOwner.ensureSemantics();
// Build our app and trigger a frame.
await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text('test'))));
final SemanticsNode root =
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final SemanticsNode firstNode = getChild(root);
expect(firstNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
// Fixes the test by expecting an additional node above the scope route.
final SemanticsNode secondNode = getChild(firstNode);
expect(secondNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
final SemanticsNode thirdNode = getChild(secondNode);
expect(thirdNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
expect(thirdNode.hasFlag(SemanticsFlag.scopesRoute), true);
final SemanticsNode forthNode = getChild(thirdNode);
expect(forthNode.rect, Rect.fromLTRB(0.0, 0.0, 56.0, 14.0));
expect(forthNode.label, 'test');
handle.dispose();
});
}
SemanticsNode getChild(SemanticsNode node) {
SemanticsNode child;
bool visitor(SemanticsNode target) {
child = target;
return false;
}
node.visitChildren(visitor);
return child;
}import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets('example test', (WidgetTester tester) async {
final SemanticsHandle handle =
tester.binding.pipelineOwner.ensureSemantics();
// Build our app and trigger a frame.
await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text('test'))));
final SemanticsNode root =
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final SemanticsNode firstNode = getChild(root);
expect(firstNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
// Fixes the test by expecting an additional node above the scope route.
final SemanticsNode secondNode = getChild(firstNode);
expect(secondNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
final SemanticsNode thirdNode = getChild(secondNode);
expect(thirdNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
expect(thirdNode.hasFlag(SemanticsFlag.scopesRoute), true);
final SemanticsNode forthNode = getChild(thirdNode);
expect(forthNode.rect, Rect.fromLTRB(0.0, 0.0, 56.0, 14.0));
expect(forthNode.label, 'test');
handle.dispose();
});
}
SemanticsNode getChild(SemanticsNode node) {
SemanticsNode child;
bool visitor(SemanticsNode target) {
child = target;
return false;
}
node.visitChildren(visitor);
return child;
}Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)
If your tests start failing due to semantics tree changes after the update, you can migrate your code by expecting a new node on above of the modal route overlay entries.
Code before migration:
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets('example test', (WidgetTester tester) async {
final SemanticsHandle handle =
tester.binding.pipelineOwner.ensureSemantics();
// Build our app and trigger a frame.
await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text('test'))));
final SemanticsNode root =
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final SemanticsNode firstNode = getChild(root);
expect(firstNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
// Fixes the test by expecting an additional node above the scope route.
final SemanticsNode secondNode = getChild(firstNode);
expect(secondNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
final SemanticsNode thirdNode = getChild(secondNode);
expect(thirdNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
expect(thirdNode.hasFlag(SemanticsFlag.scopesRoute), true);
final SemanticsNode forthNode = getChild(thirdNode);
expect(forthNode.rect, Rect.fromLTRB(0.0, 0.0, 56.0, 14.0));
expect(forthNode.label, 'test');
handle.dispose();
});
}
SemanticsNode getChild(SemanticsNode node) {
SemanticsNode child;
bool visitor(SemanticsNode target) {
child = target;
return false;
}
node.visitChildren(visitor);
return child;
}Code after migration:
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:material_ui/material_ui.dart';
void main() {
testWidgets('example test', (WidgetTester tester) async {
final SemanticsHandle handle =
tester.binding.pipelineOwner.ensureSemantics();
// Build our app and trigger a frame.
await tester.pumpWidget(MaterialApp(home: Scaffold(body: Text('test'))));
final SemanticsNode root =
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final SemanticsNode firstNode = getChild(root);
expect(firstNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
// Fixes the test by expecting an additional node above the scope route.
final SemanticsNode secondNode = getChild(firstNode);
expect(secondNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
final SemanticsNode thirdNode = getChild(secondNode);
expect(thirdNode.rect, Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
expect(thirdNode.hasFlag(SemanticsFlag.scopesRoute), true);
final SemanticsNode forthNode = getChild(thirdNode);
expect(forthNode.rect, Rect.fromLTRB(0.0, 0.0, 56.0, 14.0));
expect(forthNode.label, 'test');
handle.dispose();
});
}
SemanticsNode getChild(SemanticsNode node) {
SemanticsNode child;
bool visitor(SemanticsNode target) {
child = target;
return false;
}
node.visitChildren(visitor);
return child;
}AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).
An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.
- Source commit
- b3045439b7c0
- Source last updated
- Oct 21, 2026
- Last synchronized
- Oct 21, 2026
- Publication status
- published
Publication history
- Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.