Skip to content
Boring Doc
BreakingHigh impactOfficial migration guidesince 1.20.0landed 1.19.0

Semantics Order of the Overlay Entries in Modal Routes

Framework · Official source

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.
APIs mentioned:ModalRouteOverlayEntry

Before and after (from the official guide)

dart+0−0
Before
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 }
After
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;
}
Migration assistantAI-generated · grounded in cited sources
AI explanations are not configured
Set 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.
Official migration guide·Verified classification (95%)

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 explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set 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.
Was this useful?