We will cover briefly:
- What’s Element Embedding
- Embed Counter App in HTML webpage
What’s Element Embedding
The Flutter Forward event, which was broadcasted from Nairobi, Kenya, discussed Flutter’s future plans.
The team presented a sneak peek at the upcoming wave of investments in Flutter at this event, including early support for cutting-edge graphics performance, seamless integration for web and mobile, and early support for new and emerging architectures.
One such feature the team previewed was Element Embedding. This allows Flutter content to be added to any standard web <div>
When integrated in this manner, Flutter merely turns into a web component, integrating seamlessly with the web DOM and even allowing the styling of the parent Flutter object with CSS selectors and transformations.
Proof of Concept
This is a proof-of-concept demo that was shown at Flutter Forward, you can see a simple Flutter app embedded in an HTML-based webpage. The demo also showed how to make changes to the Flutter state using a JavaScript event handler and an HTML button.

Embed Counter App in HTML webpage
We will embed the famous Counter App inside an HTML-based webpage.
Pre-Requisite
- We should be using the flutter
master
channel - The Dart version should be
3.0.0
or above
Let’s create a project using
flutter create web_embedding --platforms web
Note: We specify the
platform
to beweb
meaning the project only supportsweb
By default, Flutter gives us the counter app using the template above.
We include the js package in our project to enable seamless interoperability between JavaScript and Dart code. Any function in your Dart code can be annotated with a @JSExport
property using js, allowing you to invoke it from JavaScript code.
flutter pub add js
Changes to the Counter App
Our project has only 2 dart files as shown below

We refactor the main.dart
take all the counter logic and put it inside the counter.dart
This contains the MyHomePage
which is a stateful widget (as we need to maintain the counter state) and _MyHomePageState
is the state class for the MyHomePage
widget
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
@js.JSExport()
class _MyHomePageState extends State<MyHomePage> {}
Next, we import the package js
and js_util
as
import 'package:js/js.dart' as js;
import 'package:js/js_util.dart' as js_util;
The _MyHomePageState
is annotated with the @JSExport
attribute since we also need to change the counter state from the JavaScript side. This makes the Dart object _MyHomePageState
as exportable
Inside our initState
we call the createDartExport
for the _MyHomePageState
which creates a JS object literal that forwards to our exported Dart class (which in our case is _MyHomePageState
)
The createDartExport()
method is used to create a JavaScript object that has a reference to a Dart object, which can then be accessed from JavaScript code. The setProperty()
and callMethod()
methods are used to set up JavaScript properties and call JavaScript functions, respectively.
void initState() {
super.initState();
final export = js_util.createDartExport(this);
// These two are used inside the [js/js-interop.js]
js_util.setProperty(js_util.globalThis, '_appState', export);
js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
}
A JsObject
instance export
is created by calling the createDartExport()
method provided by the js_util
library. This creates a JavaScript object that has a reference to the Dart object that is being exported.
Two JavaScript properties are set using the setProperty()
method provided by js_util
. The first property is _appState
, which is set to the export
object created in the previous step. This property can be accessed from JavaScript code to interact with the Dart object.
The second property is _stateSet
, which is a JavaScript function that is defined in another file (js-interop.js
). This function is called using the callMethod()
method provided by js_util
. The empty array []
passed as the second argument indicates that no arguments are being passed to the function.
Note:
_appState
and_stateSet
are present inside the js file, which we will discuss now.
We create a file js-interop.js
inside our project under the folder web/js
Inside the index.html
we import this file under the scripts section as
<script src="js/js-interop.js" defer></script>
The src
attribute specifies the URL of the JavaScript file to be loaded, in this case js-interop.js
. The defer
attribute tells the browser to defer the execution of the script until after the document has been parsed, which can improve page load times and prevent blocking of other resources.

All the interop with Dart is present inside this file. We make use of IIFE to create a function
An Immediately-invoked Function Expression (IIFE) is a way to execute functions immediately, as soon as they are created. IIFEs are useful as they don’t pollute the global object, and provide a simple way to isolate variables declarations
(function () {
window._stateSet = function () {
console.log('HELLO From Flutter!!')
};
}());
This code sets the _stateSet
function as a property of the global window
object. It is intended to be called by Flutter when it prepares the JS-interop. When called, the _stateSet
function will log the message “HELLO From Flutter!!” to the browser console.
Next, we access the _appState
and save it inside a variable inside the JS
let appState = window._appState;
We create an HTML file called index.html
and inside it create buttons using HTML tags. And since we want to show the counter app, we also create a div with id as flutter_target
<section class="contents">
<aside id="demo_controls">
<fieldset id="interop">
<legend>JS Interop</legend>
<!--This is the value box-->
<label for="value">
Value
<input id="value" value="" type="text" readonly />
</label>
<!--This is the button-->
<input id="increment" value="Increment" type="button" />
</fieldset>
</aside>
<!--This is the div which contains the flutter app-->
<article>
<div id="flutter_target" class="center"></div>
</article>
</section>
If we run the app at this point, we see this

Notice, we don’t see our Flutter Counter App, although we created a div for us to show the Flutter App.
We tweak the script
tag inside the index.html
by first adding an event listener to the window
Get the div id, which represents the flutter app
in our case flutter_target
, using the query selector
window.addEventListener("load", function (ev) {
let target = document.querySelector("#flutter_target");
_flutter.loader.loadEntrypoint({
onEntrypointLoaded: async function (engineInitializer) {
let appRunner = await engineInitializer.initializeEngine({
hostElement: target,
});
await appRunner.runApp();
},
});
});
- Using the
_flutter. loader
JavaScript API offered byflutter.js
, you can modify how a Flutter app is launched on the web.
The following phases make up the initialization process:
- Entry point script is loaded, the service worker is initialized after retrieving the
main.dart.js
script. - Initializing the Flutter engine which downloads the necessary files, including CanvasKit, fonts, and assets, to launch the Flutter web engine.
- Running the application which executes your Flutter app after preparing the DOM for it.
Once the Service Worker is started and the browser has downloaded and executed the main.dart.js
entry point, the loadEntrypoint
method invokes the onEntrypointLoaded
callback.
The onEntrypointLoaded
the callback receives an engine initializer object as its only parameter. We use the engine initializer to set the run-time configuration and start the Flutter Web engine.
The initializeEngine()
the function produces a Promise that resolves to an app runner object. The Flutter app is launched by the app runner’s single method, runApp()
And if the run the app now, we are able to the Flutter Counter App

Interoperability between Javascript and Dart
Currently, neither the floating action button
nor the increment
html button will update the contents of the JS text field in the flutter app when they are clicked.
Let’s fix this
To increase the counter
value, we create the increment
function inside of the counter.dart
file and annotate it with the @JSExport
property (making it accessible from the JS side)
@js.JSExport()
void increment() {
setState(() {
_counterScreenCount++;
});
}
@js.JSExport()
int get count => _counterScreenCount;
We tweak our counter variable and create a getter called count
that holds the current value of the screen count. This getter is then annotated with JSExport
thus making it accessible from JS side.
JS Changes
The snippet first uses the document.querySelector()
method to find the HTML input element with the id
of value
, and assign it to the variable valueField
.
// Get the input box i.e `value`
let valueField = document.querySelector("#value");
let updateState = function () {
valueField.value = appState.count;
};
// Register a callback to update the HTML field from Flutter.
appState.addHandler(updateState);
// Render the first value (0).
updateState();
Next, we define a function called updateState
, which sets the value
property of the input element to the value of appState.count
. The updateState
function is not called directly, but is instead registered as a callback using the appState.addHandler()
method. This means that the updateState
function will be called every time appState
changes.
appState
(note: this is the variable that holds the state of the flutter app)
Note: We need to create
addHandler
function
The updateState
function is responsible for updating the HTML input element to reflect changes in the Flutter application state.
And because the counter’s initial value is 0
, we use the function updateState
to display 0
in our html input text box.
addHandler
Let’s create this function inside the counter.dart
A StreamController
instance _streamController
is defined with the type StreamController<void>.broadcast()
. The broadcast()
method creates a stream controller that can handle multiple subscribers.
The @JSExport()
decorator is used to indicate that the addHandler()
method should be exported to JavaScript code.
final _streamController = StreamController<void>.broadcast();
@js.JSExport()
void addHandler(void Function() handler) {
// This registers the handler we wrote in [js/js-interop.js]
_streamController.stream.listen((event) {
handler();
});
}
The addHandler()
method is defined to take a single argument handler
that is of type void Function()
.
Inside the method, a StreamSubscription
is created by calling the listen()
method on _streamController.stream
. The listen()
method takes a callback function as an argument, which will be called each time an event is added to the stream.
The handler()
function, which was passed as a parameter, is invoked inside the callback function. This method can be called from JavaScript code to register a callback function that will be invoked whenever an event is added to the stream.
Next, we modify our existing functions and add an event to the streamController
to make sure the handler
we wrote in the js-interop.js
gets invoked.
Inside the setState()
method, the _counterScreenCount
value is incremented by 1, and then the StreamController
instance _streamController
is notified of the change by adding a null value to the stream using _streamController.add(null)
.
@js.JSExport()
void increment() {
setState(() {
_counterScreenCount++;
// This line makes sure the handler gets invoked
_streamController.add(null);
});
}
And if the run the app now, we are able to the see the input box with default value as 0

If we click on the increment
button, we see the values don’t reflect in the flutter side and vice-versa.
Sending events between JS and Dart
let incrementButton = document.querySelector("#increment");
incrementButton.addEventListener("click", (event) => {
appState.increment(); // Function present inside the `client.dart`
});
We get a reference to the HTML button element with the ID increment
using the document.querySelector()
method. This method returns the first element that matches the specified selector.
The addEventListener()
method is then called on the button element to register an event listener for the “click” event.
The second argument to addEventListener()
is an arrow function that is executed when the click
event is fired. Inside this arrow function, the appState.increment()
function is called. This function is defined in the code (in client.dart
) and is responsible for incrementing value in the application state.
This increment
function is present inside the counter.dart
which simply increases the counter value in the dart side, by adding an event to the stream controller.
_streamController.add(null);
When an event is added to the stream controller, the addHandler
method, which calls the callback set up in the JS
, is also fired.
// Register a callback to update the HTML field from Flutter.
appState.addHandler(updateState);
In this manner, the UI is updated for both whenever the increment
button is pressed, whether from the Dart side or the JS side.