EmpireUI
Get Pro
← Blog9 min read#tauri#desktop#react

Tauri + React: Build Desktop Apps With Web Technologies

Tauri lets you ship native desktop apps using React and TypeScript, with a Rust core that's 10x smaller than Electron. Here's how to actually build one.

desktop monitor showing code editor with React application running

Why Tauri Instead of Electron

Electron ships Chromium and Node.js inside every app you publish. That's fine if you're building something like VS Code, but for most tools it's absurd overhead — a "Hello World" Electron app easily weighs 150 MB. Tauri takes a completely different bet: use the OS's own webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux) and write the native layer in Rust. The same app in Tauri 2.0 ships at 3-10 MB. That's not a rounding error, that's a paradigm shift.

Honestly, the pitch is almost too good on paper. Smaller binaries, lower memory usage, a proper Rust backend with real systems access, and you still write your entire UI in React and TypeScript. There's no new frontend framework to learn. You can take an existing Vite + React project and wrap it in a Tauri shell in under 20 minutes.

That said, the tradeoff is real: the webview is not the same across platforms. Safari's WebKit on macOS might render something slightly differently than Chromium would, especially around newer CSS features. In practice, for app-style UIs — forms, dashboards, modals, settings panels — this never bites you. It's only worth worrying about if you're pushing experimental CSS that WebKit hasn't shipped yet.

Worth noting: Tauri 2.0 (released late 2024) is a major rewrite. Plugin architecture changed, the security model tightened considerably, and mobile targets (iOS and Android) are now first-class. If you're reading an older Tauri tutorial from 2022, a lot of the API surface has moved.

Setting Up Your First Tauri + React Project

You need Rust installed before anything else. Run rustup — the official installer — and let it pull down the stable toolchain. On macOS you'll also want Xcode Command Line Tools. On Windows, install Microsoft C++ Build Tools (the WebView2 runtime ships with Windows 11 already). This is the one part that feels like ops work, not frontend work, but you only do it once.

Then scaffold the project. The create-tauri-app CLI handles both the Rust side and your choice of frontend framework: ``bash npm create tauri-app@latest my-desktop-app # pick: React, TypeScript, Vite cd my-desktop-app npm install npm run tauri dev `` That last command spins up Vite's dev server and a native desktop window simultaneously. Hot module reload works exactly the same as it would in a browser — save a file, the window updates instantly. No rebuild cycle.

The project structure you end up with has two roots: src/ for your React app (standard Vite project) and src-tauri/ for the Rust core. The Rust side has src-tauri/src/main.rs (your app entry point), src-tauri/tauri.conf.json (config), and src-tauri/Cargo.toml (Rust dependencies). You mostly stay in the Rust directory only when you need to add native capabilities.

Quick aside: tauri.conf.json is where you set your app's window size, title bar behavior, and permissions. In Tauri 2.0 the permissions model is capability-based — you explicitly declare what each window can access. A window that only shows a dashboard can be locked down to zero filesystem access. This is genuinely better than Electron's "everything is allowed" default.

Communicating Between React and Rust

The bridge between your React frontend and the Rust backend is called the command system. You define functions in Rust with the #[tauri::command] attribute, register them in your app builder, and call them from TypeScript using invoke(). It's typed on both sides if you put in a bit of work, and it's async by default.

Here's the simplest possible example — a Rust command that reads a file and returns its contents to React: ``rust // src-tauri/src/main.rs use tauri::command; #[command] async fn read_file(path: String) -> Result<String, String> { std::fs::read_to_string(&path) .map_err(|e| e.to_string()) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![read_file]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ` And on the React side: `tsx import { invoke } from '@tauri-apps/api/core'; async function loadConfig() { try { const contents = await invoke<string>('read_file', { path: '/Users/me/.config/myapp/settings.json', }); return JSON.parse(contents); } catch (err) { console.error('Failed to read config:', err); } } ``

The invoke function is fully typed in TypeScript — you pass a generic to tell it what the return type should be. Errors propagate as rejected promises. In practice you'd add Zod validation on the TypeScript side before trusting whatever comes back from the filesystem, but the plumbing itself is clean.

For real-time events going from Rust to React (think: file watcher updates, progress bars, background job status), Tauri has an event system. Emit from Rust with app_handle.emit("file-changed", payload), listen in React with listen('file-changed', (event) => ...). It's basically a pub/sub bus across the language boundary. Works well. You will use it constantly once you start building anything non-trivial.

Building a Real UI: React Components Inside Tauri

Here's something people underestimate: the Tauri window is just a browser. Every React pattern you already know works unchanged — hooks, context, Suspense, React Query, all of it. You're not giving anything up on the frontend side. Your existing component library comes along for the ride.

Look, if you're building a desktop app's settings panel or dashboard, you want it to look polished. That's where Empire UI's component library pays off — you get production-quality cards, modals, tabs, and form inputs without building them from scratch. The glassmorphism components look particularly good in desktop app contexts where you can control the background entirely. A 1px frosted sidebar panel inside a dark-mode Tauri window is hard to beat.

Window chrome is worth thinking about carefully. Tauri 2.0 lets you go completely frameless ("decorations": false in tauri.conf.json) and implement your own title bar in React. That means drag regions, minimize/maximize/close buttons — all custom HTML. You can match your app's visual style precisely instead of fighting the OS chrome: ``tsx // CustomTitleBar.tsx import { getCurrentWindow } from '@tauri-apps/api/window'; export function TitleBar() { const win = getCurrentWindow(); return ( <div data-tauri-drag-region className="h-8 flex items-center justify-between px-3 bg-zinc-900" > <span className="text-sm text-zinc-400">My App</span> <div className="flex gap-1"> <button onClick={() => win.minimize()} className="w-3 h-3 rounded-full bg-yellow-400" /> <button onClick={() => win.toggleMaximize()} className="w-3 h-3 rounded-full bg-green-400" /> <button onClick={() => win.close()} className="w-3 h-3 rounded-full bg-red-400" /> </div> </div> ); } ` The data-tauri-drag-region` attribute tells Tauri which elements can drag the window — essential when you've removed the native title bar.

For styling, Tailwind is the obvious choice. It trees-shakes to near-zero at build time, which matters when your whole app might be 3 MB. Pair it with CSS variables for theming and you've got a system that handles light/dark mode without any runtime cost. If you want to push the visual further, the gradient generator or box shadow generator tools can help you dial in custom design tokens that feel genuinely native to desktop.

Accessing Native APIs and the Filesystem

This is where Tauri actually earns its keep over a PWA. You get filesystem access, system tray integration, native dialogs, shell execution, HTTP client, clipboard, notifications — all through Tauri's plugin system. Each capability is opt-in and declared explicitly in your capabilities config.

The filesystem plugin is the one you'll use constantly. Reading user config files, writing output, watching directories — all of it: ``tsx import { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; // Read from the app's config directory const config = await readTextFile('settings.json', { baseDir: BaseDirectory.AppConfig, }); // Write back await writeTextFile('settings.json', JSON.stringify(newConfig, null, 2), { baseDir: BaseDirectory.AppConfig, }); ` BaseDirectory enums handle cross-platform path resolution for you — AppConfig resolves to ~/Library/Application Support/your-app on macOS, %APPDATA%\your-app on Windows, and ~/.config/your-app` on Linux. You never hardcode paths.

System tray support is genuinely useful for tools that run in the background. Add tauri-plugin-tray to your dependencies, define a tray icon and menu in Rust, and you've got a menubar app in maybe 30 lines of code. This alone makes Tauri a better choice than any web-based alternative for utility apps.

One more thing — native file dialogs. Don't build your own file picker UI. Tauri's dialog plugin drops straight into the OS picker: ``tsx import { open } from '@tauri-apps/plugin-dialog'; const selected = await open({ multiple: false, filters: [{ name: 'JSON', extensions: ['json'] }], }); if (selected) { // selected is the full file path console.log(selected); } `` This is what users expect from a desktop app. It respects their OS theme, recent files, and sidebar bookmarks.

Building and Distributing

When you're ready to ship, npm run tauri build compiles your React app (Vite production build), compiles the Rust binary with --release optimizations, and bundles everything into platform-native installers. On macOS you get a .dmg and .app. On Windows you get an .msi and .exe NSIS installer. On Linux you get .deb, .rpm, and .AppImage. All from one command.

Cross-compilation is the hard part. Tauri 2.0 can't cross-compile to Windows from macOS or vice versa — you need to run the build on the target platform. The standard solution is GitHub Actions with matrix builds across ubuntu-latest, windows-latest, and macos-latest. Tauri's official action (tauri-apps/tauri-action) handles this well and produces signed artifacts if you supply code signing certificates as secrets.

Code signing is non-negotiable for Windows and macOS if you want to avoid scary security warnings. For macOS, you need an Apple Developer account ($99/year as of 2026) and notarization via Xcode tooling. For Windows, an EV code signing certificate from a CA. Annoying? Yes. Skippable for internal tools? Also yes — just warn your users about the Gatekeeper/SmartScreen dialog and provide instructions to bypass it.

In practice, auto-updates are what separate a hobby project from a real product. Tauri ships tauri-plugin-updater. Point it at a JSON endpoint that describes the latest version and download URLs, and the plugin handles checking, downloading, and prompting the user to install. You control the update cadence; users get a native "new version available" prompt. Combine that with a simple S3 bucket or Cloudflare R2 for hosting artifacts and you've got a full distribution pipeline for cents per month.

Pitfalls and Things Worth Knowing Before You Commit

The Rust learning curve is real, but you don't need to be a Rust expert to ship a Tauri app. For most tools, your Rust code is mostly glue — calling plugins, spawning subprocesses, reading files. The borrow checker will yell at you occasionally, but the compiler errors are genuinely the best in the industry and they usually tell you exactly how to fix the problem. Give yourself a weekend to get comfortable.

WebView inconsistencies will surprise you on Windows specifically. WebView2 (Chromium-based) is actually more consistent than you'd expect, but on older Windows 10 machines, WebView2 might not be installed. Tauri's installer can bundle the WebView2 bootstrapper to handle this, but it adds ~2 MB to your installer. Worth it.

Build times are the biggest day-to-day friction. Rust's compile times are... not fast. A fresh release build on a mid-range machine (say, 2023 M2 MacBook Pro) takes around 90-120 seconds the first time. Incremental builds are much faster — 10-15 seconds after the initial compile. The Vite side is instant as always. You get used to it, especially since tauri dev with HMR means you rarely do full rebuilds during active development.

Is Tauri worth it over just deploying a web app? If users need filesystem access, offline-first behavior, system tray presence, native file dialogs, or sub-second startup time without a loading spinner — yes, absolutely. For tools that work fine in a browser tab, the distribution overhead of a native installer probably isn't worth it. But for anything that lives on the desktop permanently and needs deep OS integration, Tauri is the most sensible answer the web ecosystem has ever produced for this problem.

The ecosystem is maturing fast. The plugin registry has grown considerably since 2.0 launched, community support on Discord is active, and the core team ships releases regularly. If you're building something new in 2026 that targets the desktop, starting with Tauri + React is a very defensible choice.

FAQ

Is Tauri production-ready in 2026?

Yes. Tauri 2.0 is stable and used in production by a growing number of teams. The plugin ecosystem is mature enough for most use cases, and the core API has stabilized significantly since 1.x.

Do I need to know Rust to build a Tauri app?

Not deeply. Most Tauri projects use Rust only for thin glue code — registering commands, calling plugins, spawning processes. You can ship a useful desktop app knowing just enough Rust to read the docs and fix compiler errors.

How does Tauri compare to Electron for bundle size?

Significantly smaller. A minimal Tauri app ships around 3-8 MB vs 80-150 MB for Electron, because Tauri uses the OS's native webview instead of bundling Chromium. Memory usage at runtime is proportionally lower too.

Can I use my existing React component library inside Tauri?

Completely. The Tauri window is just a webview running your Vite build — any React library, CSS framework, or component system works without modification. Tailwind, shadcn/ui, and Empire UI components all drop straight in.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

WebAssembly in React: Rust, wasm-pack and Real-World Use CasesThree.js with React: Particles, Blobs and Interactive 3D ScenesProgressive Web Apps with React and Next.js in 2026What Is Neumorphism? Soft UI Explained with Free React Code