Tauri 2.0 with Svelte Modern Desktop Development

Building desktop applications with Tauri 2.0 and Svelte creates exceptionally lightweight, performant apps combining Svelte's compile-time optimization philosophy with Tauri's minimal runtime overhead resulting in the smallest possible bundle sizes (often under 3MB) and instant startup times making it ideal for performance-critical desktop tools. Svelte's unique approach compiling components to efficient vanilla JavaScript at build time eliminates virtual DOM overhead providing native-like performance, while its intuitive reactive declarations and two-way binding simplify state management creating clean, maintainable code with less boilerplate than traditional frameworks. This comprehensive guide covers creating Svelte + Tauri projects with optimal Vite configuration, understanding Svelte's reactivity system perfectly suited for desktop development with automatic updates triggered by Rust backend responses, implementing IPC communication using Svelte stores wrapping Tauri commands for reactive state management, organizing code with Svelte's component structure maintaining separation of concerns, integrating Svelte UI libraries while preserving security, handling asynchronous operations with Svelte's promise blocks and reactive statements, and building real-world features demonstrating file operations and system integration. By completing this tutorial, you'll master Svelte-Tauri patterns including writable stores managing global application state, derived stores computing values from Tauri responses, reactive declarations automatically updating on data changes, and performance optimization leveraging Svelte's compile-time advantages. Before starting, ensure you understand Tauri fundamentals and project organization.
Why Choose Svelte for Tauri
| Feature | Benefit | Tauri Advantage |
|---|---|---|
| Compile-Time Optimization | No runtime framework overhead | Smallest bundle size (~2.5MB) |
| Reactive Declarations | Automatic reactivity | Clean Tauri state management |
| No Virtual DOM | Direct DOM updates | Fastest UI rendering |
| Two-Way Binding | Simplified forms | Easy input handling |
| Built-in Transitions | Smooth animations | Native-feeling interactions |
| Less Boilerplate | Concise code | Faster development |
| TypeScript Support | Type safety | Type-safe Tauri commands |
Creating Svelte-Tauri Project
# Create new Tauri + Svelte project with TypeScript
npm create tauri-app@latest my-svelte-tauri-app
# Interactive prompts:
# 1. Choose package manager: npm (or yarn, pnpm)
# 2. Choose UI template: Svelte
# 3. Choose variant: TypeScript (recommended)
# Navigate to project
cd my-svelte-tauri-app
# Install dependencies
npm install
# Start development
npm run tauri dev
# Project structure:
# my-svelte-tauri-app/
# ├── src/ # Svelte application
# │ ├── App.svelte # Root component
# │ ├── main.ts # Entry point
# │ ├── lib/ # Components and utilities
# │ │ ├── components/ # Svelte components
# │ │ └── stores/ # Svelte stores
# │ └── assets/ # Static files
# ├── src-tauri/ # Rust backend
# │ ├── src/main.rs # Tauri commands
# │ └── tauri.conf.json # Configuration
# ├── package.json # Dependencies
# ├── vite.config.ts # Vite config
# ├── svelte.config.js # Svelte config
# └── tsconfig.json # TypeScript config
# What you get:
# - Svelte with SvelteKit-style structure
# - TypeScript support
# - Vite for fast HMR
# - Tauri 2.0 APIs ready
# - Example with IPCUnderstanding Generated Structure
Svelte Entry Point (main.ts)
// src/main.ts - Svelte application entry point
import App from "./App.svelte";
import "./app.css";
// Create Svelte app instance
const app = new App({
target: document.getElementById("app")!,
});
export default app;Root Component (App.svelte)
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
// Reactive state (automatically tracked by Svelte)
let name = "";
let greetMsg = "";
let loading = false;
let error = "";
// Async function calling Rust backend
async function greet() {
loading = true;
error = "";
try {
// Invoke Tauri command
greetMsg = await invoke<string>("greet", { name });
} catch (err) {
error = `Error: ${err}`;
} finally {
loading = false;
}
}
// Reactive declaration - runs automatically when dependencies change
$: canSubmit = name.trim().length > 0 && !loading;
</script>
<main>
<h1>Welcome to Tauri + Svelte!</h1>
<form on:submit|preventDefault={greet}>
<input
bind:value={name}
type="text"
placeholder="Enter a name..."
disabled={loading}
/>
<button type="submit" disabled={!canSubmit}>
{loading ? "Greeting..." : "Greet"}
</button>
</form>
{#if greetMsg}
<p class="success">{greetMsg}</p>
{/if}
{#if error}
<p class="error">{error}</p>
{/if}
</main>
<style>
main {
padding: 20px;
text-align: center;
}
form {
display: flex;
gap: 10px;
justify-content: center;
margin: 20px 0;
}
input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}
button {
padding: 8px 16px;
background: #ff3e00;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.success {
color: #40b87b;
margin-top: 10px;
}
.error {
color: #ff3e00;
margin-top: 10px;
}
</style>$: canSubmit = ...—this is Svelte's reactive declaration. It automatically re-runs whenever name or loading changes. No hooks, no manual subscriptions—just pure, efficient reactivity!Svelte Stores for Tauri State
Svelte stores provide reactive global state management perfect for sharing Tauri data across components. Create writable stores wrapping Tauri commands for centralized state. Learn more about IPC patterns and command creation.
// src/lib/stores/tauriStore.ts
import { writable, derived, get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
interface CommandState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
/**
* Create a Svelte store that wraps a Tauri command
* @param commandName - Name of the Rust command
* @returns Writable store with execute function
*/
export function createTauriStore<T = unknown, P = Record<string, unknown>>(
commandName: string
) {
const { subscribe, set, update } = writable<CommandState<T>>({
data: null,
loading: false,
error: null,
});
async function execute(params?: P): Promise<T | void> {
update((state) => ({ ...state, loading: true, error: null }));
try {
const result = await invoke<T>(commandName, params);
set({ data: result, loading: false, error: null });
return result;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
set({ data: null, loading: false, error: errorMessage });
throw err;
}
}
return {
subscribe,
execute,
reset: () => set({ data: null, loading: false, error: null }),
};
}
// src/lib/stores/systemStore.ts
import { createTauriStore } from "./tauriStore";
interface SystemInfo {
os: string;
arch: string;
version: string;
}
// Create store for system information
export const systemInfoStore = createTauriStore<SystemInfo>("get_system_info");
// Derived store for just the OS name
export const osName = derived(
systemInfoStore,
($systemInfo) => $systemInfo.data?.os || "Unknown"
);
// src/lib/stores/appStore.ts - Application state
import { writable } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
interface User {
id: string;
username: string;
email: string;
}
interface Settings {
theme: "light" | "dark";
fontSize: number;
}
function createAppStore() {
const { subscribe, set, update } = writable({
user: null as User | null,
settings: { theme: "light" as const, fontSize: 14 },
loading: false,
});
return {
subscribe,
async login(username: string, password: string) {
update((state) => ({ ...state, loading: true }));
try {
const user = await invoke<User>("login", { username, password });
update((state) => ({ ...state, user, loading: false }));
} catch (err) {
update((state) => ({ ...state, loading: false }));
throw err;
}
},
async logout() {
await invoke("logout");
update((state) => ({ ...state, user: null }));
},
updateSettings(settings: Partial<Settings>) {
update((state) => ({
...state,
settings: { ...state.settings, ...settings },
}));
invoke("save_settings", { settings: get({ subscribe }).settings });
},
};
}
export const appStore = createAppStore();
// Usage in component:
<script lang="ts">
import { systemInfoStore, osName } from "./stores/systemStore";
import { appStore } from "./stores/appStore";
import { onMount } from "svelte";
// Load system info on mount
onMount(() => {
systemInfoStore.execute();
});
// Auto-subscribe to stores using $ prefix
// $systemInfoStore gives us the current value
// $osName gives us the derived OS name
</script>
<div>
{#if $systemInfoStore.loading}
<p>Loading system info...</p>
{:else if $systemInfoStore.error}
<p class="error">{$systemInfoStore.error}</p>
{:else if $systemInfoStore.data}
<p>OS: {$systemInfoStore.data.os}</p>
<p>Architecture: {$systemInfoStore.data.arch}</p>
<p>Derived OS: {$osName}</p>
{/if}
{#if $appStore.user}
<p>Welcome, {$appStore.user.username}!</p>
<button on:click={() => appStore.logout()}>Logout</button>
{/if}
</div>Reactive Declarations and Statements
Svelte's reactive declarations (prefixed with $:) automatically re-run when dependencies change providing elegant state management without hooks or manual subscriptions perfect for desktop apps responding to Tauri events.
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
let count = 0;
let multiplier = 2;
// Reactive declaration - recalculates when count or multiplier changes
$: result = count * multiplier;
// Reactive statement - runs side effects when count changes
$: if (count > 10) {
console.log("Count exceeded 10!");
invoke("log_event", { event: "count_exceeded", value: count });
}
// Multiple dependencies
$: displayMessage = `${count} × ${multiplier} = ${result}`;
// Async reactive statement
let data = null;
let searchQuery = "";
$: if (searchQuery) {
// This runs whenever searchQuery changes
searchData(searchQuery);
}
async function searchData(query: string) {
try {
data = await invoke("search", { query });
} catch (err) {
console.error("Search failed:", err);
}
}
// Reactive statement for computed properties
let items = [1, 2, 3, 4, 5];
$: total = items.reduce((sum, item) => sum + item, 0);
$: average = items.length > 0 ? total / items.length : 0;
function increment() {
count += 1;
}
</script>
<div>
<p>{displayMessage}</p>
<button on:click={increment}>Increment</button>
<input bind:value={multiplier} type="number" />
<p>Total: {total}</p>
<p>Average: {average}</p>
<input bind:value={searchQuery} placeholder="Search..." />
{#if data}
<pre>{JSON.stringify(data, null, 2)}</pre>
{/if}
</div>
<style>
div {
padding: 20px;
}
</style>File Operations with Svelte
Implement file management using Svelte's reactive system automatically updating UI when file content changes maintaining clean, readable code. Learn more about file system APIs.
<!-- src/lib/components/FileEditor.svelte -->
<script lang="ts">
import { open, save } from "@tauri-apps/plugin-dialog";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
let currentFile: string | null = null;
let content = "";
let status = "";
let loading = false;
let modified = false;
// Track if content has been modified
$: if (content !== originalContent) {
modified = true;
}
let originalContent = "";
async function openFile() {
loading = true;
try {
const selected = await open({
multiple: false,
filters: [{
name: "Text Files",
extensions: ["txt", "md", "json", "ts", "svelte"]
}]
});
if (selected && typeof selected === "string") {
const fileContent = await readTextFile(selected);
content = fileContent;
originalContent = fileContent;
currentFile = selected;
status = `Opened: ${selected}`;
modified = false;
}
} catch (err) {
status = `Error: ${err}`;
} finally {
loading = false;
}
}
async function saveFile() {
loading = true;
try {
let filePath = currentFile;
if (!filePath) {
const selected = await save({
filters: [{
name: "Text Files",
extensions: ["txt"]
}]
});
if (!selected) {
loading = false;
return;
}
filePath = selected;
}
await writeTextFile(filePath, content);
currentFile = filePath;
originalContent = content;
modified = false;
status = `Saved: ${filePath}`;
} catch (err) {
status = `Error: ${err}`;
} finally {
loading = false;
}
}
async function saveAsFile() {
loading = true;
try {
const selected = await save({
defaultPath: currentFile || undefined,
filters: [{
name: "Text Files",
extensions: ["txt"]
}]
});
if (selected) {
await writeTextFile(selected, content);
currentFile = selected;
originalContent = content;
modified = false;
status = `Saved as: ${selected}`;
}
} catch (err) {
status = `Error: ${err}`;
} finally {
loading = false;
}
}
function newFile() {
if (modified) {
if (!confirm("Discard unsaved changes?")) return;
}
content = "";
originalContent = "";
currentFile = null;
modified = false;
status = "New file";
}
// Keyboard shortcuts
function handleKeyDown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
saveFile();
} else if ((e.ctrlKey || e.metaKey) && e.key === "o") {
e.preventDefault();
openFile();
} else if ((e.ctrlKey || e.metaKey) && e.key === "n") {
e.preventDefault();
newFile();
}
}
</script>
<svelte:window on:keydown={handleKeyDown} />
<div class="file-editor">
<div class="toolbar">
<button on:click={newFile} disabled={loading}>
New (Ctrl+N)
</button>
<button on:click={openFile} disabled={loading}>
Open (Ctrl+O)
</button>
<button on:click={saveFile} disabled={loading || !content}>
Save (Ctrl+S)
</button>
<button on:click={saveAsFile} disabled={loading || !content}>
Save As...
</button>
{#if modified}
<span class="modified-indicator">●</span>
{/if}
</div>
{#if status}
<div class="status">{status}</div>
{/if}
<textarea
bind:value={content}
disabled={loading}
placeholder="Start typing or open a file..."
rows="25"
class="editor"
/>
{#if currentFile}
<div class="file-info">
Current file: {currentFile}
{#if modified}
<span class="modified">(modified)</span>
{/if}
</div>
{/if}
<div class="stats">
Characters: {content.length} |
Lines: {content.split('\n').length} |
Words: {content.split(/\s+/).filter(w => w.length > 0).length}
</div>
</div>
<style>
.file-editor {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20px;
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
button {
padding: 8px 16px;
background: #ff3e00;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.modified-indicator {
color: #ff3e00;
font-size: 24px;
line-height: 1;
}
.status {
padding: 8px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 10px;
}
.editor {
flex: 1;
width: 100%;
font-family: "Fira Code", monospace;
font-size: 14px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
}
.file-info {
margin-top: 10px;
font-size: 0.9em;
color: #666;
}
.modified {
color: #ff3e00;
font-weight: bold;
}
.stats {
margin-top: 5px;
font-size: 0.85em;
color: #888;
}
</style>Async Await Blocks
Svelte's {#await} blocks provide elegant syntax for handling promises directly in templates eliminating need for separate loading state variables perfect for Tauri async operations.
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
// Function that returns a promise
async function fetchData() {
return await invoke<string[]>("get_data");
}
// Create promise on component mount
let dataPromise = fetchData();
function refresh() {
dataPromise = fetchData(); // Reassigning triggers reactivity
}
</script>
<!-- Await block handles loading, success, and error states -->
{#await dataPromise}
<p>Loading data...</p>
{:then data}
<ul>
{#each data as item}
<li>{item}</li>
{/each}
</ul>
<button on:click={refresh}>Refresh</button>
{:catch error}
<p class="error">Error: {error}</p>
<button on:click={refresh}>Retry</button>
{/await}
<!-- Alternative: Show content immediately while loading in background -->
{#await dataPromise then data}
<ul>
{#each data as item}
<li>{item}</li>
{/each}
</ul>
{/await}
<style>
.error {
color: #ff3e00;
}
</style>Svelte UI Component Libraries
| Library | Best For | Bundle Size | Desktop Ready |
|---|---|---|---|
| Carbon Components Svelte | Enterprise apps, IBM design | Medium (~150KB) | Excellent |
| Svelte Material UI | Material Design | Medium (~180KB) | Very Good |
| Skeleton | Modern, customizable | Small (~80KB) | Excellent |
| SvelteStrap | Bootstrap familiarity | Medium (~120KB) | Good |
| Flowbite Svelte | Tailwind-based components | Small (tree-shakable) | Excellent |
| Attractions | Minimalist, lightweight | Very Small (~50KB) | Very Good |
Svelte-Tauri Best Practices
- Use Reactive Declarations: Leverage
$:for automatic updates when Tauri data changes - Two-Way Binding: Use
bind:valuefor forms simplifying input handling - Stores for Global State: Wrap Tauri commands in Svelte stores for reactive global state
- Await Blocks: Use
{#await}for elegant promise handling without manual state - Component Composition: Break UI into small, reusable components maintaining clarity
- TypeScript Always: Enable TypeScript for type-safe Tauri commands and better DX
- Auto-Subscribe: Use
$prefix for automatic store subscriptions in templates - Lifecycle Hooks: Use
onMountandonDestroyfor setup and cleanup - Scoped Styles: Keep component styles scoped preventing CSS conflicts
- Minimal Dependencies: Leverage Svelte's built-in features before adding libraries
Next Steps
- Compare Frameworks: Review React and Vue alternatives
- Master IPC: Deep dive into IPC communication
- Build Commands: Learn creating Rust commands
- Global State: Explore state management patterns
- File System: Build file operations features
- Security: Implement security best practices
Conclusion
Building desktop applications with Tauri 2.0 and Svelte creates the most lightweight, performant combination possible for cross-platform development combining Svelte's compile-time optimization eliminating runtime overhead with Tauri's minimal architecture resulting in exceptionally small bundle sizes (often under 3MB) and instant startup times perfect for performance-critical desktop tools. Svelte's unique approach including reactive declarations automatically updating on changes, two-way binding simplifying forms, await blocks elegantly handling promises, and component-scoped styling maintaining clean CSS creates intuitive development experience with less boilerplate than traditional frameworks enabling rapid desktop application creation. Best practices include using reactive declarations for automatic UI updates responding to Tauri commands, wrapping Tauri APIs in Svelte stores for reactive global state management, leveraging await blocks for elegant promise handling without manual loading states, implementing two-way binding for simplified form handling, and composing applications from small, focused components maintaining code clarity throughout development lifecycle. The Svelte-Tauri combination excels at building desktop applications requiring maximum performance, minimal bundle size, intuitive reactive patterns, and clean, maintainable code enabling developers to focus on features rather than framework boilerplate. Your Svelte knowledge translates perfectly to desktop development creating professional applications with modern patterns and exceptional performance!
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


