$ cat /posts/tauri-20-with-svelte-modern-desktop-development.md
[tags]Tauri 2.0

Tauri 2.0 with Svelte Modern Desktop Development

drwxr-xr-x2026-01-285 min0 views
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

FeatureBenefitTauri Advantage
Compile-Time OptimizationNo runtime framework overheadSmallest bundle size (~2.5MB)
Reactive DeclarationsAutomatic reactivityClean Tauri state management
No Virtual DOMDirect DOM updatesFastest UI rendering
Two-Way BindingSimplified formsEasy input handling
Built-in TransitionsSmooth animationsNative-feeling interactions
Less BoilerplateConcise codeFaster development
TypeScript SupportType safetyType-safe Tauri commands

Creating Svelte-Tauri Project

bashcreate_svelte_tauri.sh
# 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 IPC

Understanding Generated Structure

Svelte Entry Point (main.ts)

typescriptmain.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)

svelteApp.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>
Svelte Reactivity: Notice $: 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.

typescriptsvelte_stores.ts
// 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.

sveltereactive_declarations.svelte
<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.

svelteFileEditor.svelte
<!-- 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.

svelteasync_blocks.svelte
<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

LibraryBest ForBundle SizeDesktop Ready
Carbon Components SvelteEnterprise apps, IBM designMedium (~150KB)Excellent
Svelte Material UIMaterial DesignMedium (~180KB)Very Good
SkeletonModern, customizableSmall (~80KB)Excellent
SvelteStrapBootstrap familiarityMedium (~120KB)Good
Flowbite SvelteTailwind-based componentsSmall (tree-shakable)Excellent
AttractionsMinimalist, lightweightVery Small (~50KB)Very Good

Svelte-Tauri Best Practices

  • Use Reactive Declarations: Leverage $: for automatic updates when Tauri data changes
  • Two-Way Binding: Use bind:value for 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 onMount and onDestroy for setup and cleanup
  • Scoped Styles: Keep component styles scoped preventing CSS conflicts
  • Minimal Dependencies: Leverage Svelte's built-in features before adding libraries
Ultimate Performance: Svelte + Tauri is the most performant combination for desktop apps. Svelte compiles to vanilla JavaScript with no runtime overhead, and Tauri uses native webviews—resulting in the smallest bundles and fastest startup times possible!

Next Steps

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!

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.