Repository navigation
Description
Steps to reproduce
Steps to reproduce
On web only, in an app that mounts an Overlay (or a Navigator, which owns
one) above the root Navigator in response to an async state change, where the
mounted subtree takes focus:
- Build a Flutter web app (CanvasKit) whose root builder conditionally wraps its
child, e.g.MaterialApp.builder: (context, child) => Barrier(child: child!). Barrierreturnschildverbatim until an async provider resolves, then
returnsStack(children: [child, Positioned.fill(child: Overlay(initialEntries: [...]))]).- The overlay entry renders normal Material widgets (
Material→InkWell/
MenuAnchor— anything that installs a focus node). - Load the page in Chrome or Safari with the barrier condition true.
I could not reduce this to a testWidgets case: FocusManager._markNeedsUpdate
defers through scheduleMicrotask (focus_manager.dart:1947), and the window
only opens because the web engine services requestViewFocusChange
synchronously. Firing WidgetsBinding.instance.handleViewFocusChanged by hand
— including from inside build — does not reproduce it. It reproduces reliably
in a real browser.
Deterministic in-browser measurement (release and profile builds, Chrome 151,
ANGLE Metal, macOS): with the barrier mounted, exactly one throw per cold
load; with the barrier's condition false so nothing is mounted, zero. 3
runs each.
Expected results
No exception. Focus traversal should skip, or defer measuring, a render object
that has not been laid out yet.
Actual results
Bad state: RenderBox was not laid out: _RenderTheater#6abe2
Stack from a profile build (names intact), innermost last:
_scopeFocusChangeListener$0 widgets/view.dart:227
changeViewFocus$2
_handleFocusChange$1
invokeOnViewFocusChange$1
handleViewFocusChanged$1 widgets/binding.dart
→ FlutterError_dumpErrorToConsole
and, from a release build of the same app, the throwing half:
RenderBox.size rendering/box.dart:2304
RenderBox.semanticBounds
FocusNode.rect widgets/focus_manager.dart:861
_ReadingOrderSortData widgets/focus_traversal.dart:1399
ReadingOrderTraversalPolicy._findInitialFocus / .findFirstFocus
_ViewState.didChangeViewFocus widgets/view.dart:249
WidgetsBinding.handleViewFocusChanged
Code sample
Mechanism
_ViewState.didChangeViewFocus answers a ViewFocusEvent by running
findFirstFocus over the view scope (view.dart:254).
ReadingOrderTraversalPolicy sorts by geometry, so _ReadingOrderSortData reads
FocusNode.rect for every member (focus_traversal.dart:1398-1399), and
FocusNode.rect goes straight to context!.findRenderObject()!.semanticBounds
(focus_manager.dart:861-873). If any of those render objects has not been laid
out yet — which is the case for an Overlay's _RenderTheater mounted in the
current frame — RenderBox.size throws.
Only the web engine emits ViewFocusEvents, and it services
requestViewFocusChange synchronously, which is why this is web-only: on
iOS/Android didChangeViewFocus is never reached.
Navigator makes the shape especially easy to hit: it wraps itself in a
FocusTraversalGroup (navigator.dart:5975) with no render object in between,
so the group's focus node resolves directly to its Overlay's _RenderTheater.
Why an app cannot work around it
Three independent doors are shut. All verified in 3.47.0 source:
-
The policy cannot be replaced.
_ViewStatehardcodes it —
view.dart:209:final FocusTraversalPolicy _policy = ReadingOrderTraversalPolicy();
Wrapping the app in
FocusTraversalGroup(policy: WidgetOrderTraversalPolicy())
does not affect this call. -
Group nodes are exempt from every focusability filter. In
_findGroups
(focus_traversal.dart:452), thenode == groupNodebranch appends and
continues before the "Skip non-focusable and non-traversable nodes" check
further down. SoExcludeFocus,canRequestFocus: falseandskipTraversal
cannot keep a group node out of the sort. -
FocusNode.recthas nohasSizeguard. It asserts oncontext != null
only (focus_manager.dart:862-867) and then measures unconditionally.
Three app-side attempts, all measured against the repro, all failed:
- a stable
GlobalKeyon the router subtree — no effect (and unnecessary:
go_routeralready pins the rootNavigatorwith aGlobalKey); - replacing the mid-session
Navigatorwith a bareOverlay— removed the
FocusTraversalGroup-over-_RenderTheatershape, error unchanged; - keeping that
Overlaymounted from the first frame and swapping only its
entry content — no effect, which rules out mount timing: the trigger is
the mounted content taking focus, not when the theater appeared.
Severity
Lower than it first looks, and worth stating plainly: the top frame is
FlutterError.dumpErrorToConsole, so the framework catches and reports it. The
frame is not aborted and rendering continues. It is persistent console noise
on every affected load, not a visual failure — I originally suspected it of
blanking sections of the UI and that turned out to be an unrelated bug.
Suggested fixes (in preference order)
- Guard the read:
FocusNode.rectreturnsRect.zero(or the node is skipped)
when the render object!hasSize. Cheapest, and geometry sorting has no
meaningful answer for an unlaid-out box anyway. - Have
_ReadingOrderSortData/_findGroupsskip members whose render object
is not laid out. - Let
_ViewState's policy be overridable, so an app can choose
WidgetOrderTraversalPolicyand avoid geometry reads entirely.
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
[✓] Flutter (Channel stable, 3.47.0, on macOS 27.0 26A5416b darwin-arm64, locale en-US) [392ms]
• Flutter version 3.47.0 on channel stable at /opt/homebrew/share/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 4cf2416 (10 days ago), 2026-08-11 11:53:49 -0700
• Engine revision 5f77625
• Dart version 3.13.0
• DevTools version 2.60.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager,
omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,647ms]
• Android SDK at /Users/vs/Library/Android/sdk
• Emulator version unknown
• Platform android-36, build-tools 36.0.0
• Java binary at: /opt/homebrew/opt/openjdk@17/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: flutter config --jdk-dir="path/to/jdk".
• Java version OpenJDK Runtime Environment Homebrew (build 17.0.20.1+0)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[!] Xcode - develop for iOS and macOS (Xcode 27.0) [1,875ms]
• Xcode at /Applications/Xcode-beta.app/Contents/Developer
• Build 27A5237l
✗ Unable to get list of installed Simulator runtimes.
! CocoaPods not installed.
CocoaPods is a package manager for iOS or macOS platform code.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/to/platform-plugins
For installation instructions, see https://guides.cocoapods.org/using/getting-started.html#installation
[✓] Chrome - develop for the web [88ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [5.9s]
• macOS (desktop) • macos • darwin-arm64 • macOS 27.0 26A5416b darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.174
! Unexpected failure parsing device information from adb output:
adb-41251JEHN18762-8RTCj9._adb-tls-connect._tcp device product:lynx_beta model:Pixel_7a device:lynx transport_id:2
Please report a bug at https://github.com/flutter/flutter/issues.
[✓] Network resources [452ms]
• All expected network resources are available.
Activity
suryaKommana2662 commentedon Aug 24, 2026
@grupoclip Thanks for the detailed investigation. Could you please provide a minimal runnable reproduction (
main.dart) or a sample repository that demonstrates the issue? This will help us reproduce and investigate theRenderBox was not laid out: _RenderTheatererror in a browser environment.Guard FocusNode.rect/offset/size against an unlaid-out RenderBox
SleepyFoxP1N commentedon Aug 26, 2026
Independent confirmation from a different production app, plus one data point that
may narrow the mechanism. Not a reproduction, sorry - we could not build one either,
and for the same reason you describe.
Environment. Flutter web, CanvasKit, engine revision
4c525dac5ebe5971c5708ef73558ed8edcf4a362(read from the deployedflutter_bootstrap.js). Seen in release builds on Chrome 150/151 and onMobile Safari 26.3 / iOS 18.7, so it is not Chrome-specific.
Same signal. Every event carries
flutter_error_details.context = "thrown while dispatching notifications for WidgetsBindingObserver.didChangeViewFocus", and we override that observernowhere - it is the framework's own
_ViewState.The part that may be useful: it is not only
_RenderTheater. We have fiveseparate Sentry groups for this. One reports
_RenderTheater, matching yourreport; the other four report minified class names. I want to be careful here -
minified identifiers are not stable across builds, and those four span several
releases, so I cannot claim they are four distinct classes. What I can say is
that at least one throwing render object is not
_RenderTheater, on routeswhere we mount no
Overlayat all (below), so the failing measurement does notlook specific to a just-mounted
Overlay.Consistent with that, our trigger is not an
Overlayat all. OurMaterialApp.builderwrapschildin a chain of gates, and one of them watchesan async provider and restructures once it resolves - but it composes with a
Column, not aStack/Overlay. The shape that matters therefore seems to be"a root-level builder wrapper that changes structure when an async provider
resolves during cold load, and the new subtree installs a focus node", with the
Overlaybeing one instance of it rather than the cause.Where it happens for us: only ever on a cold load, and only on our public /
unauthenticated entry routes (
/, a gate route, two legal pages, a token route,a public profile route). Never mid-session.
Impact, for triage weighting: the binding catches it, so the app keeps
running - the user-visible effect is a lost initial focus, not a crash or a blank
page. It is noisy rather than fatal for us. Some of our volume is a crawler, but
a clear majority is real Chrome and Mobile Safari traffic.
On the reproduction request - independently of this thread we tried and
failed the same way:
FocusManager._markNeedsUpdatedefers throughscheduleMicrotask, so drivinghandleViewFocusChangedby hand (including frominside
build) never reproduces it, and any fixture that forces a permanentlyunlaid-out subtree trips a different assertion instead. The production condition
appears to be genuinely transient - the subtree is mid-layout, not never-laid-out
testWidgetsharness will not give you.Happy to pull more specifics from our telemetry if any particular tag or context
would help.
Tim0theus commentedon Sep 3, 2026
Third independent confirmation, and this one comes with a minimal repro (24 lines, no third-party packages), plus a working app-side workaround. Hopefully that unblocks the "waiting for response" state.
Environment: Flutter 3.47.0 stable, Dart 3.13.0, macOS 26.6.2 (darwin-arm64), Chrome 152.0.7977.66, debug build via
flutter run -d chrome(DDC).Minimal repro
flutter create --platforms=web -e repro, then:flutter run -d chromethrows on startup, every time:Note this needs no async gate and no mid-startup subtree swap — a plain
MaterialAppwith aSelectionAreain itsbuilderis enough. The focus request that starts the chain is just the initial route's own scope.Why
RenderStackand not_RenderTheaterThis thread already notes that the render object is not always
_RenderTheater. In this repro it is deterministic:SelectableRegionputs its focus node directly above its child inFocus.withExternalFocusNode(selectable_region.dart:1983).PlatformSelectableRegionContextMenu, whosebuildreturns aStack(_platform_selectable_region_context_menu_web.dart:146).So
FocusNode.recton the SelectableRegion node resolves to thatRenderStack, and it is read before the first layout. Any focusable node whose nearest descendant render object happens to be unlaid-out at that moment will do — which is why the reported type differs per app.Variants tried
SelectionAreainMaterialApp.builder(above the Navigator), as aboveSelectionArea(focusNode: FocusNode(skipTraversal: true), ...)MaterialApp(home: SelectionArea(child: Scaffold(...)))(below the Navigator)The last row may help narrow it: the node has to be mounted above the Navigator, in the first frame, for the traversal to reach it in that window.
Workaround for anyone hitting this
Give the
SelectionAreaits own non-traversable focus node:_findGroupshonoursskipTraversalfor ordinary nodes (focus_traversal.dart:486), so the node drops out of the sort. It costs nothing functionally — the region takes focus on tap/selection, never via Tab. This does not help when the unlaid-out node is aFocusTraversalGroupnode, since those are added before the focusability filter (focus_traversal.dart:466) — which is presumably theNavigator/Overlaycase in the original report.Confirmed in our production app (same fix, assertion gone across repeated cold starts, full test suite green) and in the minimal repro above.
Full stack trace from the minimal repro
github-actions commentedon Sep 14, 2026
Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now.
If you find this problem please file a new issue with the same description, what happens, logs and the output of 'flutter doctor -v'. All system setups can be slightly different so it's always better to open new issues and reference the related ones.
Thanks for your contribution.
Reopen the strategy after a reload on web and clear two startup errors (
github-actions commentedon Sep 28, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.