Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WebView

In this chapter, you will:

  • Embed web content directly inside your WaterUI application
  • Navigate, inject scripts, and execute JavaScript from Rust
  • Set up bidirectional communication between Rust and web code
  • Manage cookies and redirect behavior programmatically
  • Build a minimal in-app browser with back/forward controls

Sometimes the best tool for the job is the web. Maybe you need to display documentation, embed an OAuth login flow, or wrap an existing web app inside your native shell. The waterui-webview crate makes this seamless – you get a full-featured embedded browser with navigation, JavaScript execution, cookie management, and a Rust-to-JS bridge, all from Rust.

Feature flag: WebView lives behind the webview feature on waterui. Enable it in Cargo.toml (waterui = { version = "...", features = ["webview"] }) before importing waterui::webview.

Architecture

The WebView system follows a layered design:

LayerTypeRole
TraitWebViewHandleImperative API that native backends implement
Type-erased wrapperAnyWebViewHandleWraps any WebViewHandle with downcast support
FactoryWebViewControllerEnvironment-injected factory for creating web views
Reactive viewWebViewCombines AnyWebViewHandle with Binding state

Native backends (Apple, Android) implement CustomWebViewController and inject a WebViewController into the Environment at startup. Your application code then obtains the controller and creates web views through it.


Quick Start

The simplest way to embed web content is with WebView::open:

use waterui_webview::WebView;

fn docs_page() -> impl View {
    WebView::open("https://waterui.dev/docs")
}

WebView::open pulls the WebViewController from the environment automatically, creates a new web view handle, navigates to the URL, and returns a View that renders the embedded browser.

That is all it takes – one line to go from URL to rendered web content.


Creating a WebView Manually

For more control, obtain the controller from the environment, open a fresh WebView, and configure it before placing it in the view hierarchy:

use waterui::prelude::*;
use waterui_webview::{WebView, WebViewController};

fn custom_browser() -> impl View {
    use_env(|controller: WebViewController| {
        let webview = controller.open();
        webview.go_to("https://example.com");
        webview.set_user_agent("MyApp/1.0");
        webview
    })
}

WebViewController::open() returns a fresh WebView already wrapped with reactive event state. Use the WebView itself as the imperative handle.

open_then for Post-Creation Configuration

When you want to configure the underlying handle immediately after creation but still build the view in a single expression, use WebView::open_then:

use waterui_webview::WebView;

fn configured_webview() -> impl View {
    WebView::open_then("https://example.com", |handle| {
        handle.set_user_agent("MyApp/1.0");
        handle.set_redirects_enabled(false);
    })
}

The closure receives an AnyWebViewHandle, which exposes the same imperative API as WebView (navigation, user agent, cookie store, script injection).


Once you have a WebView instance, control navigation imperatively:

// Navigate to a URL
webview.go_to("https://example.com");

// Refresh the current page
webview.refresh();

// Stop loading
webview.stop();

// History navigation
webview.go_back();
webview.go_forward();

Reactive Navigation State

WebView exposes reactive signals for history state:

// Returns Computed<bool>
let can_back = webview.can_go_back();
let can_forward = webview.can_go_forward();

These update automatically as the user navigates. Use them to enable/disable back and forward buttons in your custom browser chrome.


Events

Subscribe to navigation lifecycle events through the reactive event() signal:

use waterui_webview::{WebView, WebViewEvent};

let webview = WebView::new(handle);

// Watch events reactively
webview.event(); // returns impl Signal<Output = WebViewEvent>

WebViewEvent Variants

EventFieldsDescription
NoneInitial state before any event fires
WillNavigateurl: UrlNavigation is about to begin
Loadingprogress: f32Page load progress (0.0 to 1.0)
LoadedPage finished loading
Redirectfrom: Url, to: UrlA redirect occurred during navigation
Error(WebViewError)An error occurred

Error Types

use waterui_webview::WebViewError;
ErrorDescription
WebViewError::Network(msg)A network error occurred
WebViewError::Ssl { url, message }An SSL/TLS verification failure
WebViewError::LoadFailed(msg)The page failed to load

JavaScript Execution

One of the most powerful features of the WebView is the ability to run JavaScript from Rust and get results back.

Running Scripts

Execute JavaScript in the context of the loaded page:

let result = webview.run_javascript("document.title").await;
match result {
    Ok(title) => tracing::info!("Page title: {title}"),
    Err(err) => tracing::error!("JS error: {err}"),
}

run_javascript is async and returns Result<Str, Str>. It executes after the page has loaded. For scripts that must run before the DOM is constructed, use script injection instead.

Script Injection

Inject scripts that run automatically on every page load:

use waterui_webview::ScriptInjectionTime;

// Run before DOM construction
webview.inject_script(
    r#"window.APP_VERSION = "1.0.0";"#,
    ScriptInjectionTime::DocumentStart,
);

// Run after DOM is ready
webview.inject_script(
    r#"document.body.style.backgroundColor = "#f0f0f0";"#,
    ScriptInjectionTime::DocumentEnd,
);
Injection TimeDescriptionUse Cases
DocumentStartBefore the DOM is constructedNative bridges, global object setup, request interception
DocumentEndAfter the document finishes loadingDOM manipulation, event listeners

Rust-to-JavaScript Bridge

The WebView supports bidirectional communication through message handlers. This is how you connect your Rust business logic to your web UI.

Setting Up a Handler

Register a Rust function that JavaScript can call:

webview.handle().add_handler("greet", Box::new(|data: &[u8]| {
    let name = String::from_utf8_lossy(data);
    tracing::info!("Greeting requested for: {name}");
    format!("Hello, {name}!").into_bytes()
}));

Calling from JavaScript

The JavaScript API depends on the platform:

// Apple (WKWebView)
window.webkit.messageHandlers.greet.postMessage("World");

// Android
window.greet.postMessage("World");

Setting Up a Convenient Bridge

Combine inject_script and add_handler for a clean API that hides platform differences from your web code:

use waterui_webview::ScriptInjectionTime;

// Inject a friendly JavaScript API
webview.inject_script(r#"
    window.myApp = {
        greet: function(name) {
            window.webkit.messageHandlers.greet.postMessage(name);
        }
    };
"#, ScriptInjectionTime::DocumentStart);

// Register the native handler
webview.handle().add_handler("greet", Box::new(|data: &[u8]| {
    let name = String::from_utf8_lossy(data);
    tracing::info!("JS called greet({name})");
    Vec::new()
}));

Removing a Handler

webview.handle().remove_handler("greet");

Cookies

Manage cookies programmatically – useful for authentication flows or session management:

use waterui_webview::cookie::Cookie;

// Set a cookie
let cookie = Cookie::build(("session", "abc123"))
    .domain("example.com")
    .path("/")
    .secure(true)
    .build()
    .unwrap();

webview.handle().set_cookie(cookie);

// Retrieve all cookies
let cookies = webview.handle().get_cookies();
for c in &cookies {
    tracing::info!("Cookie: {} = {}", c.name(), c.value());
}

The cookie crate (re-exported as waterui_webview::cookie) provides the Cookie type.


Redirect Control

Enable or disable HTTP redirect following:

// Imperatively
webview.set_redirects_enabled(false);

// Reactively via a binding
let allow_redirects = Binding::bool(true);
let webview = WebView::new(handle)
    .redirects_enabled(allow_redirects.into_computed());

The redirects_enabled builder method watches the signal and syncs the setting automatically when the value changes.


User Agent

Customize the user agent string sent with requests:

webview.set_user_agent("MyApp/1.0 (WaterUI)");

Complete Example

Let’s put it all together. Here is a minimal in-app browser with back/forward buttons:

use waterui::prelude::*;
use waterui_webview::{WebView, WebViewController};

fn mini_browser() -> impl View {
    use_env(|controller: WebViewController| {
        let webview = controller.open();
        webview.go_to("https://waterui.dev");

        let can_back = webview.can_go_back();
        let can_forward = webview.can_go_forward();

        let back = webview.clone();
        let forward = webview.clone();
        let reload = webview.clone();

        vstack((
            hstack((
                button("Back")
                    .action(move || back.go_back())
                    .disabled(can_back.map(|ok| !ok)),
                button("Forward")
                    .action(move || forward.go_forward())
                    .disabled(can_forward.map(|ok| !ok)),
                button("Refresh").action(move || reload.refresh()),
            )),
            webview,
        ))
    })
}

The disabled modifier accepts the inverted reactive signal, so the back and forward buttons grey out automatically as the navigation history changes.


Platform Considerations

FeatureAppleAndroidDesktop
EngineWKWebView (WebKit)Platform WebViewWIP
JavaScript executionFull supportFull support
Script injectionDocumentStart / DocumentEndDocumentStart / DocumentEnd
Message handlerswebkit.messageHandlerswindow.<name>
CookiesFull supportFull support
Redirect controlFull supportBackend-dependent

WebView is declared as a raw view with StretchAxis::Both, so it expands to fill all available space by default. Wrap it in a .frame() modifier or constrain it with layout containers to control its size.

Downcasting the Handle

When you need access to the platform-specific handle (for example, to configure WKWebView preferences on Apple), you can downcast:

if let Some(native) = webview.handle().downcast_ref::<MyNativeHandle>() {
    // Access platform-specific APIs
}

This is primarily useful for backend authors and advanced platform integration.


What’s Next

You have seen how to embed the entire web inside your app. Next, let’s look at something more focused: Barcodes and QR Codes, where you will generate scannable codes entirely on the GPU.