Chromium Embedded Framework
The Chromium Embedded Framework (CEF) embeds a full Chromium browser in your app.
Tauri 3.0 supports it as a webview runtime through the tauri-runtime-cef crate, as an alternative to the system webview used by tauri-runtime-wry.
CEF is the right choice when the system webview is not enough:
- Consistent rendering: every user runs the same Chromium version on Windows, macOS, and Linux, and you choose which one. You no longer need to work around platform-specific CSS bugs, missing web APIs on older WebKit releases, or the webview version matrix.
- Chromium features: the Chrome DevTools Protocol, Chromium’s process sandbox, its permission and content settings model, and its command line switches are all available from Rust.
It also has costs:
- Size: the app ships its own copy of Chromium, so bundles are hundreds of megabytes larger than with wry, and the app uses more memory at runtime.
- Desktop only: CEF supports Windows, macOS, and Linux. Mobile targets keep using wry.
- Build time: the first build downloads the CEF binary distribution (about 1 GB) and, on Windows and macOS, compiles CEF’s C++ wrapper library.
Prerequisites
Section titled “Prerequisites”In addition to the Tauri prerequisites, you need:
- CMake and Ninja on Windows and macOS. They compile CEF’s
libcef_dll_wrapperlibrary when thecefcrate is built. - Disk space for the CEF binary distribution. The first build downloads it (about 1 GB per platform) into a cache directory shared by every Tauri project on the machine:
~/.cache/tauri-cefon Linux,~/Library/Caches/tauri-cefon macOS, and%LOCALAPPDATA%\tauri-cefon Windows. Set theCEF_PATHenvironment variable to use a different directory, or to point to a CEF binary distribution you downloaded and extracted yourself.
On Linux, Chromium’s process sandbox works out of the box on distributions that allow unprivileged user namespaces. See Linux for the others.
Dependencies
Section titled “Dependencies”Replace tauri-runtime-wry with tauri-runtime-cef in your Cargo manifest:
[dependencies]tauri = "3.0.0-alpha.0"tauri-runtime-wry = "3.0.0-alpha.0"tauri-runtime-cef = "3.0.0-alpha.0"This dependency is what selects CEF for the whole toolchain. tauri-build and the Tauri CLI detect it in the manifest, and only then ship the CEF binary distribution with the bundle, sign the app with the entitlements Chromium needs on macOS, and run the app from inside an .app bundle in tauri dev.
The devtools, macos-private-api, and unstable Cargo features are enabled on the runtime crate. See Runtime Crate Features.
Entry Point
Section titled “Entry Point”A CEF app is a single executable that also serves as its own renderer, GPU, network, and utility process: Chromium launches the same binary again with a --type= switch for each of them.
The #[tauri_runtime_cef::cef_entry_point] attribute on your main function runs those helper processes and returns before the Tauri app is built, so they never create windows of their own:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[tauri_runtime_cef::cef_entry_point]fn main() { app_lib::run();}The attribute expands to a check for the --type= switch that calls tauri_runtime_cef::run_cef_helper_process() when the switch is present. That function is public for apps whose entry point cannot take an attribute. Nothing in main runs before the check, so put the attribute on the real entry point rather than on a function it calls.
Select the Runtime
Section titled “Select the Runtime”Pass tauri_runtime_cef::Cef to tauri::Builder::runtime:
pub fn run() { tauri::Builder::default() .runtime(tauri_runtime_wry::Wry::default()) .runtime(tauri_runtime_cef::Cef::default()) .run(tauri::generate_context!()) .expect("error while running tauri application");}From here on, tauri dev and tauri build work as usual, and the portable Tauri APIs (commands, events, windows, menus, the tray icon, and plugins) behave the same as on wry.
The frontend is served from http://tauri.localhost on every platform (or https:// when app > windows > useHttpsScheme is enabled), which is the URL the wry runtime already uses on Windows.
Configuring Chromium
Section titled “Configuring Chromium”Cef is a builder that configures Chromium before it starts. Its defaults suit an app rather than a browser: release builds ignore Chromium switches on their own command line, the remote debugging server is off, Chrome’s “Save password?”, autofill, and translate bubbles are disabled, and development builds skip the OS secret store so that rebuilds do not prompt for the keychain.
use tauri_runtime_cef::{Cef, SandboxPolicy, SecretStorage};
let cef = Cef::default() // Never run unsandboxed, even when that means Chromium aborts at startup. .sandbox(SandboxPolicy::Required) // Always use the OS secret store, even in development builds. .secret_storage(SecretStorage::System) // Chromium profile preferences, boolean or of any type. .profile_preference("safebrowsing.enabled", false) .profile_preference_value("download.default_directory", "/tmp/downloads") // Raw Chromium switches, applied to the browser process only. .command_line_arg("disable-gpu", None::<String>) // The `cef::Settings` struct as CEF receives it, for what `Cef` has no method for. .with_settings(|settings| settings.persist_session_cookies = 1);
tauri::Builder::default().runtime(cef);The Cef methods and the enums they take are documented in the crate. The sections below group the ones you are most likely to need.
Process Sandbox
Section titled “Process Sandbox”sandbox(SandboxPolicy) controls Chromium’s process sandbox, which isolates the renderer processes from the operating system. The default, SandboxPolicy::Auto, keeps the sandbox wherever it can and logs a warning where it cannot. Required refuses to start without it, and Disabled never uses it.
- macOS always keeps the sandbox: the helper apps load
libcef_sandboxbefore the framework. - Linux keeps it on distributions with unprivileged user namespaces or with the setuid
chrome-sandboxhelper, which the Debian and RPM packages install. An AppImage cannot ship a setuid helper, so on a system that also restricts user namespaces (Ubuntu 23.10 and later, through AppArmor),Autodrops the sandbox rather than aborting with Chromium’s “No usable sandbox!” error. - Windows currently runs unsandboxed regardless of the policy. Since Chromium M138, only a binary built with Chromium’s own toolchain can create the sandbox broker, and a Tauri app is not one.
Autologs a warning andRequiredfails to start.
Storage
Section titled “Storage”root_cache_pathis the directory Chromium writes its profile to. It defaults to{user cache}/{identifier}/cef.secret_storage(SecretStorage)selects the key that encrypts cookies and saved passwords at rest.Auto(the default) uses the OS secret store in release builds and Chromium’s mock keychain in development builds, so an ad-hoc signed development build on macOS does not prompt for the keychain password after every rebuild.Systemalways uses the OS store.Mocknever does, which is what a release build needs when it runs where there is no secret store at all, such as a container or a CI image. Switching between the two makes the cookies stored under the previous key unreadable.persist_session_cookieskeeps session cookies across restarts, for an app that should keep users signed in.
Command Line and Features
Section titled “Command Line and Features”command_line_argandcommand_line_argsappend raw Chromium switches. They apply to the browser process only, since Chromium forwards to each child process the switches it needs, and they are unaffected byallow_chromium_command_line_args.enable_featuresanddisable_featuresadd names to Chromium’s--enable-featuresand--disable-featureslists while keeping the entries CEF put there. A raw--disable-featuresswitch would replace the whole list.javascript_flagspasses flags to V8, such as--max-old-space-size=512.allow_chromium_command_line_argslets Chromium read switches from the process command line in release builds. It is off by default, so that whoever can start the shipped executable cannot also start it with--remote-debugging-portor--disable-web-security. Development builds always honor the command line.
Preferences and Content Settings
Section titled “Preferences and Content Settings”profile_preferenceandprofile_preference_valueset Chromium profile preferences on every webview’s request context. They are applied after the runtime’s own defaults, so they can turn a preference the runtime disabled (credentials_enable_service,autofill.profile_enabled,translate.enabled, and others) back on. Preferences worth knowing includeprinting.enabled,download.default_directory,download.prompt_for_download,enable_do_not_track,dns_over_https.mode,profile.cookie_controls_mode, and thehardware.audio_capture_enabledandhardware.video_capture_enabledkill switches.global_preferencesets a preference in Chromium’s local state, the store shared by every profile, such ashardware_acceleration_mode.enabled.default_content_settingsets the default answer of a content setting for every origin. This is the app-wide policy, as opposed to the per-requeston_permission_requesthandler. WithContentSettingValues::BLOCKa page cannot ask at all, and withALLOWthe permission is granted without a prompt. BlockingContentSettingTypes::JAVASCRIPT_JITruns V8 without its optimizing compilers, at a cost in JavaScript performance.- Typed shortcuts exist for the common cases:
proxy(ProxyConfig),autoplay(AutoplayPolicy),webrtc_ip_handling(WebRtcIpHandling),certificate_errors(CertificateErrorPolicy),spell_checking,safe_browsing, andcomponent_updates.
Developer Tools and Debugging
Section titled “Developer Tools and Debugging”devtools(DevToolsPolicy)decides whether the app may open a DevTools window at all.Auto(the default) allows it in a build that could open one anyway, that is, a debug build or one with thedevtoolsfeature, and refuses it otherwise. It combines with the per-webviewdevtoolsattribute.remote_debugging(RemoteDebugging)runs Chromium’s DevTools protocol server, the one behindchrome://inspect. It isDisabledby default.Port { port, allowed_origins }listens on a TCP port, reachable by every process on the machine, andPipespeaks the protocol over inherited file descriptors, reachable only by the process that launched the app.debug_environment(DebugEnvironment)decides whether Chromium reads its diagnostic environment variables, such asSSLKEYLOGFILE. By default they are honored in development builds and refused in release builds.log_file,log_severity, andlog_itemscontrol the Chromium log, which is written tocef.loginside the cache directory by default.
Identity
Section titled “Identity”user_agent_productinserts a product token such asMyApp/1.2.0into Chromium’s own User-Agent string.user_agentreplaces the whole string, dropping the Chrome and platform tokens that sites branch on.accept_language_listis sent as theAccept-Languageheader and reported bynavigator.languages.localeselects the locale Chromium loads its own UI strings from (context menus, error pages, and form controls). Leave it unset: the bundler ships only theen-USlocale pack, so naming another locale leaves Chromium unable to load its strings.chrome_policy_idenables Chrome enterprise policy management. Policies are read from the registry key, bundle identifier, or/etc/optdirectory it names.
Deep Links
Section titled “Deep Links”Deep links delivered to an already running app go through Chromium’s process singleton, which relays the command line of the new process. CEF clears that command line, and the runtime restores the deep link URL onto it so that RunEvent::Opened still fires.
The schemes declared under plugins > deep-link > desktop in tauri.conf.json are picked up automatically. deep_link_schemes adds schemes an app registers itself.
Webview APIs
Section titled “Webview APIs”The CEF-specific webview APIs are extension traits on the Tauri types, implemented for both CefRuntime and the type-erased tauri::DynRuntime, so nothing has to name the runtime type. On a non-CEF runtime they fail with tauri_runtime::Error::RuntimeTypeMismatch.
Building a Webview
Section titled “Building a Webview”WebviewWindowBuilderCefExt adds these methods to tauri::WebviewWindowBuilder:
browser_runtime_style(RuntimeStyle)picksRuntimeStyle::ChromeorRuntimeStyle::Alloyfor each browser. The runtime’s defaults are written for the Chrome style. The Alloy style has no Chrome UI at all, so it lacks both Chrome’s accelerator table and its desktop media picker.allow_chrome_commands([ChromeCommandGroup, ...])gives back groups of Chrome keyboard shortcuts. A Chrome style browser keeps its whole accelerator table live even when hosted as a child view with no browser UI, so the runtime swallows the families that make no sense in an app window: new window and tab, the tab strip, history, downloads, settings, print, save page, view source, and the omnibox. Name a group to restore that family, for exampleChromeCommandGroup::Documentfor Ctrl+P andChromeCommandGroup::Historyfor Alt+Left.on_console_messageobserves the messages the renderer writes to the JavaScript console, without DevTools having to be open.on_frame_eventobserves the native lifecycle of every frame of the browser, child frames included: creation, attachment, navigation start, document commit, navigation failure, renderer termination, and so on. It does not replace the navigation policy ofon_navigation.with_browser_settingsgives you a last look at thecef::BrowserSettingsbefore the browser is created, to reach the fields Tauri has no attribute for: the font families and sizes,remote_fonts,local_storage,webgl, anddefault_encoding.
use tauri::{WebviewUrl, WebviewWindowBuilder};use tauri_runtime_cef::{ChromeCommandGroup, RuntimeStyle, WebviewWindowBuilderCefExt};
tauri::Builder::default() .runtime(tauri_runtime_cef::Cef::default()) .setup(|app| { WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) .browser_runtime_style(RuntimeStyle::Chrome) // Ctrl+P prints and Alt+Left goes back, as a user expects. .allow_chrome_commands([ChromeCommandGroup::Document, ChromeCommandGroup::History]) .on_console_message(|message| { println!("[{}:{}] {:?}: {}", message.source, message.line, message.level, message.message); }) .build()?; Ok(()) });The same methods are available on tauri::webview::WebviewBuilder through WebviewBuilderCefExt, for a window hosting several webviews. Like the multiwebview API it extends, that trait is behind the unstable feature.
Chrome DevTools Protocol
Section titled “Chrome DevTools Protocol”WebviewCefExt lets you drive a webview through the Chrome DevTools Protocol:
send_dev_tools_messagesends one JSON request to the DevTools agent of the webview’s browser.on_dev_tools_protocolobserves the protocol traffic of that browser:DevToolsProtocol::Event { method, params }for notifications,DevToolsProtocol::MethodResult { message_id, success, result }for responses, and the rawDevToolsProtocol::Messagefor both.
Every observer on a browser sees every response, including the responses to requests the runtime itself sends. Take the request id from allocate_devtools_message_id() rather than from a counter of your own, and match the responses against it:
use tauri::Manager;use tauri_runtime_cef::{DevToolsProtocol, WebviewCefExt, allocate_devtools_message_id};
fn enable_page_events(app: &tauri::AppHandle) -> tauri::Result<()> { let webview = app.get_webview_window("main").unwrap(); let message_id = allocate_devtools_message_id()?;
webview.on_dev_tools_protocol(move |protocol| match protocol { DevToolsProtocol::Event { method, params } => { println!("{method}: {}", String::from_utf8_lossy(¶ms)); } DevToolsProtocol::MethodResult { message_id: id, success, .. } if id == message_id => { println!("Page.enable success={success}"); } _ => {} })?;
let message = format!(r#"{{"id":{message_id},"method":"Page.enable","params":{{}}}}"#); webview.send_dev_tools_message(message.as_bytes())?; Ok(())}The observer is scoped to the webview’s own native browser. A popup that CEF opened itself (see Popups and Permissions) is a separate browser, and its traffic never reaches the observer.
Native Handle
Section titled “Native Handle”WebviewCefExt::with_cef_webview runs a closure on CEF’s UI thread with the tauri_runtime_cef::Webview handle. The handle exposes the raw cef::Browser through browser(), along with a snapshot of the native state CEF sampled for that callback: the browser identity, the admitted document, the native window identity, visibility and bounds, observed JavaScript dialogs, and the browser’s CEF-owned popups through popups().
The closure cannot return a value, so hand the result back through a channel.
tauri_runtime_cef::cef re-exports the cef crate for the types those APIs take and return. It follows the Chromium Embedded Framework interface and offers no API stability, so pin the runtime to a minor version if you reach into it.
Popups and Permissions
Section titled “Popups and Permissions”The portable on_new_window handler works as on wry, with one CEF-specific addition: features.opener().as_cef_window_opener() (from the AsCefWindowOpener trait) returns the opener’s main-frame URL directly. This is safer than a blocking webview getter in a callback that runs on CEF’s UI thread.
Answering NewWindowResponse::Allow leaves the popup to CEF, which opens a native browser of its own with no Tauri window label. Answering NewWindowResponse::Create keeps it a Tauri window.
The portable on_permission_request handler is honored: an Allow grants the permission without Chrome’s prompt, and a Deny refuses it without one. Three things are worth knowing:
- Chromium consults the handler only while the stored content setting still says “ask”, and an answer persists the decision to the on-disk profile. The handler therefore runs once per origin and permission, across restarts. Camera and microphone are the exception: every
getUserMedia()call reaches the handler. - Permissions Tauri has no kind for (storage access, FedCM, protocol handler registration, idle detection, and WebXR) arrive as
PermissionKind::Other. AnswerPermissionResponse::Defaultfor the ones you did not mean to decide about. Denying the whole set breaks third-party sign-in flows. PermissionKind::DisplayCaptureis never granted by anAllow. An allow with no named source would share the whole desktop without a picker, so the runtime hands the request back to Chromium’s picker.
Development
Section titled “Development”tauri dev builds and runs the app as usual. What differs from wry:
- The CLI sets
CEF_PATHfor the Cargo build to{user cache}/tauri-cef(or your own value), so that thecefcrate’s build script finds the CEF binary distribution there and downloads it on the first build. - On Windows and Linux, the build script copies the CEF binaries and resources next to the executable in the Cargo target directory, where Chromium expects them.
- On macOS, the app runs from inside an
.appbundle. CEF launches its helper apps by path from inside the bundle, so it cannot run as a bare executable. The CLI builds, bundles, and launches the bundled executable, which makestauri devtake a little longer than with wry.
DevTools open with F12 or Ctrl+Shift+I and through the context menu, and Webview::open_devtools works as on wry. Release builds need the devtools feature of tauri-runtime-cef, subject to the DevToolsPolicy.
Bundling
Section titled “Bundling”tauri build ships the CEF binary distribution with your app:
- Windows and Linux:
libcef, its resources, and theen-USlocale pack are shipped with the app, next to the executable on Windows and in the AppImage, and under/usr/share/<product name>in the Debian and RPM packages. The Windows installers skip the WebView2 installation step, and the Debian and RPM packages do not depend on webkit2gtk. - macOS:
Chromium Embedded Framework.frameworkis placed in the app bundle’sContents/Frameworks, along with five helper apps (<App> Helper,Helper (GPU),Helper (Renderer),Helper (Plugin), andHelper (Alerts)). The bundler compiles the helper executable at bundle time against thecefcrate version your app resolved. The app is signed with the entitlements Chromium’s JIT needs under the hardened runtime (com.apple.security.cs.allow-jit,com.apple.security.cs.allow-unsigned-executable-memory, andcom.apple.security.cs.disable-library-validation), so code signing and notarization work as usual.
The Debian and RPM packages install Chromium’s setuid chrome-sandbox helper with mode 4755, so packaged apps always have a working sandbox.
An AppImage cannot: its payload is mounted nosuid, so it relies on unprivileged user namespaces, which some distributions restrict. See Process Sandbox for what happens there.
Example
Section titled “Example”The CEF example in the Tauri repository is a full app that exercises every API on this page, with comments on the Rust side of each.
© 2026 Tauri Contributors. CC-BY / MIT