Tauri Frontend
Frontend integration with Tauri 2.0 including React, Vue, Svelte, and Solid frameworks, Vite configuration, asset handling, window management, multiple windows, and webview communication.
You are an expert in integrating modern frontend frameworks with Tauri 2.0, including window management, asset handling, and cross-webview communication. ## Key Points - **Using browser-specific APIs without checks**: `navigator.bluetooth`, `window.showOpenFilePicker`, and similar APIs may not exist in the webview. Use Tauri plugins instead. - **Ignoring cross-webview rendering differences**: WebKit (macOS) and WebView2 (Windows) render CSS differently. Test on all platforms. - **Large synchronous IPC calls on render**: Calling `invoke` in a render loop or useEffect without debouncing causes excessive IPC overhead. - **Not cleaning up event listeners**: Tauri event listeners (`listen()`) return unsubscribe functions. Failing to call them causes memory leaks. ## Quick Example ```bash npm create tauri-app@latest my-react-app -- --template react-ts ```
skilldb get tauri-skills/Tauri FrontendFull skill: 449 linesTauri Frontend — Webview UI Integration
You are an expert in integrating modern frontend frameworks with Tauri 2.0, including window management, asset handling, and cross-webview communication.
Core Philosophy
Tauri's frontend is a webview, not a browser. It runs your HTML/CSS/JS directly without a bundled browser engine. This means your app shares the system's webview implementation -- WebView2 on Windows, WebKit on macOS and Linux. The practical impact: your CSS and JS must work across these engines, not just Chrome.
Use Vite as your build tool. Tauri's toolchain is optimized for Vite, and the dev experience (HMR, fast rebuilds) is excellent. Pick whatever frontend framework you prefer -- React, Vue, Svelte, Solid -- Tauri is agnostic. The framework only runs in the webview; it has no special integration with the Rust backend beyond the IPC invoke system.
Keep your frontend lightweight. Unlike web apps where bundle size matters for network transfer, in Tauri the bundle is local. But webview memory and startup time still matter, especially on lower-end machines. Avoid loading massive JavaScript bundles at startup -- use code splitting and lazy loading for secondary views.
Anti-Patterns
- Using browser-specific APIs without checks:
navigator.bluetooth,window.showOpenFilePicker, and similar APIs may not exist in the webview. Use Tauri plugins instead. - Ignoring cross-webview rendering differences: WebKit (macOS) and WebView2 (Windows) render CSS differently. Test on all platforms.
- Large synchronous IPC calls on render: Calling
invokein a render loop or useEffect without debouncing causes excessive IPC overhead. - Not cleaning up event listeners: Tauri event listeners (
listen()) return unsubscribe functions. Failing to call them causes memory leaks.
Vite Configuration
Standard Vite Config for Tauri
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
// Prevent Vite from obscuring Rust errors
clearScreen: false,
// Tauri expects a fixed port, fail if it's not available
server: {
port: 5173,
strictPort: true,
watch: {
// Tell Vite to ignore watching src-tauri
ignored: ["**/src-tauri/**"],
},
},
// Env variables that start with TAURI_ are exposed to the frontend
envPrefix: ["VITE_", "TAURI_"],
build: {
// Tauri uses Chromium on Windows and WebKit on macOS/Linux
target: process.env.TAURI_PLATFORM === "windows" ? "chrome105" : "safari14",
// Don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
// Produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
},
});
React Integration
Project Setup
npm create tauri-app@latest my-react-app -- --template react-ts
Using Invoke in React Components
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
function FileViewer() {
const [content, setContent] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function loadFile(path: string) {
setLoading(true);
setError(null);
try {
const text = await invoke<string>("read_file", { path });
setContent(text);
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
}
useEffect(() => {
// Listen for backend events
const unlisten = listen<string>("file-changed", (event) => {
setContent(event.payload);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
return (
<div>
{loading && <p>Loading...</p>}
{error && <p className="error">{error}</p>}
<pre>{content}</pre>
</div>
);
}
Custom Hook for Tauri Commands
import { useState, useCallback } from "react";
import { invoke } from "@tauri-apps/api/core";
function useCommand<TArgs, TResult>(command: string) {
const [data, setData] = useState<TResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const execute = useCallback(
async (args?: TArgs) => {
setLoading(true);
setError(null);
try {
const result = await invoke<TResult>(command, args ?? {});
setData(result);
return result;
} catch (e) {
setError(String(e));
throw e;
} finally {
setLoading(false);
}
},
[command]
);
return { data, error, loading, execute };
}
// Usage
function UserList() {
const { data: users, loading, execute: fetchUsers } = useCommand<
{ page: number },
User[]
>("list_users");
useEffect(() => {
fetchUsers({ page: 1 });
}, []);
if (loading) return <Spinner />;
return <ul>{users?.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
Vue Integration
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
const items = ref<string[]>([]);
const loading = ref(false);
let unlisten: UnlistenFn;
async function fetchItems() {
loading.value = true;
try {
items.value = await invoke<string[]>("get_items");
} finally {
loading.value = false;
}
}
onMounted(async () => {
await fetchItems();
unlisten = await listen<string>("item-added", (event) => {
items.value.push(event.payload);
});
});
onUnmounted(() => {
unlisten?.();
});
</script>
<template>
<div v-if="loading">Loading...</div>
<ul v-else>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</template>
Svelte Integration
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
let count = 0;
let unlistenFn: (() => void) | undefined;
async function increment() {
count = await invoke<number>("increment");
}
onMount(async () => {
count = await invoke<number>("get_count");
unlistenFn = await listen<number>("count-changed", (event) => {
count = event.payload;
});
});
onDestroy(() => {
unlistenFn?.();
});
</script>
<button on:click={increment}>Count: {count}</button>
Window Management
Creating Windows from Rust
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
#[tauri::command]
async fn open_settings(app: AppHandle) -> Result<(), String> {
let _settings_window = WebviewWindowBuilder::new(
&app,
"settings",
WebviewUrl::App("settings.html".into()),
)
.title("Settings")
.inner_size(600.0, 400.0)
.resizable(false)
.center()
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
Creating Windows from Frontend
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
const settingsWindow = new WebviewWindow("settings", {
url: "/settings",
title: "Settings",
width: 600,
height: 400,
resizable: false,
center: true,
});
settingsWindow.once("tauri://created", () => {
console.log("Settings window created");
});
settingsWindow.once("tauri://error", (e) => {
console.error("Failed to create window:", e);
});
Window Operations
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
const appWindow = getCurrentWebviewWindow();
// Window controls
await appWindow.minimize();
await appWindow.maximize();
await appWindow.unmaximize();
await appWindow.toggleMaximize();
await appWindow.close();
// Window properties
await appWindow.setTitle("New Title");
await appWindow.setSize(new LogicalSize(800, 600));
await appWindow.setPosition(new LogicalPosition(100, 100));
await appWindow.setFullscreen(true);
await appWindow.setAlwaysOnTop(true);
// Listen for window events
await appWindow.onResized(({ payload: size }) => {
console.log(`Window resized to ${size.width}x${size.height}`);
});
await appWindow.onCloseRequested(async (event) => {
const confirmed = await confirm("Are you sure you want to close?");
if (!confirmed) {
event.preventDefault();
}
});
Multi-Window Communication
// From window A: emit to specific window
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
const targetWindow = await WebviewWindow.getByLabel("settings");
if (targetWindow) {
await targetWindow.emit("update-config", { theme: "dark" });
}
// From window B: listen for events
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
const currentWindow = getCurrentWebviewWindow();
await currentWindow.listen<{ theme: string }>("update-config", (event) => {
applyTheme(event.payload.theme);
});
// Broadcast to all windows (from Rust)
// app.emit("global-event", payload)?;
Asset Handling
Static Assets in Frontend
src/
├── assets/
│ ├── images/
│ │ └── logo.png
│ └── fonts/
│ └── inter.woff2
// Vite handles static imports
import logo from "./assets/images/logo.png";
function App() {
return <img src={logo} alt="Logo" />;
}
Bundled Resources in Rust Side
// tauri.conf.json
{
"bundle": {
"resources": ["resources/*"]
}
}
use tauri::Manager;
#[tauri::command]
async fn load_resource(app: AppHandle) -> Result<String, String> {
let resource_path = app
.path()
.resolve("resources/data.json", tauri::path::BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
std::fs::read_to_string(resource_path).map_err(|e| e.to_string())
}
Custom Titlebar (Frameless Window)
// tauri.conf.json
{
"app": {
"windows": [
{
"decorations": false,
"width": 1024,
"height": 768
}
]
}
}
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
function Titlebar() {
const appWindow = getCurrentWebviewWindow();
return (
<div data-tauri-drag-region className="titlebar">
<span>My App</span>
<div className="titlebar-buttons">
<button onClick={() => appWindow.minimize()}>–</button>
<button onClick={() => appWindow.toggleMaximize()}>□</button>
<button onClick={() => appWindow.close()}>✕</button>
</div>
</div>
);
}
.titlebar {
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
background: #1a1a2e;
color: white;
user-select: none;
}
.titlebar-buttons button {
background: transparent;
border: none;
color: white;
width: 36px;
height: 36px;
cursor: pointer;
}
.titlebar-buttons button:hover {
background: rgba(255, 255, 255, 0.1);
}
Install this skill directly: skilldb add tauri-skills
Related Skills
Tauri Commands
Rust commands with the invoke pattern, argument passing, return types, async commands, error handling, state management, and type safety between Rust and TypeScript in Tauri 2.0.
Tauri Distribution
Distributing Tauri applications including installers for MSI, DMG, AppImage, and deb, auto-update with the built-in updater, code signing for Windows and macOS, CI/CD builds, and cross-compilation.
Tauri Fundamentals
Tauri 2.0 architecture, Rust backend with webview frontend, project setup with Cargo and npm, development workflow, and build targets for Windows, macOS, Linux, iOS, and Android.
Tauri Mobile
Tauri 2.0 mobile development for iOS and Android, including platform-specific code, mobile plugins, testing on simulators and devices, and app store distribution.
Tauri Patterns
Common Tauri 2.0 patterns: system tray apps, menu bar apps, file handling, SQLite database integration, IPC communication patterns, background tasks, and single-instance enforcement.
Tauri Plugins
Tauri 2.0 plugin system including official plugins for filesystem, shell, dialog, notification, HTTP, clipboard, updater, and deep-link, plus community plugins and writing custom plugins.