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.
Selecting a Runtime
Section titled “Selecting a Runtime”Add the runtime crate to your dependencies and pass its attributes to tauri::Builder::runtime:
[dependencies]tauri = "3.0.0-alpha.0"tauri-runtime-wry = "3.0.0-alpha.0"#[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");}[dependencies]tauri = "3.0.0-alpha.0"tauri-runtime-cef = "3.0.0-alpha.0"#[tauri_runtime_cef::cef_entry_point]fn main() { tauri::Builder::default() .runtime(tauri_runtime_cef::Cef::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.
Dynamic and Static Dispatch
Section titled “Dynamic and Static Dispatch”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.
Dynamic Dispatch (Default)
Section titled “Dynamic Dispatch (Default)”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.
Static Dispatch
Section titled “Static Dispatch”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.
Generic Code
Section titled “Generic Code”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.
Runtime Crate Features
Section titled “Runtime Crate Features”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 andWebview::open_devtoolsin release builds.macos-private-api: enables the transparent window andfullScreenEnabledfeatures, which use private macOS APIs.unstable: enables the multiwebview APIs and the runtime’s extension traits fortauri::webview::WebviewBuilder.
[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.
Runtime-Specific APIs
Section titled “Runtime-Specific APIs”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, andwry_plugin.AppWryExt:wry_pluginonApp.WebviewWryExt:with_wry_webview, which runs a closure with thetauri_runtime_wry::Webviewplatform handle (thewebkit2gtk::WebView, the WebView2 controller, or theWKWebViewpointers).WebviewWindowBuilderWryExtandWebviewBuilderWryExt(the latter behind theunstablefeature):with_environment(Windows),with_related_view(Linux), andwith_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, andwith_cef_webview.WebviewWindowBuilderCefExtandWebviewBuilderCefExt(the latter behind theunstablefeature):browser_runtime_style,allow_chrome_commands,on_console_message,on_frame_event, andwith_browser_settings.AsCefWindowOpener: reads the CEF-specific opener information in anon_new_windowhandler.
See the CEF guide for what each of them does.
Platform Webview Handle
Section titled “Platform Webview Handle”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.
Webview Version
Section titled “Webview Version”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.
Custom Scheme URL
Section titled “Custom Scheme URL”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.
GTK Version
Section titled “GTK Version”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.
Tray Icon
Section titled “Tray Icon”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.
Implementing a Runtime
Section titled “Implementing a Runtime”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