Skip to content

JavaScript

JavaScript support is split into four areas: execution, return values, channels, and browser-style dialogs.

await controller.setJavaScriptMode(JavaScriptMode.unrestricted);

Disable JavaScript for untrusted content when your app does not need page scripting:

await controller.setJavaScriptMode(JavaScriptMode.disabled);

The web implementation applies a restrictive iframe sandbox when JavaScript is disabled and restores the configured sandbox when JavaScript is unrestricted.

await controller.runJavaScript('document.body.classList.add("ready")');
final value = await controller.runJavaScriptReturningResult(
'JSON.stringify({title: document.title})',
);

Return-value behavior:

Platform Return behavior
Android Uses Android WebView evaluation result.
iOS/macOS Uses WebKit evaluation; unsupported native values may throw.
Windows Uses WebView2 script execution and decodes returned values.
Linux Uses WebKitGTK and decodes JSON-marked results where needed.
Web Uses direct iframe eval for same-origin content and a source-validated message bridge for plugin-managed isolated HTML. Results must be JSON-serializable.

null and undefined are rejected by runJavaScriptReturningResult.

await controller.addJavaScriptChannel(
'Checkout',
onMessageReceived: (JavaScriptMessage message) {
debugPrint('Checkout event: ${message.message}');
},
);

Page JavaScript:

Checkout.postMessage(JSON.stringify({ type: 'loaded' }));

Remove a channel when it is no longer needed:

await controller.removeJavaScriptChannel('Checkout');
await controller.setOnConsoleMessage((JavaScriptConsoleMessage message) {
debugPrint('[${message.level.name}] ${message.message}');
});

Android, iOS, macOS, Windows, Linux, and controllable Web content support console message callbacks.

await controller.setOnJavaScriptAlertDialog((request) async {
debugPrint('alert from ${request.url}: ${request.message}');
});
await controller.setOnJavaScriptConfirmDialog((request) async {
return request.message == 'Continue?';
});
await controller.setOnJavaScriptTextInputDialog((request) async {
return request.defaultText ?? '';
});

Dialog support by platform:

Platform alert confirm prompt Notes
Android Supported Supported Supported Native WebChromeClient bridge.
iOS/macOS Supported Supported Supported WebKit UI delegate bridge.
Windows Supported Supported Supported WebView2 JavaScript dialog bridge.
Linux Supported Supported Supported WebKitGTK dialog events.
Web Same-origin or managed isolated HTML Same-origin callback; isolated HTML keeps the browser dialog Same-origin callback; isolated HTML keeps the browser dialog Custom confirm and prompt callbacks must complete synchronously.

On web, confirm and prompt are browser-synchronous APIs. Return a SynchronousFuture from those callbacks if you need deterministic behavior for same-origin content:

await controller.setOnJavaScriptConfirmDialog((request) {
return SynchronousFuture<bool>(true);
});

Browser iframes block direct scripting of cross-origin pages. On web, these APIs require content that the host page can access:

  • runJavaScript
  • runJavaScriptReturningResult
  • addJavaScriptChannel
  • setOnConsoleMessage
  • JavaScript dialog hooks
  • scroll position reads and writes
  • runJavaScriptAsync
  • addUserScript (srcdoc and same-origin only)
final JavaScriptAsyncResult result = await controller.runJavaScriptAsync(
'''
const price = await window.getIcpPrice();
return { success: true, price };
''',
arguments: <String, Object?>{'expectedPrincipal': principal},
timeout: const Duration(seconds: 8),
);
if (result.hasError) {
debugPrint(result.error);
} else {
debugPrint('${result.value}');
}

functionBody runs inside an async function. Named [arguments] become JS locals. iOS 14+ / macOS 11+ use native callAsyncJavaScript. Linux uses webkit_web_view_call_async_javascript_function when WebKitGTK is 2.40 or newer. Android, Windows, and older Linux inject a document-start helper. Web awaits Promises on same-origin or plugin-managed isolated HTML.

await controller.addUserScript(
const UserScript(
source: 'window.__appReady = true;',
injectionTime: UserScriptInjectionTime.documentStart,
forMainFrameOnly: true,
),
);
await controller.removeAllUserScripts();

Document-start injection is native on WKWebView, WebKitGTK, WebView2, and Android WebView 91+. Older Android falls back to onPageStarted. Web injects script tags into srcdoc/managed HTML; cross-origin iframes cannot be patched.

Use loadHtmlString, same-origin URLs, or fetch-backed loadRequest when you need those features in Flutter Web. Direct cross-origin iframe URLs remain inaccessible. Isolated HTML keeps browser-native confirm and prompt dialogs because synchronous callbacks cannot cross the frame boundary.