Tauri 3.0: Desktop Apps with Web Tech
TOP 5 Feb. 6, 2026, 5:30 p.m.

Tauri 3.0: Desktop Apps with Web Tech

Tauri 3.0 has finally arrived, and it feels like a breath of fresh air for anyone who wants to ship native‑looking desktop apps without abandoning the web stack they already love. By marrying a tiny Rust core with the familiar HTML, CSS, and JavaScript you already use for web development, Tauri lets you ship binaries that are often under 5 MB, run blazing‑fast, and stay completely sandboxed from the host OS. In this article we’ll walk through the new features of Tauri 3.0, build a small but functional example app, and explore real‑world scenarios where Tauri shines.

Why Tauri 3.0 Matters

Since its first release, Tauri has been praised for its tiny footprint, but version 3.0 takes that advantage to the next level. The runtime has been rewritten in Rust 1.71, which brings better memory safety and a smoother developer experience. The new tauri.conf.json schema is more declarative, allowing you to configure window behavior, security policies, and bundling options in a single place.

Another game‑changing addition is the plugin system. Plugins are now first‑class citizens, meaning you can drop in community‑maintained modules (like tauri-plugin-log or tauri-plugin-store) without writing any native glue code. This dramatically reduces the amount of Rust you need to write for common tasks such as persistent storage, file system access, or native notifications.

Performance Highlights

  • Binary size reduced by up to 30 % thanks to aggressive dead‑code elimination.
  • WebView initialization is now lazy, so the UI appears instantly while the Rust core boots in the background.
  • Improved IPC (inter‑process communication) latency – messages now travel over a zero‑copy channel, cutting round‑trip times to sub‑millisecond levels.

Getting Started: A Minimal “Hello, Tauri!” App

Before diving into advanced features, let’s scaffold a brand‑new Tauri project using the official CLI. The steps below work on macOS, Linux, and Windows.

# Install the Tauri CLI (requires Node.js ≥ 16)
npm install -g @tauri-apps/cli

# Create a fresh Vite + React template (you can swap React for Vue, Svelte, etc.)
npm create vite@latest my-tauri-app -- --template react
cd my-tauri-app

# Add the Tauri dependencies
npm i -D @tauri-apps/api @tauri-apps/cli

# Initialize the Tauri side (creates src-tauri)
npx tauri init

After the init step you’ll see a src-tauri folder containing Cargo.toml, the Rust manifest, and a default tauri.conf.json. Open that file and notice the new bundle section – you can now specify an icon set for each platform, a custom installer, and even a copyright field that will be baked into the final executable.

Adding a Simple Backend Command

One of Tauri’s strengths is the ability to expose Rust functions to the JavaScript front‑end via the invoke API. Let’s create a tiny command that returns the current system time.

# src-tauri/src/main.rs
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

use tauri::Manager;

#[tauri::command]
fn get_current_time() -> String {
    // Use chrono for date‑time handling (already a dependency of Tauri)
    chrono::Local::now().to_rfc3339()
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_current_time])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Now call this command from the front‑end. In a React component you could write:

import { invoke } from '@tauri-apps/api/tauri';
import { useEffect, useState } from 'react';

export default function Clock() {
  const [time, setTime] = useState('');

  useEffect(() => {
    const fetchTime = async () => {
      const result = await invoke('get_current_time');
      setTime(result);
    };
    fetchTime();
    const interval = setInterval(fetchTime, 1000);
    return () => clearInterval(interval);
  }, []);

  return <div>Current time: {time}</div>;
}

Run npm run tauri dev and you’ll see a native window displaying a live clock – all without writing a single line of platform‑specific UI code.

Pro tip: During development, keep the Tauri dev server open in a separate terminal and use npm run tauri dev -- --debug. The --debug flag disables code‑signing and enables hot‑reloading of the web assets, shaving minutes off each iteration.

Real‑World Use Cases for Tauri 3.0

Now that you’ve seen a basic example, let’s explore where Tauri truly shines in production environments.

1. Internal Tools & Dashboards

  • Data‑heavy visualizations: Because the UI runs in a WebView, you can leverage D3.js, Chart.js, or even WebGL for complex charts, while the Rust backend handles heavy CSV parsing or database queries.
  • Secure file handling: Tauri’s sandboxed file APIs let you open, edit, and save files without exposing the whole filesystem to the web layer.
  • Single‑sign‑on (SSO) integration: Use the tauri-plugin-oauth plugin to perform native OAuth flows, then hand the token to your front‑end for API calls.

2. Cross‑Platform Productivity Apps

Think note‑taking apps, to‑do lists, or markdown editors. The tiny binary size means users can download the app instantly, and the native menu bar integrates seamlessly with macOS, Windows, and Linux. Tauri’s menu API lets you define platform‑specific shortcuts in tauri.conf.json, ensuring a native feel without extra code.

3. Edge‑Computing Clients

When you need a desktop client that talks to IoT devices or runs offline AI inference, Tauri’s Rust core can embed libraries like onnxruntime or libtorch. The front‑end stays lightweight, and the heavy lifting stays safely compiled into the binary.

Advanced Feature Deep‑Dive

Tauri 3.0 introduces several new APIs that make it easier to write production‑grade apps. Below we’ll cover three of them: the tauri-plugin-store for persistent key‑value storage, the tauri::window::WindowBuilder for dynamic window creation, and the new tauri::api::notification module.

Persisting Settings with tauri-plugin-store

The store plugin abstracts away file‑system paths and serializes data as JSON. It’s perfect for saving user preferences, theme selections, or last‑opened files.

# src-tauri/src/main.rs (add the plugin)
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_store::Plugin::default())
        .invoke_handler(tauri::generate_handler![
            get_current_time,
            save_theme,
            load_theme
        ])
        .run(tauri::generate_context!())
        .expect("failed to run app");
}

// New commands
#[tauri::command]
fn save_theme(app_handle: tauri::AppHandle, theme: String) -> Result<(), String> {
    let store = app_handle.state::();
    let mut lock = store.lock().map_err(|e| e.to_string())?;
    lock.insert("theme".into(), theme.into());
    lock.save().map_err(|e| e.to_string())
}

#[tauri::command]
fn load_theme(app_handle: tauri::AppHandle) -> Result {
    let store = app_handle.state::();
    let lock = store.lock().map_err(|e| e.to_string())?;
    match lock.get("theme") {
        Some(value) => Ok(value.as_str().unwrap_or("light").to_string()),
        None => Ok("light".to_string()),
    }
}

From JavaScript you can now persist the theme whenever the user toggles a dark‑mode switch:

import { invoke } from '@tauri-apps/api/tauri';
import { useEffect, useState } from 'react';

export default function ThemeToggle() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    invoke('load_theme').then(setTheme);
  }, []);

  const toggle = async () => {
    const next = theme === 'light' ? 'dark' : 'light';
    setTheme(next);
    await invoke('save_theme', { theme: next });
  };

  return (
    <button onClick={toggle}>
      Switch to {theme === 'light' ? 'dark' : 'light'} mode
    </button>
  );
}

Pro tip: The store plugin writes to a platform‑specific directory (AppData on Windows, ~/Library/Application Support on macOS). You can override this location via the storeDir field in tauri.conf.json for portable apps.

Dynamic Windows with WindowBuilder

Sometimes you need a secondary window – for example, a settings pane or a pop‑out chat. Tauri 3.0’s WindowBuilder lets you spawn windows on demand, with custom dimensions, titles, and even transparent backgrounds.

#[tauri::command]
fn open_settings(app_handle: tauri::AppHandle) {
    let label = "settings";
    let url = "settings.html"; // relative to the web assets folder

    tauri::WindowBuilder::new(&app_handle, label, tauri::WindowUrl::App(url.into()))
        .title("App Settings")
        .inner_size(400.0, 600.0)
        .resizable(false)
        .transparent(true)
        .build()
        .unwrap();
}

Trigger the command from the front‑end with a simple button click:

import { invoke } from '@tauri-apps/api/tauri';

export function SettingsButton() {
  return (
    <button onClick={() => invoke('open_settings')}>
      Open Settings
    </button>
  );
}

The new window will appear as a native OS window, but its UI is still powered by your HTML/CSS bundle, meaning you can reuse the same component library you already use for the main view.

Native Notifications Made Easy

Desktop notifications are a must‑have for productivity tools. Tauri’s notification API abstracts the platform differences, delivering a consistent experience across macOS, Windows, and Linux.

#[tauri::command]
fn notify(title: String, body: String) {
    tauri::api::notification::Notification::new(&title)
        .body(&body)
        .show()
        .unwrap();
}

From JavaScript you can fire a reminder when a timer ends:

import { invoke } from '@tauri-apps/api/tauri';
import { useState } from 'react';

export function PomodoroTimer() {
  const [seconds, setSeconds] = useState(1500); // 25 minutes

  const tick = async () => {
    setSeconds(prev => {
      if (prev <= 1) {
        invoke('notify', {
          title: 'Time is up!',
          body: 'Take a short break.'
        });
        return 1500;
      }
      return prev - 1;
    });
  };

  // omitted setInterval boilerplate for brevity
  return <div>{Math.floor(seconds/60)}:{seconds%60}</div>;
}

Packaging, Signing, and Distribution

One of the biggest headaches for desktop developers is creating installers that work on every major OS. Tauri 3.0 ships with an improved bundler that supports .dmg (macOS), .msi (Windows), and .deb/.rpm (Linux) out of the box. The tauri.conf.json file now contains a signer block where you can provide code‑signing certificates for each platform.

{
  "tauri": {
    "bundle": {
      "identifier": "com.codeyaan.tauriapp",
      "icon": ["icons/32x32.png", "icons/128x128.png"],
      "resources": [],
      "signer": {
        "windows": {
          "certificate_path": "certs/mycert.pfx",
          "certificate_password": "superSecret"
        },
        "macOS": {
          "identity": "Developer ID Application: CodeYaan Ltd (ABCD1234)"
        }
      },
      "targets": ["dmg", "msi", "deb"]
    }
  }
}

To produce the final installers, run:

npm run tauri build

The command will compile the Rust core in release mode, bundle the web assets, sign the binaries (if you supplied a certificate), and drop the installers into src-tauri/target/release/bundle. You can then upload those files to your website, GitHub releases, or any distribution platform you prefer.

Pro tip: Enable updater in the config to let your app self‑update. Tauri’s built‑in updater checks a JSON manifest on a remote server and applies delta patches, keeping the download size to a few hundred kilobytes even for large apps.

Testing and Debugging Strategies

Even though Tauri apps are primarily JavaScript/HTML, you still need to test the native side. Here are three approaches that work well with the new 3.0 toolchain.

  1. Unit tests for Rust logic: Use cargo test as usual. Because the Tauri runtime is decoupled, you can mock the AppHandle and test your commands in isolation.
  2. End‑to‑end (E2E) with Playwright: The official Tauri template includes a Playwright config that launches the compiled binary. Write tests that click UI elements, invoke commands, and assert on the rendered DOM.
  3. Hot‑reload debugging: Run npm run tauri dev -- --features devtools to enable the Chromium DevTools panel inside the native window. You can inspect elements, view console logs, and set breakpoints just like in a regular web app.

Sample Cargo Test

#[cfg(test)]
mod tests {
    use super::*;
    use tauri::AppHandle;

    #[test]
    fn test_get_current_time_format() {
        let result = get_current_time();
        // Simple check: ISO 8601 format contains a 'T' character
        assert!(result.contains('T'), "Returned time should be RFC3339");
    }
}

Best Practices for Production‑Ready Tauri Apps

  • Min
Share this article