| Web-site | https://tauri.app |
| Platforms | Linux, macOS, Windows (+ Android, iOS since v2) |
| Renderer | WKWebView (macOS), WebView2 / Chromium (Windows), WebKitGTK (Linux) |
| Download Tauri Chat | Not implemented yet |
Tech stack used for building a showcase app:
| Build system | Cargo + tauri-cli, any bundler for the frontend |
| Languages | Rust (core), TypeScript (UI) |
| Libraries | WRY, TAO, any web framework |
| Editor | VSCode + rust-analyzer |
Tauri is what you get when you take the Electron idea — describe the UI in HTML and CSS — and refuse to ship the browser with it.
There are two halves. The UI half runs in the WebView that the operating system already has: WKWebView on macOS, WebView2 on Windows, WebKitGTK (4.1 in v2) on Linux. The application half is a Rust binary that owns the window, the menus, the filesystem and the process lifecycle. Two crates do the mediating: TAO creates and manages windows, WRY puts a WebView inside them and normalizes the three platform APIs into one.
The consequence that everybody quotes is the size. Electron carries a full Chromium per app — roughly 96 MB compressed, comfortably past 200 MB once node_modules land.
A minimal Tauri app is around a 600 KB installer on macOS, and a real one with a frontend framework and dependencies typically still fits in single-digit megabytes.
Memory follows the same shape: there is no second Chromium process tree, no per-renderer V8 isolate, no Node main process, so resident memory tends to land near half of the equivalent Electron build.
The consequence that people discover later is that “the system WebView” is not one renderer, it is three. On Windows you are testing against Chromium; on macOS and Linux you are testing against two different WebKits, on two different release cadences, neither of which you pin. Browser compatibility testing, which the web had mostly stopped worrying about, comes back as a build matrix.
The two halves talk over an IPC bridge. A Rust function becomes callable from the UI by annotating it:
#[tauri::command]
fn send_message(channel: String, text: String) -> Result<(), String> {
// ...
Ok(())
}
and from the frontend it is an ordinary promise:
import { invoke } from "@tauri-apps/api/core";
await invoke("send_message", { channel: "#webrender", text: "hello" });
That boundary is the actual design decision in a Tauri app. Anything you put on the Rust side is fast, typed and off the UI thread, and costs you a serialization hop plus a rebuild. Anything you leave in TypeScript is hot-reloadable and cheap to change. For a chat client, the split more or less writes itself: rendering, layout and state in the WebView; the socket, the token storage and the notification plumbing in Rust.
The frontend half is the good part of Electron, unchanged. tauri dev runs your normal dev server, hot reload works, and the WebView’s inspector is a right-click away — WKWebView’s Web Inspector on macOS, WebView2 DevTools on Windows, the WebKit inspector on Linux.
The backend half is Rust, and that is the trade. You get a type system and a compiler that makes whole categories of bugs unrepresentable, and you pay for it in compile times — a cold build of the core is minutes, and the incremental rebuild after touching a #[tauri::command] is long enough to break flow in a way that editing a React component never is.
The other cost is googlability. Electron’s answer to almost any question is an npm package and a decade of Stack Overflow. Tauri’s ecosystem is much younger, split between the official plugin set and crates.io, and v1-era answers are actively misleading now that v2 has reorganized the API surface.
Nothing stops you from using fetch and WebSocket from the WebView, and for most of a chat app that is the right answer.
Where it stops working is CORS — the frontend is served from a custom protocol, so third-party APIs that do not know about your origin will refuse you. The escape hatch is to do the request from Rust instead, where there is no origin and no preflight:
#[tauri::command]
async fn list_users(token: String) -> Result<Vec<SlackUser>, String> {
reqwest::Client::new()
.get("https://slack.com/api/users.list")
.bearer_auth(token)
.send()
.await
.map_err(|e| e.to_string())?
.json()
.await
.map_err(|e| e.to_string())
}
This is a real advantage over the browser and a real annoyance over Electron: you get an unrestricted HTTP client and native TLS, but every call you move across the bridge is a Rust function you now maintain.
Long-lived connections work the same way — hold the socket in Rust, and push into the WebView with an event rather than returning from a command.
This is where the Rust half earns its place. Tray icons, global shortcuts, native menus, notifications, single-instance enforcement, deep links and autostart are official plugins rather than things you reimplement.
The bundler is unusually complete for how young the project is: .app, .dmg, .deb, .rpm, .AppImage, NSIS .exe and WiX .msi, with signing and an updater built in.
The Windows caveat is WebView2. It is preinstalled on Windows 11 and self-updates, but on older Windows 10 builds it may be missing, so the generated installer either ensures it or bundles the bootstrapper depending on bundle.windows.webviewInstallMode.
v2 also replaced v1’s global allowlist with per-window capabilities — each command has to be granted to a window explicitly. It is more work to configure and it means a compromised remote page in one window cannot reach the whole native API.
Tauri inherits the web’s accessibility story, which — as the Electron page argues — is the strongest one available here.
The DOM is a semantic tree, and each system WebView already maps it onto the platform’s accessibility API: WKWebView to NSAccessibility, WebView2 to UI Automation, WebKitGTK to AT-SPI. Correct HTML and aria- attributes are picked up by VoiceOver, Narrator and Orca with no bridge to write, which is precisely what an immediate mode GUI cannot offer.
The nuance is that you are now testing three implementations. WebKit and Chromium do not expose identical trees, so an app that reads correctly under VoiceOver is not automatically correct under Narrator.