Embedded Flutter Dropdown/overlay Web Element Clipped By Parent Html Element Constraints
I am implementing Flutter Web Multi-View Embedding to integrate independent Flutter components into a React.js frontend. Following the official documentation, I am initializing each Flutter widget into its own specific HTML host element.
The Problem:
Any widget that relies on the Flutter Overlay (such as DropdownButton or PopupMenuButton) is being visually clipped by the boundaries of its parent HTML container. Instead of rendering the overlay at the browser viewport level, the Flutter engine appears to constrain the RenderView to the dimensions of the host div. This prevents the dropdown from "floating" over the surrounding React content, making it unusable in small UI components.
Technical Context:
Hosting Environment: React.js.
Embedding Method: flutter.loader.loadEntrypoint with a specific hostElement.
Expected Behavior: The overlay should render at the top level of the browser viewport (or at least outside the host div).
Actual Behavior: The overlay is restricted to the host element's CSS constraints and overflow: hidden behavior of the Flutter canvas.
Figure of expected and actual result (sorry, I am a new contributor and don't have enough reputation, I can only make it as a link)
Minimal Reproducible Example:
Flutter:
import 'dart:ui_web';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:ui' show FlutterView;
import 'dart:js_interop';
import 'package:dropdown_button2/dropdown_button2.dart';
@JS()
extension type InitialDataJS(JSObject _) implements JSObject {
external JSString? get path;
}
void main() {
runWidget(
MultiViewApp(
viewBuilder: (BuildContext context) {
final int viewId = View.of(context).viewId;
final InitialDataJS? rawData =
views.getInitialData(viewId) as InitialDataJS?;
final String? path = rawData?.path?.toDart;
return MyApp(path: path);
},
),
);
}
/// Calls [viewBuilder] for every view added to the app to obtain the widget to
/// render into that view. The current view can be looked up with [View.of].
class MultiViewApp extends StatefulWidget {
const MultiViewApp({super.key, required this.viewBuilder});
final WidgetBuilder viewBuilder;
@override
State<MultiViewApp> createState() => _MultiViewAppState();
}
class _MultiViewAppState extends State<MultiViewApp>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_updateViews();
}
@override
void didUpdateWidget(MultiViewApp oldWidget) {
super.didUpdateWidget(oldWidget);
// Need to re-evaluate the viewBuilder callback for all views.
_views.clear();
_updateViews();
}
@override
void didChangeMetrics() {
_updateViews();
}
Map<Object, Widget> _views = <Object, Widget>{};
void _updateViews() {
final Map<Object, Widget> newViews = <Object, Widget>{};
for (final FlutterView view
in WidgetsBinding.instance.platformDispatcher.views) {
final Widget viewWidget = _views[view.viewId] ?? _createViewWidget(view);
newViews[view.viewId] = viewWidget;
}
setState(() {
_views = newViews;
});
}
Widget _createViewWidget(FlutterView view) {
return View(
view: view,
child: Builder(builder: widget.viewBuilder),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ViewCollection(views: _views.values.toList(growable: false));
}
}
class MyApp extends StatefulWidget {
const MyApp({super.key, required this.path});
final String? path;
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
final Widget? content = {
'/dropdown': DropdownBtn(),
'/text': _buildText(),
}[widget.path ?? '/dropdown'];
return MaterialApp(
navigatorKey: navigatorKey,
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Align(
alignment: Alignment.topCenter,
child: content ?? SizedBox.shrink(),
),
),
),
);
}
Widget _buildText() {
return Center(
child: Text('Text Content', textDirection: TextDirection.ltr));
}
}
class DropdownBtn extends StatefulWidget {
const DropdownBtn({super.key});
static const List<String> options = [
'option 1',
'option 2',
'option 3',
'option 4',
'option 5'
];
@override
State<DropdownBtn> createState() => _DropdownBtnState();
}
class _DropdownBtnState extends State<DropdownBtn> {
String selectedValue = DropdownBtn.options.first;
@override
Widget build(BuildContext context) {
return DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: selectedValue,
items: DropdownBtn.options
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e),
),
)
.toList(),
onChanged: (e) {
if (e != null) {
setState(() {
selectedValue = e;
});
}
},
),
);
}
}
React
import { useEffect, useRef, useState } from "react";
const App = () => {
const flutterContainer1 = useRef(null);
const flutterContainer2 = useRef(null);
let appInstance = null;
useEffect(() => {
const script = document.createElement("script");
const localhost = "http://localhost:8000/";
script.src = localhost + "flutter.js";
script.async = true;
document.body.appendChild(script);
script.onload = () => {
if (window._flutter && window._flutter.loader) {
window._flutter.loader.loadEntrypoint({
entrypointUrl: localhost + "main.dart.js",
onEntrypointLoaded: async (engineInitializer) => {
const appRunner = await engineInitializer.initializeEngine({
assetBase: localhost,
multiViewEnabled: true,
});
appInstance = await appRunner.runApp();
appInstance.addView({
hostElement: flutterContainer1.current,
initialData: {
path: "/dropdown",
},
viewConstraints: {
minHeight: 100,
maxHeight: Infinity,
},
});
appInstance.addView({
hostElement: flutterContainer2.current,
initialData: {
path: "/text",
},
viewConstraints: {
minHeight: 0,
maxHeight: Infinity,
},
});
},
});
}
};
return () => {
if (document.body.contains(script)) {
document.body.removeChild(script);
}
};
}, []);
return (
<>
<div
ref={flutterContainer1}
/>
<div
ref={flutterContainer2}
/>
</>
);
};
export default App;
What I have tried:
Container Dimension Validation: I have verified that the React host
divhas sufficient height for the initial widget state, but the requirement is for theOverlayto "break out" of the container boundaries without forcing a resize of the parent DOM element.CSS Overflow Manipulation: I applied
overflow: visible !importantand adjusted thez-indexon the host element and its parent hierarchy. Despite these CSS overrides, the Flutter-rendered overlay remains strictly constrained to the boundaries, similarly to not having any CSS applied.
Motivation:
I am implementing a cross-platform design system where UI components are unified in Flutter, while business logic is distributed between Flutter and an existing React.js infrastructure. To avoid a full-scale migration of our React application to Flutter, I am adopting a hybrid approach by embedding specific Flutter components. Solving this overlay clipping issue is critical for maintaining a consistent user experience across our shared design system.
Popular Products
-
Put Me Down Funny Toilet Seat Sticker$33.56$16.78 -
Stainless Steel Tongue Scrapers$33.56$16.78 -
Stylish Blue Light Blocking Glasses$85.56$42.78 -
Adjustable Ankle Tension Rope$53.56$26.78 -
Electronic Bidet Toilet Seat$981.56$490.78