$ cat /posts/tauri-20-with-vue-3-building-desktop-apps.md
[tags]Tauri 2.0

Tauri 2.0 with Vue 3 Building Desktop Apps

drwxr-xr-x2026-01-285 min0 views
Tauri 2.0 with Vue 3 Building Desktop Apps

Building desktop applications with Tauri 2.0 and Vue 3 combines Vue's progressive framework philosophy, intuitive reactivity system, and excellent developer experience with Tauri's lightweight architecture creating performant cross-platform apps with small bundle sizes (3-5MB) and native system access while maintaining the flexibility to incrementally adopt advanced features. Vue 3's Composition API provides powerful abstractions for organizing Tauri API logic into reusable composables enabling clean separation of concerns, TypeScript support ensures type safety across frontend-backend boundaries, and Single File Components (SFCs) keep template, logic, and styling colocated improving maintainability. This comprehensive guide covers creating Vue 3 + Tauri projects from scratch with Vite configuration, understanding Vue 3 Composition API patterns perfect for desktop development, implementing IPC communication with type-safe composables wrapping Tauri commands, managing reactive state with Vue's reactivity system and Pinia for complex scenarios, integrating Vue Router for multi-view desktop applications, using popular Vue UI libraries while maintaining security, and building real-world features like file management and system dialogs. By completing this tutorial, you'll master Vue-Tauri integration patterns including composables encapsulating Tauri functionality, reactive refs and computed properties managing async operations, provide/inject for dependency injection across components, and performance optimization techniques leveraging Vue's reactivity. Before starting, ensure you understand Tauri basics and project structure.

Why Choose Vue 3 for Tauri

FeatureBenefitTauri Advantage
Composition APIOrganize logic by featureClean Tauri composables
Reactivity SystemAutomatic UI updatesSync with Rust backend easily
Single File ComponentsColocated codeMaintainable desktop features
Progressive FrameworkUse what you needSmall initial bundle
TypeScript SupportType safetyType-safe Tauri commands
Vite IntegrationFast HMRInstant development feedback
Easy Learning CurveGentle adoptionQuick desktop app prototyping

Creating Vue 3-Tauri Project

bashcreate_vue_tauri.sh
# Create new Tauri + Vue 3 project with TypeScript
npm create tauri-app@latest my-vue-tauri-app

# Interactive prompts:
# 1. Choose package manager: npm (or yarn, pnpm)
# 2. Choose UI template: Vue
# 3. Choose variant: TypeScript (recommended)

# Navigate to project
cd my-vue-tauri-app

# Install dependencies
npm install

# Start development
npm run tauri dev

# Project structure:
# my-vue-tauri-app/
# โ”œโ”€โ”€ src/                    # Vue 3 application
# โ”‚   โ”œโ”€โ”€ App.vue            # Root component
# โ”‚   โ”œโ”€โ”€ main.ts            # Vue entry point
# โ”‚   โ”œโ”€โ”€ components/        # Vue components
# โ”‚   โ”œโ”€โ”€ composables/       # Reusable logic
# โ”‚   โ””โ”€โ”€ assets/            # Static files
# โ”œโ”€โ”€ src-tauri/             # Rust backend
# โ”‚   โ”œโ”€โ”€ src/main.rs        # Tauri commands
# โ”‚   โ””โ”€โ”€ tauri.conf.json    # Configuration
# โ”œโ”€โ”€ package.json           # Dependencies
# โ”œโ”€โ”€ vite.config.ts         # Vite config
# โ””โ”€โ”€ tsconfig.json          # TypeScript config

# What you get:
# - Vue 3 with Composition API
# - TypeScript support
# - Vite for fast development
# - Tauri 2.0 APIs pre-configured
# - Example component with IPC

Understanding Generated Structure

Vue Entry Point (main.ts)

typescriptmain.ts
// src/main.ts - Vue application entry point
import { createApp } from "vue";
import App from "./App.vue";
import "./styles.css";

// Create and mount Vue application
const app = createApp(App);

// Mount to DOM element with id="app"
app.mount("#app");

// Optional: Add global properties or plugins
// app.config.globalProperties.$appVersion = "1.0.0";
// app.use(router);
// app.use(pinia);

Root Component (App.vue)

vueApp.vue
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";

// Reactive state
const greetMsg = ref<string>("");
const name = ref<string>("");
const loading = ref<boolean>(false);
const error = ref<string>("");

// Function to call Rust backend
async function greet() {
  loading.value = true;
  error.value = "";
  
  try {
    // Invoke Rust command
    greetMsg.value = await invoke<string>("greet", { name: name.value });
  } catch (err) {
    error.value = `Error: ${err}`;
  } finally {
    loading.value = false;
  }
}
</script>

<template>
  <div class="container">
    <h1>Welcome to Tauri + Vue 3!</h1>

    <form @submit.prevent="greet" class="row">
      <input
        v-model="name"
        type="text"
        placeholder="Enter a name..."
        :disabled="loading"
      />
      <button
        type="submit"
        :disabled="loading || !name.trim()"
      >
        {{ loading ? "Greeting..." : "Greet" }}
      </button>
    </form>

    <p v-if="greetMsg" class="success">
      {{ greetMsg }}
    </p>

    <p v-if="error" class="error">
      {{ error }}
    </p>
  </div>
</template>

<style scoped>
.container {
  padding: 20px;
  text-align: center;
}

.row {
  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: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.success {
  color: #42b883;
}

.error {
  color: #f56c6c;
}
</style>

Creating Composables for Tauri

Composables are Vue 3's powerful pattern for extracting and reusing stateful logic. Create composables wrapping Tauri APIs for clean, testable, and reusable code. Learn more about IPC communication and Rust commands.

typescriptcomposables.ts
// src/composables/useTauriCommand.ts
import { ref, Ref } from "vue";
import { invoke } from "@tauri-apps/api/core";

interface CommandState<T> {
  data: Ref<T | null>;
  loading: Ref<boolean>;
  error: Ref<string | null>;
  execute: (params?: Record<string, unknown>) => Promise<T | void>;
}

/**
 * Composable for invoking Tauri commands with reactive state
 * @param commandName - Name of the Rust command
 * @returns Reactive state and execute function
 */
export function useTauriCommand<T = unknown>(
  commandName: string
): CommandState<T> {
  const data = ref<T | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function execute(params?: Record<string, unknown>): Promise<T | void> {
    loading.value = true;
    error.value = null;

    try {
      const result = await invoke<T>(commandName, params);
      data.value = result;
      return result;
    } catch (err) {
      error.value = err instanceof Error ? err.message : String(err);
      throw err;
    } finally {
      loading.value = false;
    }
  }

  return { data, loading, error, execute };
}

// src/composables/useSystemInfo.ts
import { onMounted } from "vue";
import { useTauriCommand } from "./useTauriCommand";

interface SystemInfo {
  os: string;
  arch: string;
  version: string;
  hostname: string;
}

/**
 * Composable to fetch system information
 */
export function useSystemInfo() {
  const { data: systemInfo, loading, error, execute } = 
    useTauriCommand<SystemInfo>("get_system_info");

  // Fetch on mount
  onMounted(() => {
    execute();
  });

  return { systemInfo, loading, error, refresh: execute };
}

// Usage in component:
<script setup lang="ts">
import { useSystemInfo } from "@/composables/useSystemInfo";

const { systemInfo, loading, error, refresh } = useSystemInfo();
</script>

<template>
  <div>
    <button @click="refresh">Refresh</button>
    
    <div v-if="loading">Loading...</div>
    
    <div v-else-if="error" class="error">
      Error: {{ error }}
    </div>
    
    <div v-else-if="systemInfo">
      <p>OS: {{ systemInfo.os }}</p>
      <p>Architecture: {{ systemInfo.arch }}</p>
      <p>Version: {{ systemInfo.version }}</p>
      <p>Hostname: {{ systemInfo.hostname }}</p>
    </div>
  </div>
</template>

File Operations with Vue 3

Implement file operations using composables that wrap Tauri's file system APIs maintaining reactivity and providing clean component interfaces. Learn more about file system API.

typescriptuseFileSystem.ts
// src/composables/useFileSystem.ts
import { ref } from "vue";
import { open, save } from "@tauri-apps/plugin-dialog";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";

export function useFileSystem() {
  const currentFile = ref<string | null>(null);
  const content = ref<string>("");
  const status = ref<string>("");
  const loading = ref(false);

  // Open file picker and read content
  async function openFile() {
    loading.value = true;
    try {
      const selected = await open({
        multiple: false,
        filters: [{
          name: "Text Files",
          extensions: ["txt", "md", "json", "ts", "vue"]
        }]
      });

      if (selected && typeof selected === "string") {
        const fileContent = await readTextFile(selected);
        content.value = fileContent;
        currentFile.value = selected;
        status.value = `Opened: ${selected}`;
      }
    } catch (err) {
      status.value = `Error opening file: ${err}`;
    } finally {
      loading.value = false;
    }
  }

  // Save content to current or new file
  async function saveFile() {
    loading.value = true;
    try {
      let filePath = currentFile.value;

      if (!filePath) {
        const selected = await save({
          filters: [{
            name: "Text Files",
            extensions: ["txt"]
          }]
        });

        if (!selected) {
          loading.value = false;
          return;
        }
        filePath = selected;
      }

      await writeTextFile(filePath, content.value);
      currentFile.value = filePath;
      status.value = `Saved: ${filePath}`;
    } catch (err) {
      status.value = `Error saving file: ${err}`;
    } finally {
      loading.value = false;
    }
  }

  // Save as new file
  async function saveAsFile() {
    loading.value = true;
    try {
      const selected = await save({
        defaultPath: currentFile.value || undefined,
        filters: [{
          name: "Text Files",
          extensions: ["txt"]
        }]
      });

      if (selected) {
        await writeTextFile(selected, content.value);
        currentFile.value = selected;
        status.value = `Saved as: ${selected}`;
      }
    } catch (err) {
      status.value = `Error saving file: ${err}`;
    } finally {
      loading.value = false;
    }
  }

  // Create new file
  function newFile() {
    content.value = "";
    currentFile.value = null;
    status.value = "New file";
  }

  return {
    currentFile,
    content,
    status,
    loading,
    openFile,
    saveFile,
    saveAsFile,
    newFile
  };
}

// Component using the composable:
<script setup lang="ts">
import { useFileSystem } from "@/composables/useFileSystem";

const {
  currentFile,
  content,
  status,
  loading,
  openFile,
  saveFile,
  saveAsFile,
  newFile
} = useFileSystem();
</script>

<template>
  <div class="file-editor">
    <div class="toolbar">
      <button @click="newFile" :disabled="loading">New</button>
      <button @click="openFile" :disabled="loading">Open</button>
      <button @click="saveFile" :disabled="loading || !content">Save</button>
      <button @click="saveAsFile" :disabled="loading || !content">Save As</button>
    </div>

    <div v-if="status" class="status">
      {{ status }}
    </div>

    <textarea
      v-model="content"
      :disabled="loading"
      placeholder="Start typing or open a file..."
      rows="20"
      class="editor"
    />

    <div v-if="currentFile" class="file-info">
      Current file: {{ currentFile }}
    </div>
  </div>
</template>

<style scoped>
.file-editor {
  padding: 20px;
}

.toolbar {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.editor {
  width: 100%;
  font-family: monospace;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.status {
  margin: 10px 0;
  padding: 8px;
  background: #f0f0f0;
  border-radius: 4px;
}

.file-info {
  margin-top: 10px;
  font-size: 0.9em;
  color: #666;
}
</style>

State Management with Pinia

For complex applications, Pinia provides intuitive state management with TypeScript support and Vue 3 integration enabling centralized state across components and windows. Learn more about state management patterns.

typescriptpinia_store.ts
// Install Pinia
// npm install pinia

// src/main.ts - Setup Pinia
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount("#app");

// src/stores/appStore.ts - Pinia store
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { invoke } from "@tauri-apps/api/core";

interface User {
  id: string;
  username: string;
  email: string;
}

interface Settings {
  theme: "light" | "dark";
  fontSize: number;
  autoSave: boolean;
}

export const useAppStore = defineStore("app", () => {
  // State
  const user = ref<User | null>(null);
  const settings = ref<Settings>({
    theme: "light",
    fontSize: 14,
    autoSave: true
  });
  const loading = ref(false);
  const error = ref<string | null>(null);

  // Getters
  const isLoggedIn = computed(() => user.value !== null);
  const isDarkMode = computed(() => settings.value.theme === "dark");

  // Actions
  async function login(username: string, password: string) {
    loading.value = true;
    error.value = null;

    try {
      const userData = await invoke<User>("login", { username, password });
      user.value = userData;
      
      // Load user settings
      await loadSettings();
    } catch (err) {
      error.value = err instanceof Error ? err.message : String(err);
      throw err;
    } finally {
      loading.value = false;
    }
  }

  async function logout() {
    try {
      await invoke("logout");
      user.value = null;
    } catch (err) {
      console.error("Logout error:", err);
    }
  }

  async function loadSettings() {
    try {
      const loadedSettings = await invoke<Settings>("get_settings");
      settings.value = loadedSettings;
    } catch (err) {
      console.error("Failed to load settings:", err);
    }
  }

  async function updateSettings(newSettings: Partial<Settings>) {
    const updated = { ...settings.value, ...newSettings };
    settings.value = updated;

    try {
      await invoke("save_settings", { settings: updated });
    } catch (err) {
      console.error("Failed to save settings:", err);
    }
  }

  function toggleTheme() {
    updateSettings({
      theme: settings.value.theme === "light" ? "dark" : "light"
    });
  }

  return {
    // State
    user,
    settings,
    loading,
    error,
    // Getters
    isLoggedIn,
    isDarkMode,
    // Actions
    login,
    logout,
    loadSettings,
    updateSettings,
    toggleTheme
  };
});

// Usage in component:
<script setup lang="ts">
import { useAppStore } from "@/stores/appStore";

const appStore = useAppStore();

async function handleLogin() {
  try {
    await appStore.login("username", "password");
  } catch (err) {
    // Handle error
  }
}
</script>

<template>
  <div>
    <div v-if="appStore.isLoggedIn">
      <p>Welcome, {{ appStore.user?.username }}!</p>
      <button @click="appStore.logout">Logout</button>
      <button @click="appStore.toggleTheme">
        Switch to {{ appStore.isDarkMode ? "Light" : "Dark" }} Mode
      </button>
    </div>
    <div v-else>
      <button @click="handleLogin">Login</button>
    </div>
  </div>
</template>

Vue Router for Multi-View Apps

typescriptvue_router.ts
// Install Vue Router
// npm install vue-router@4

// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import Settings from "@/views/Settings.vue";
import Editor from "@/views/Editor.vue";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/editor",
      name: "editor",
      component: Editor
    },
    {
      path: "/settings",
      name: "settings",
      component: Settings
    }
  ]
});

export default router;

// src/main.ts - Register router
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";

const app = createApp(App);

app.use(createPinia());
app.use(router);
app.mount("#app");

// src/App.vue - Add router navigation
<script setup lang="ts">
import { RouterView, RouterLink } from "vue-router";
</script>

<template>
  <div id="app">
    <nav>
      <RouterLink to="/">Home</RouterLink>
      <RouterLink to="/editor">Editor</RouterLink>
      <RouterLink to="/settings">Settings</RouterLink>
    </nav>
    
    <main>
      <RouterView />
    </main>
  </div>
</template>

<style scoped>
nav {
  display: flex;
  gap: 20px;
  padding: 20px;
  background: #f5f5f5;
}

nav a {
  text-decoration: none;
  color: #42b883;
  font-weight: 500;
}

nav a.router-link-active {
  color: #35495e;
  font-weight: bold;
}

main {
  padding: 20px;
}
</style>

Vue UI Component Libraries

LibraryBest ForBundle SizeDesktop Ready
Element PlusEnterprise apps, rich componentsMedium (~200KB)Excellent
VuetifyMaterial Design, complete systemLarge (~400KB)Very Good
Naive UIModern design, TypeScriptMedium (~150KB)Excellent
Ant Design VueEnterprise, data-heavy appsLarge (~300KB)Very Good
PrimeVueRich components, themingMedium (~200KB)Excellent
QuasarComplete framework, MaterialLarge (framework)Good

Vue 3-Tauri Best Practices

  • Use Composition API: Leverage script setup for concise, readable components with Tauri logic
  • Create Composables: Extract Tauri API logic into reusable composables for DRY code
  • Type Everything: Use TypeScript with strict mode for type-safe Tauri commands
  • Reactive Refs: Use ref() for primitive values and reactive() for objects
  • Cleanup Lifecycle: Use onUnmounted to cleanup event listeners preventing leaks
  • Computed Properties: Derive state with computed() instead of manual updates
  • Pinia for Global State: Use Pinia store for complex state management across components
  • Error Handling: Wrap Tauri calls in try-catch and provide user feedback
  • Loading States: Always indicate async operations with loading indicators
  • Single File Components: Keep template, logic, and styles together for maintainability
Vue Reactivity Magic: Vue 3's reactivity system automatically tracks dependencies and updates the DOM when Tauri command results change. No manual subscriptions neededโ€”just assign to reactive refs and Vue handles the rest!

Next Steps

Conclusion

Building desktop applications with Tauri 2.0 and Vue 3 combines Vue's progressive framework philosophy, intuitive Composition API, and powerful reactivity system with Tauri's lightweight architecture creating performant cross-platform applications with excellent developer experience and minimal bundle sizes. Vue 3's Composition API provides perfect abstractions for organizing Tauri functionality into reusable composables maintaining clean separation of concerns, TypeScript integration ensures type safety across frontend-backend boundaries, and Single File Components keep related code colocated improving maintainability throughout application lifecycle. Best practices include creating composables wrapping Tauri APIs for reusability, using reactive refs and computed properties managing async state automatically, implementing Pinia stores for complex global state, properly handling errors and loading states, and leveraging Vue Router for multi-view desktop applications maintaining navigation consistency. The Vue-Tauri combination excels at building desktop applications with intuitive APIs, excellent TypeScript support, flexible architecture allowing incremental adoption of advanced features, and vibrant ecosystem of UI libraries and tools accelerating development. Your Vue expertise translates directly to Tauri development enabling rapid desktop application creation with modern web development patterns and practices!

$ 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.