Skip to content

[web] RenderBox was not laid out: _RenderTheater — ViewFocusEvent handling measures a just-mounted Overlay before layout #191508

Closed

Description

@grupoclip

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:

  1. Build a Flutter web app (CanvasKit) whose root builder conditionally wraps its
    child, e.g. MaterialApp.builder: (context, child) => Barrier(child: child!).
  2. Barrier returns child verbatim until an async provider resolves, then
    returns Stack(children: [child, Positioned.fill(child: Overlay(initialEntries: [...]))]).
  3. The overlay entry renders normal Material widgets (Material → InkWell /
    MenuAnchor — anything that installs a focus node).
  4. 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:

  1. The policy cannot be replaced. _ViewState hardcodes it —
    view.dart:209:

    final FocusTraversalPolicy _policy = ReadingOrderTraversalPolicy();

    Wrapping the app in FocusTraversalGroup(policy: WidgetOrderTraversalPolicy())
    does not affect this call.

  2. Group nodes are exempt from every focusability filter. In _findGroups
    (focus_traversal.dart:452), the node == groupNode branch appends and
    continues before the "Skip non-focusable and non-traversable nodes" check
    further down. So ExcludeFocus, canRequestFocus: false and skipTraversal
    cannot keep a group node out of the sort.

  3. FocusNode.rect has no hasSize guard. It asserts on context != null
    only (focus_manager.dart:862-867) and then measures unconditionally.

Three app-side attempts, all measured against the repro, all failed:

  • a stable GlobalKey on the router subtree — no effect (and unnecessary:
    go_router already pins the root Navigator with a GlobalKey);
  • replacing the mid-session Navigator with a bare Overlay — removed the
    FocusTraversalGroup-over-_RenderTheater shape, error unchanged;
  • keeping that Overlay mounted 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)

  1. Guard the read: FocusNode.rect returns Rect.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.
  2. Have _ReadingOrderSortData / _findGroups skip members whose render object
    is not laid out.
  3. Let _ViewState's policy be overridable, so an app can choose
    WidgetOrderTraversalPolicy and 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

  1. suryaKommana2662 commented on Aug 24, 2026

    @suryaKommana2662
    Contributor

    @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 the RenderBox was not laid out: _RenderTheater error in a browser environment.

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    and removed
    in triagePresently being triaged by the triage team
    on Aug 24, 2026
  3. added a commit that references this issue on Aug 25, 2026
    a8346d1
  4. SleepyFoxP1N commented on Aug 26, 2026

    @SleepyFoxP1N

    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 deployed
    flutter_bootstrap.js). Seen in release builds on Chrome 150/151 and on
    Mobile 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 observer
    nowhere - it is the framework's own _ViewState.

    The part that may be useful: it is not only _RenderTheater. We have five
    separate Sentry groups for this. One reports _RenderTheater, matching your
    report; 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 routes
    where we mount no Overlay at all (below), so the failing measurement does not
    look specific to a just-mounted Overlay.

    Consistent with that, our trigger is not an Overlay at all. Our
    MaterialApp.builder wraps child in a chain of gates, and one of them watches
    an async provider and restructures once it resolves - but it composes with a
    Column, not a Stack/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
    Overlay being 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._markNeedsUpdate defers through
    scheduleMicrotask, so driving handleViewFocusChanged by hand (including from
    inside build) never reproduces it, and any fixture that forces a permanently
    unlaid-out subtree trips a different assertion instead. The production condition
    appears to be genuinely transient - the subtree is mid-layout, not never-laid-out

    • which is exactly what a testWidgets harness will not give you.

    Happy to pull more specifics from our telemetry if any particular tag or context
    would help.

  5. Tim0theus commented on Sep 3, 2026

    @Tim0theus

    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:

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: const Scaffold(body: Center(child: Text('hello'))),
          // An app-wide SelectionArea, mounted from the very first frame.
          // The Overlay is only here because SelectionArea's toolbar needs an
          // Overlay ancestor above MaterialApp's own Navigator.
          builder: (context, child) => Overlay(
            initialEntries: [
              OverlayEntry(
                builder: (context) =>
                    SelectionArea(child: child ?? const SizedBox.shrink()),
              ),
            ],
          ),
        );
      }
    }

    flutter run -d chrome throws on startup, every time:

    The following assertion was thrown while dispatching notifications for
    WidgetsBindingObserver.didChangeViewFocus:
    "RenderBox was not laid out: RenderStack#fe158 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE"
    

    Note this needs no async gate and no mid-startup subtree swap — a plain MaterialApp with a SelectionArea in its builder is enough. The focus request that starts the chain is just the initial route's own scope.

    Why RenderStack and not _RenderTheater

    This thread already notes that the render object is not always _RenderTheater. In this repro it is deterministic:

    • SelectableRegion puts its focus node directly above its child in Focus.withExternalFocusNode (selectable_region.dart:1983).
    • On web that child is wrapped in PlatformSelectableRegionContextMenu, whose build returns a Stack (_platform_selectable_region_context_menu_web.dart:146).

    So FocusNode.rect on the SelectableRegion node resolves to that RenderStack, 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

    Variant Throws?
    SelectionArea in MaterialApp.builder (above the Navigator), as above yes, every start
    Same, but SelectionArea(focusNode: FocusNode(skipTraversal: true), ...) no
    MaterialApp(home: SelectionArea(child: Scaffold(...))) (below the Navigator) no

    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 SelectionArea its own non-traversable focus node:

    final FocusNode _selectionFocusNode = FocusNode(skipTraversal: true);
    // ...
    SelectionArea(focusNode: _selectionFocusNode, child: child)

    _findGroups honours skipTraversal for 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 a FocusTraversalGroup node, since those are added before the focusability filter (focus_traversal.dart:466) — which is presumably the Navigator/Overlay case 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
    ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
    The following assertion was thrown while dispatching notifications for
    WidgetsBindingObserver.didChangeViewFocus:
    Assertion failed:
    file:///Users/<user>/flutter/packages/flutter/lib/src/rendering/box.dart:2251:12
    hasSize
    "RenderBox was not laid out: RenderStack#fe158 NEEDS-LAYOUT NEEDS-PAINT
    NEEDS-COMPOSITING-BITS-UPDATE"
    
    When the exception was thrown, this was the stack:
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 274:3       throw_
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 44:3        assertFailed
    package:flutter/src/rendering/box.dart 2251:12                                    get size
    package:flutter/src/rendering/box.dart 2437:44                                    get semanticBounds
    package:flutter/src/widgets/focus_manager.dart 871:13                             get rect
    package:flutter/src/widgets/focus_traversal.dart 1399:18                          new
    package:flutter/src/widgets/focus_traversal.dart 1580:43                          <fn>
    package:flutter/src/widgets/focus_traversal.dart 1579:41                          sort
    package:flutter/src/widgets/focus_traversal.dart 1709:7                           sortDescendants
    package:flutter/src/widgets/focus_traversal.dart 514:55                           _sortAllDescendants
    package:flutter/src/widgets/focus_traversal.dart 338:9                            [_findInitialFocus]
    package:flutter/src/widgets/focus_traversal.dart 299:12                           findFirstFocus
    package:flutter/src/widgets/view.dart 254:33                                      didChangeViewFocus
    package:flutter/src/widgets/binding.dart 1355:17                                  handleViewFocusChanged
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 118:77  tear
    lib/_engine/engine/platform_dispatcher.dart 1748:5                                invoke1
    lib/_engine/engine/platform_dispatcher.dart 206:5                                 invokeOnViewFocusChange
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 118:77  tear
    lib/_engine/engine/platform_dispatcher/view_focus_binding.dart 109:23             [_handleFocusChange]
    lib/_engine/engine/platform_dispatcher/view_focus_binding.dart 54:5               <fn>
    dart-sdk/lib/async/zone.dart 1034:54                                              runUnary
    dart-sdk/lib/async/zone.dart 989:26                                               <fn>
    dart-sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart 218:27    _callDartFunctionFast1
    dart-sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart 78:15     ret
    lib/_engine/engine/dom.dart 518:8                                                 DomElement.focusWithoutScroll
    lib/_engine/engine/platform_dispatcher/view_focus_binding.dart 45:24              changeViewFocus
    lib/_engine/engine/platform_dispatcher.dart 215:5                                 requestViewFocusChange
    package:flutter/src/widgets/view.dart 233:29                                      [_scopeFocusChangeListener]
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 118:77  tear
    package:flutter/src/foundation/change_notifier.dart 435:24                        notifyListeners
    package:flutter/src/widgets/focus_manager.dart 1139:5                             [_notify]
    package:flutter/src/widgets/focus_manager.dart 2011:11                            applyFocusChangesIfNeeded
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 118:77  tear
    dart-sdk/lib/async/schedule_microtask.dart 40:34                                  _microtaskLoop
    dart-sdk/lib/async/schedule_microtask.dart 49:5                                   _startMicrotaskLoop
    dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 118:77  tear
    dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 183:69               <fn>
    ════════════════════════════════════════════════════════════════════════════════════════════════════
    
  6. github-actions commented on Sep 14, 2026

    @github-actions

    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.

  7. github-actions commented on Sep 28, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  8. locked as resolved and limited conversation to collaborators on Sep 28, 2026
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

    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions