Skip to content

Webview Runtime

Tauri itself does not create windows or webviews. It delegates that to a webview runtime, a crate that implements the tauri_runtime::Runtime trait. The same app code runs on any runtime. Tauri 3.0 ships two:

tauri-runtime-wry tauri-runtime-cef
Engine The webview installed on the system: WebKitGTK on Linux, WebView2 (Chromium) on Windows, WKWebView on macOS, iOS, and Android The Chromium Embedded Framework, shipped with the app
Platforms Windows, macOS, Linux, Android, iOS Windows, macOS, Linux
Bundle size Small: the engine is already on the user’s machine Large: the app ships its own copy of Chromium
Rendering behavior Varies with the platform and the version of the installed webview The same Chromium version on every platform, chosen by you
Windowing tao winit

wry is the runtime every Tauri 2.0 app uses, and the one the templates generated by create-tauri-app and tauri init pick, so the examples on this page use it. Read the CEF guide to learn what changes when you pick CEF.

Add the runtime crate to your dependencies and pass its attributes to tauri::Builder::runtime:

src-tauri/Cargo.toml
[dependencies]
tauri = "3.0.0-alpha.0"
tauri-runtime-wry = "3.0.0-alpha.0"
src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.runtime(tauri_runtime_wry::Wry::default())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

The attributes type (Wry or Cef) both selects the runtime and configures it. wry has no runtime-wide options, so Wry is a unit-like struct. Cef has builder methods for Chromium’s process sandbox, its command line, its profile preferences, and more. The attributes also read the Tauri configuration before the runtime starts, which is how Cef picks up the deep link schemes of the deep-link plugin.

Building the app without selecting a runtime fails with tauri_runtime::Error::RuntimeNotConfigured.

The dependency is also how the rest of the toolchain learns which runtime the app uses. The Tauri CLI and tauri-build detect the runtime from the tauri-runtime-wry and tauri-runtime-cef dependencies in the app manifest, and only ship or install what that runtime needs: the WebView2 bootstrapper and the webkit2gtk package dependencies for wry, or the CEF binary distribution and code signing entitlements for CEF. A binary can only run one runtime. When both crates are linked, the CLI assumes CEF, since it is the runtime that must be shipped with the app.

Tauri’s types are generic over the runtime: AppHandle<R>, Window<R>, Webview<R>, WebviewWindow<R>, Builder<R>, and so on, where R: tauri::Runtime. In Tauri 3.0 that generic parameter defaults to tauri::DynRuntime, a type-erased runtime that wraps the selected runtime behind trait objects.

tauri::Builder::default() builds a Builder<DynRuntime>, whose runtime method accepts the attributes of any runtime crate. Since DynRuntime is the default, app code never has to name the runtime: tauri::AppHandle, tauri::WebviewWindow, and the other types just work, and swapping wry for CEF is a change to Cargo.toml and to the runtime call.

#[tauri::command]
fn greet(app: tauri::AppHandle, window: tauri::WebviewWindow) -> String {
format!("Hello from {} on {}!", window.label(), app.package_info().name)
}

This is the model the templates use, and the one to prefer unless you have a reason not to. The cost is a dynamic dispatch (a virtual call) between the tauri crate and the runtime for every runtime operation, which is negligible next to the work those operations do.

If you prefer a fully monomorphized app, name the concrete runtime type on the builder. Its runtime method then only accepts the attributes of that runtime, and all the generic types must name it too:

use tauri_runtime_wry::WryRuntime;
#[tauri::command]
fn greet(app: tauri::AppHandle<WryRuntime>) -> String {
app.package_info().name.clone()
}
pub fn run() {
tauri::Builder::<WryRuntime>::new()
.runtime(tauri_runtime_wry::Wry::default())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

The concrete runtime type of CEF is tauri_runtime_cef::CefRuntime.

Plugins and libraries should keep taking the runtime as a generic parameter, so they work with both models and with any runtime:

use tauri::{AppHandle, Runtime};
pub fn do_something<R: Runtime>(app: &AppHandle<R>) {
// ...
}

R is DynRuntime in an app using dynamic dispatch, and the concrete runtime type otherwise.

A few Cargo features must be enabled on the runtime crate rather than on tauri, because the runtime is the crate that implements them. Enabling them on the runtime crate also enables them on tauri:

  • devtools: enables the web inspector and Webview::open_devtools in release builds.
  • macos-private-api: enables the transparent window and fullScreenEnabled features, which use private macOS APIs.
  • unstable: enables the multiwebview APIs and the runtime’s extension traits for tauri::webview::WebviewBuilder.
src-tauri/Cargo.toml
[dependencies]
tauri = "3.0.0-alpha.0"
tauri-runtime-wry = { version = "3.0.0-alpha.0", features = ["devtools"] }

tauri-runtime-wry also has the x11 and dbus features (enabled by default), macos-proxy, and tracing, which were features of the tauri crate in 2.0.

The portable API of the tauri crate covers what every runtime can do. What only one runtime can do is exposed by that runtime’s crate as extension traits on the Tauri types. Once the trait is imported, the calls look like methods of AppHandle, Webview, or WebviewWindowBuilder.

The traits are implemented both for the concrete runtime type and for tauri::DynRuntime. With dynamic dispatch, calling a wry method while the app runs on CEF (or vice versa) fails at runtime with tauri_runtime::Error::RuntimeTypeMismatch. With static dispatch it does not compile.

  • AppHandleWryExt: create_tao_window, send_tao_window_event, and wry_plugin.
  • AppWryExt: wry_plugin on App.
  • WebviewWryExt: with_wry_webview, which runs a closure with the tauri_runtime_wry::Webview platform handle (the webkit2gtk::WebView, the WebView2 controller, or the WKWebView pointers).
  • WebviewWindowBuilderWryExt and WebviewBuilderWryExt (the latter behind the unstable feature): with_environment (Windows), with_related_view (Linux), and with_webview_configuration (macOS), used to link the webview of a new window to its opener.

The tao and wry crates are re-exported as tauri_runtime_wry::tao and tauri_runtime_wry::wry.

use tauri_runtime_wry::{AppHandleWryExt, tao::window::WindowBuilder};
fn create_native_window(app: &tauri::AppHandle) -> tauri::Result<()> {
app.create_tao_window(|| ("native".to_string(), WindowBuilder::new().with_title("A tao window")))?;
Ok(())
}
  • WebviewCefExt: send_dev_tools_message, on_dev_tools_protocol, and with_cef_webview.
  • WebviewWindowBuilderCefExt and WebviewBuilderCefExt (the latter behind the unstable feature): browser_runtime_style, allow_chrome_commands, on_console_message, on_frame_event, and with_browser_settings.
  • AsCefWindowOpener: reads the CEF-specific opener information in an on_new_window handler.

See the CEF guide for what each of them does.

Webview::with_webview is portable. It runs a closure on the main thread with a PlatformWebview, which dereferences to the webview handle type defined by the runtime in use. With DynRuntime that type is only known at runtime, so use PlatformWebview::downcast_ref to reach it. It returns None when the app runs on another runtime:

use tauri::Manager;
fn set_zoom(app: &tauri::AppHandle) -> tauri::Result<()> {
app.get_webview_window("main").unwrap().with_webview(|webview| {
if let Some(webview) = webview.downcast_ref::<tauri_runtime_wry::Webview>() {
#[cfg(target_os = "linux")]
{
use webkit2gtk::WebViewExt;
webview.inner().set_zoom_level(2.);
}
#[cfg(windows)]
unsafe {
webview.controller().SetZoomFactor(2.).unwrap();
}
}
})
}

WebviewWryExt::with_wry_webview and WebviewCefExt::with_cef_webview do the downcast for you.

The version of the webview engine depends on the runtime, so it is a method of the app instead of a free function. App::webview_version and AppHandle::webview_version return the WebKitGTK, WebView2, or WebKit version with wry, and the Chromium version with CEF.

The runtime defines the URL your frontend is served from, tauri://localhost or http://tauri.localhost, through RuntimeHandle::custom_scheme_url, and the convertFileSrc JavaScript API follows it. wry uses http://tauri.localhost on Windows and Android and tauri://localhost elsewhere, as in Tauri 2.0. CEF uses http://tauri.localhost on every platform (or https:// when app > windows > useHttpsScheme is enabled), and tauri::test::MockRuntime uses tauri://localhost on every platform.

wry is built on GTK 3 (through WebKitGTK) and CEF on GTK 4, and the two cannot be initialized in the same process. The tauri crate picks its GTK bindings at compile time through the gtk3 and gtk4 Cargo features, which the runtime crates enable for you: tauri-runtime-wry enables gtk3 and tauri-runtime-cef enables gtk4.

The GTK APIs of tauri (Window::gtk_window, Window::default_vbox, WindowBuilder::transient_for_raw, and the menu integration) are gated behind those features. A plugin that depends on tauri alone and uses them must enable one of the features explicitly. Enabling both selects GTK 4. Under a runtime whose GTK version is not the selected one, the GTK APIs fail with tauri::Error::GtkVersionMismatch instead of reinterpreting the runtime’s window objects.

Without either feature, that is, without a runtime crate, tauri compiles on Linux and BSD with no GTK dependency at all.

The tray icon does not depend on the runtime’s GTK version. It uses the ksni backend, a pure D-Bus implementation of the StatusNotifierItem specification, so it needs no libayatana-appindicator system dependency. Enable the linux-libappindicator feature of tauri to use libappindicator instead.

A runtime is a crate that implements the traits of the tauri-runtime crate: Runtime, RuntimeHandle, WindowDispatch, WebviewDispatch, and WindowBuilder, plus a RuntimeInitAttrs type that selects it. To work with tauri::Builder::default(), the attributes type must also implement From<Self> for tauri_runtime::dynamic::DynRuntimeInitAttrs. The crate should expose its runtime-specific APIs as extension traits implemented for both the concrete runtime and tauri::DynRuntime, downcasting through tauri_runtime::dynamic::{DynRuntimeHandle, DynWebviewDispatcher, DynWindowDispatcher, DynWebview}.

tauri-runtime-wry and tauri-runtime-cef are the reference implementations. The Tauri CLI treats any other runtime as Other and does nothing runtime-specific when building or bundling the app.


© 2026 Tauri Contributors. CC-BY / MIT