Skip to main content

Solving 'dart:html' Errors When Building Flutter Web with WASM

 You have upgraded your Flutter project to the latest version. You are ready to unlock the performance gains of WebAssembly (Wasm) by running flutter build web --wasm. Instead of a successful build, the compiler halts with a blocking error:

Error: Dart library 'dart:html' is not available on this platform.

This error is the single most common blocker for Flutter developers migrating to Wasm. It occurs because the legacy dart:html library relies on dynamic JavaScript interop mechanisms that are incompatible with the strict typing required by WasmGC (Garbage Collection).

To compile to Wasm, you must migrate your code from dart:html and dart:js to the modern package:web and dart:js_interop. This guide details the root cause and provides the precise code refactoring required to fix it.

The Root Cause: Why dart:html Is Incompatible with Wasm

Historically, Flutter Web (via the dart2js compiler) interacted with the browser DOM using dart:html. This library was a monolithic wrapper around standard browser APIs. It relied heavily on dynamic dispatch and JavaScript-specific behaviors that allowed Dart objects to effectively "pretend" to be JavaScript objects.

WebAssembly operates differently. It requires a strictly typed boundary between the Wasm module and the host browser environment. The legacy dart:html library cannot enforce the static analysis guarantees that WasmGC requires to manage memory safely and efficiently.

To solve this, the Dart team introduced dart:js_interop and package:web.

  1. dart:js_interop: A core library providing zero-cost, static interoperability with JavaScript.
  2. package:web: A comprehensive binding to standard DOM APIs (like windowdocument, and HTMLElement) built strictly on top of dart:js_interop.

If your code or any of your dependencies import dart:html, the Wasm compiler will fail immediately.

Step 1: Audit and Update Dependencies

Before refactoring your own code, ensure your dependencies support Wasm. Many popular packages (like url_launcher or shared_preferences) have already migrated, but you must be on their latest versions.

Run the following command to check your dependency tree:

flutter pub outdated --mode=platform-web

If a package is flagged as incompatible with the web platform or is preventing the Wasm build, check its changelog on pub.dev. You generally need to upgrade to versions released after mid-2023.

Update your pubspec.yaml to include package:web and ensure the Dart SDK constraint is at least 3.3.0 (which stabilized dart:js_interop):

environment:
  sdk: '>=3.3.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  # Add this package
  web: ^0.5.1 

Run flutter pub get to install the new package.

Step 2: Refactoring Code from dart:html to package:web

You must replace every import of dart:html with package:web. However, the API surface is not 1:1. package:web adheres strictly to standard IDL definitions, meaning the names and types match native browser APIs closer than dart:html did.

The Import Switch

Legacy (Remove this):

import 'dart:html' as html;

Modern (Add this):

import 'package:web/web.dart' as web;
import 'dart:js_interop'; // Required for type casting

Example: Modifying DOM Elements

A common use case in Flutter Web is manipulating the DOM directly, such as accessing a specific input field or checking the window size.

The Legacy Way (dart:html): In the old system, casting was done using standard Dart as checks, which often hid runtime JS logic.

// OLD CODE - DO NOT USE
import 'dart:html';

void logInputValue() {
  final input = document.querySelector('#my-input') as InputElement?;
  if (input != null) {
    print(input.value);
  }
}

The Wasm-Ready Way (package:web): In the new system, document.querySelector returns a Element?. To treat it as an HTMLInputElement, you cannot simply use as. You must use the extension methods provided by package:web or strictly cast via plain Dart if the types align with the JS interop hierarchy.

Note that package:web types (like HTMLInputElement) are "extension types" wrapping JS objects.

// NEW CODE - WASM COMPATIBLE
import 'package:web/web.dart';
import 'dart:js_interop';

void logInputValue() {
  // 1. querySelector returns 'Element?'
  final element = document.querySelector('#my-input');

  // 2. Check strict type compatibility using library helpers if necessary, 
  // or simple null checks.
  if (element != null) {
    // 3. Cast to the specific HTML type
    final input = element as HTMLInputElement;
    print(input.value);
  }
}

Handling Events

Event listeners also look slightly different. The package:web library uses standard generic Event types.

import 'package:web/web.dart';
import 'dart:js_interop';

void attachListener() {
  final btn = document.querySelector('#submit-btn') as HTMLButtonElement?;
  
  // Note: 'toJS' is often required when passing Dart functions to JS,
  // but package:web helpers handle standard EventStreams gracefully.
  btn?.onClick.listen((MouseEvent event) {
    print('Button clicked at coordinates: ${event.clientX}, ${event.clientY}');
  });
}

Step 3: Replacing Custom JS Interop

If you were using dart:js or generic js annotations to call custom JavaScript functions, those will also fail in Wasm builds. You must migrate to strict @JS interop.

The Problem (dart:js):

// FAILS IN WASM
import 'dart:js';

void callAlert(String message) {
  context.callMethod('alert', [message]);
}

The Solution (dart:js_interop): You must define an external function that maps to the JavaScript function.

// WORKS IN WASM
import 'dart:js_interop';

@JS()
external void alert(String message);

void callAlert(String message) {
  alert(message);
}

If the JavaScript function lives on the window object or inside a namespace, usage remains similar but requires strict typing for arguments and return values. JSAnyJSString, and JSNumber are common types you will encounter.

Deep Dive: Handling window and Globals

In dart:html, the window object was globally available. In package:webwindow is a getter that returns a Window object.

A major friction point is accessing properties like localStorage or location.

import 'package:web/web.dart';

void checkLocalStorage() {
  // Directly access the 'window' getter from package:web
  final storage = window.localStorage;
  
  if (storage.getItem('authToken') == null) {
    window.location.href = '/login';
  }
}

The syntax is cleaner, but remember that standard Dart types (like String) are automatically converted to JS types in package:web methods, unlike the raw dart:js_interop where you often have to call .toJS.

Common Pitfall: Type Casting Runtime Errors

When running in Wasm, the distinction between a Dart object and a JS object is rigid.

If you attempt to cast a Dart List to a JS Array using as, your app will crash. You must use conversion methods.

import 'dart:js_interop';

void sendDataToJS(List<String> data) {
  // WRONG: will crash
  // externalFunction(data); 

  // CORRECT: Convert Dart List to JS Array
  final jsArray = data.map((e) => e.toJS).toList().toJS;
  externalFunction(jsArray);
}

@JS('processData')
external void externalFunction(JSAny array);

Always prefer using JSAny as a catch-all for JS objects if you are unsure of the structure, but refine it to specific interop types (JSArrayJSObject) whenever possible for type safety.

Conclusion

Migrating to package:web is not optional for modern Flutter Web development. While dart:html served the community well for years, dart:js_interop and package:web provide the type safety required for the WebAssembly Garbage Collection (WasmGC) proposal.

By updating your imports, refactoring DOM interaction to use the new standard types, and ensuring your dependencies are updated, you eliminate the dart:html error. The result is a Flutter web application that loads faster, performs better, and is future-proofed for the next generation of web capabilities.