Documentation Index
Fetch the complete documentation index at: https://mintlify.com/sveltejs/svelte/llms.txt
Use this file to discover all available pages before exploring further.
Migrating from Vue to Svelte offers benefits like smaller bundle sizes, simpler reactivity, and less framework overhead. This guide will help you understand the differences and successfully migrate your Vue applications to Svelte.
Why Migrate to Svelte?
- True Compilation: Svelte compiles to vanilla JavaScript, no runtime framework
- Simpler Reactivity: No need for
.value or reactivity APIs
- Smaller Bundles: Significantly smaller production builds
- Less Magic: More explicit, easier to understand
- Better Performance: Faster runtime with surgical DOM updates
Key Concept Mapping
| Vue Concept | Svelte Equivalent | Notes |
|---|
ref() | $state | Similar reactive state |
reactive() | $state | Unified state primitive |
computed() | $derived | Automatic reactivity |
watch() | $effect | Side effects |
watchEffect() | $effect | Auto-tracking effects |
props | $props() | Destructured props |
emit() | Callback props | No event emitter needed |
slots | Snippets | More powerful composition |
v-model | bind:value | Two-way binding |
v-if | {#if} | Conditional rendering |
v-for | {#each} | List rendering |
v-on | Event attributes | Simpler syntax |
| Composables | Runes | More powerful primitives |
Component Basics
Component Definition
<template>
<div>
<h1>Hello, {{ name }}!</h1>
</div>
</template>
<script setup>
const props = defineProps({
name: String
});
</script>
<script>
let { name } = $props();
</script>
<div>
<h1>Hello, {name}!</h1>
</div>
Reactive State
<template>
<button @click="count++">
Count: {{ count }}
</button>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
Count: {count}
</button>
Computed Properties
<template>
<div>Total: ${{ total }}</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
items: Array
});
const total = computed(() => {
return props.items.reduce((sum, item) => sum + item.price, 0);
});
</script>
<script>
let { items } = $props();
let total = $derived(
items.reduce((sum, item) => sum + item.price, 0)
);
</script>
<div>Total: ${total}</div>
Reactivity Patterns
Watchers and Effects
<script setup>
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
// Watch specific value
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// Watch effect (auto-tracking)
watchEffect(() => {
console.log(`Count is ${count.value}`);
});
</script>
<script>
let count = $state(0);
// Auto-tracking effect
$effect(() => {
console.log(`Count is ${count}`);
});
// Can access previous value with $effect.pre
$effect.pre(() => {
const previous = count;
return () => {
console.log(`Count changed from ${previous} to ${count}`);
};
});
</script>
Reactive Objects
<template>
<div>
<p>{{ user.name }} - {{ user.email }}</p>
<button @click="updateUser">Update</button>
</div>
</template>
<script setup>
import { reactive } from 'vue';
const user = reactive({
name: 'Alice',
email: 'alice@example.com'
});
function updateUser() {
user.name = 'Bob';
}
</script>
<script>
let user = $state({
name: 'Alice',
email: 'alice@example.com'
});
function updateUser() {
user.name = 'Bob'; // Direct mutation works!
}
</script>
<div>
<p>{user.name} - {user.email}</p>
<button onclick={updateUser}>Update</button>
</div>
Arrays and Lists
<template>
<div>
<button @click="addItem">Add Item</button>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
const items = ref([]);
function addItem() {
items.value.push({ id: Date.now(), text: 'New item' });
}
</script>
<script>
let items = $state([]);
function addItem() {
items.push({ id: Date.now(), text: 'New item' });
}
</script>
<div>
<button onclick={addItem}>Add Item</button>
<ul>
{#each items as item (item.id)}
<li>{item.text}</li>
{/each}
</ul>
</div>
Props and Events
Props Definition
<template>
<div>
<h2>{{ title }}</h2>
<p>Age: {{ age }}</p>
</div>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
required: true
},
age: {
type: Number,
default: 0
}
});
</script>
<script>
let { title, age = 0 } = $props();
</script>
<div>
<h2>{title}</h2>
<p>Age: {age}</p>
</div>
Event Emission
<!-- Child.vue -->
<template>
<button @click="handleClick">
Click me
</button>
</template>
<script setup>
const emit = defineEmits(['click']);
function handleClick() {
emit('click', { timestamp: Date.now() });
}
</script>
<!-- Parent.vue -->
<template>
<Child @click="onChildClick" />
</template>
<script setup>
function onChildClick(data) {
console.log('Child clicked:', data);
}
</script>
<!-- Child.svelte -->
<script>
let { onclick } = $props();
function handleClick() {
onclick?.({ timestamp: Date.now() });
}
</script>
<button onclick={handleClick}>
Click me
</button>
<!-- Parent.svelte -->
<script>
function onChildClick(data) {
console.log('Child clicked:', data);
}
</script>
<Child onclick={onChildClick} />
Two-Way Binding (v-model)
<!-- Child.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
<!-- Parent.vue -->
<template>
<Child v-model="text" />
<p>{{ text }}</p>
</template>
<script setup>
import { ref } from 'vue';
const text = ref('');
</script>
<!-- Child.svelte -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
let text = $state('');
</script>
<Child bind:value={text} />
<p>{text}</p>
Template Directives
Conditional Rendering
<template>
<div v-if="isLoggedIn">
Welcome back!
</div>
<div v-else-if="isLoading">
Loading...
</div>
<div v-else>
Please log in
</div>
</template>
{#if isLoggedIn}
<div>Welcome back!</div>
{:else if isLoading}
<div>Loading...</div>
{:else}
<div>Please log in</div>
{/if}
List Rendering
<template>
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
</template>
<ul>
{#each items as item, index (item.id)}
<li>{index}: {item.name}</li>
{/each}
</ul>
Class and Style Binding
<template>
<!-- Class binding -->
<div :class="{ active: isActive, 'text-danger': hasError }">
Content
</div>
<!-- Style binding -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
Styled
</div>
</template>
<!-- Class binding -->
<div class:active={isActive} class:text-danger={hasError}>
Content
</div>
<!-- Style binding -->
<div style:color={textColor} style:font-size="{fontSize}px">
Styled
</div>
Event Handling
<template>
<button @click="count++">{{ count }}</button>
<button @click="handleClick($event)">Click</button>
<form @submit.prevent="handleSubmit">
<input v-model="text" @keyup.enter="search" />
</form>
</template>
<button onclick={() => count++}>{count}</button>
<button onclick={handleClick}>Click</button>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input bind:value={text} onkeyup={(e) => e.key === 'Enter' && search()} />
</form>
Lifecycle Hooks
<script setup>
import { onMounted, onUnmounted, onBeforeUpdate, onUpdated } from 'vue';
onMounted(() => {
console.log('Component mounted');
});
onUnmounted(() => {
console.log('Component unmounted');
});
onBeforeUpdate(() => {
console.log('Before update');
});
onUpdated(() => {
console.log('After update');
});
</script>
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log('Component mounted');
return () => {
console.log('Component unmounted');
};
});
// Use $effect for update tracking
$effect(() => {
console.log('Effect runs on mount and updates');
});
</script>
Slots and Content Projection
Default Slot
<!-- Card.vue -->
<template>
<div class="card">
<slot></slot>
</div>
</template>
<!-- Usage -->
<template>
<Card>
<p>Card content</p>
</Card>
</template>
<!-- Card.svelte -->
<script>
let { children } = $props();
</script>
<div class="card">
{@render children()}
</div>
<!-- Usage -->
<Card>
<p>Card content</p>
</Card>
Named Slots
<!-- Modal.vue -->
<template>
<div class="modal">
<div class="header">
<slot name="header"></slot>
</div>
<div class="body">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<!-- Usage -->
<template>
<Modal>
<template #header>
<h2>Title</h2>
</template>
<p>Content</p>
<template #footer>
<button>Close</button>
</template>
</Modal>
</template>
<!-- Modal.svelte -->
<script>
let { header, children, footer } = $props();
</script>
<div class="modal">
<div class="header">
{@render header()}
</div>
<div class="body">
{@render children()}
</div>
<div class="footer">
{@render footer()}
</div>
</div>
<!-- Usage -->
<Modal>
{#snippet header()}
<h2>Title</h2>
{/snippet}
<p>Content</p>
{#snippet footer()}
<button>Close</button>
{/snippet}
</Modal>
Scoped Slots
<!-- List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="items.indexOf(item)"></slot>
</li>
</ul>
</template>
<!-- Usage -->
<template>
<List :items="users">
<template #default="{ item, index }">
<strong>{{ index }}: {{ item.name }}</strong>
</template>
</List>
</template>
<!-- List.svelte -->
<script>
let { items, children } = $props();
</script>
<ul>
{#each items as item, index (item.id)}
<li>
{@render children(item, index)}
</li>
{/each}
</ul>
<!-- Usage -->
<List items={users}>
{#snippet children(item, index)}
<strong>{index}: {item.name}</strong>
{/snippet}
</List>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="formData.name" />
<input v-model="formData.email" type="email" />
<textarea v-model="formData.message"></textarea>
<input v-model="formData.agree" type="checkbox" />
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { reactive } from 'vue';
const formData = reactive({
name: '',
email: '',
message: '',
agree: false
});
function handleSubmit() {
console.log(formData);
}
</script>
<script>
let formData = $state({
name: '',
email: '',
message: '',
agree: false
});
function handleSubmit(e) {
e.preventDefault();
console.log(formData);
}
</script>
<form onsubmit={handleSubmit}>
<input bind:value={formData.name} />
<input bind:value={formData.email} type="email" />
<textarea bind:value={formData.message}></textarea>
<input bind:checked={formData.agree} type="checkbox" />
<button type="submit">Submit</button>
</form>
Provide/Inject vs Context
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme);
</script>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
</script>
<template>
<div>Theme: {{ theme }}</div>
</template>
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte';
let theme = $state('dark');
setContext('theme', theme);
</script>
<!-- Child.svelte -->
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>
<div>Theme: {theme}</div>
Composables vs Runes
// useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
return {
count,
doubled,
increment,
decrement
};
}
// Usage
<script setup>
const { count, doubled, increment } = useCounter(10);
</script>
// counter.svelte.js
export function createCounter(initialValue = 0) {
let count = $state(initialValue);
let doubled = $derived(count * 2);
function increment() {
count++;
}
function decrement() {
count--;
}
return {
get count() { return count; },
get doubled() { return doubled; },
increment,
decrement
};
}
// Usage
<script>
import { createCounter } from './counter.svelte.js';
const counter = createCounter(10);
</script>
Styling
Scoped Styles
<template>
<div class="container">
<button class="btn">Click me</button>
</div>
</template>
<style scoped>
.container {
padding: 20px;
}
.btn {
background: blue;
color: white;
}
</style>
<div class="container">
<button class="btn">Click me</button>
</div>
<style>
/* Scoped by default - no need for 'scoped' attribute */
.container {
padding: 20px;
}
.btn {
background: blue;
color: white;
}
</style>
Dynamic Styles
<template>
<div :style="{ color: textColor, fontSize: size + 'px' }">
Styled text
</div>
</template>
<script setup>
import { ref } from 'vue';
const textColor = ref('red');
const size = ref(16);
</script>
<script>
let textColor = $state('red');
let size = $state(16);
</script>
<div style:color={textColor} style:font-size="{size}px">
Styled text
</div>
Transitions and Animations
<template>
<Transition name="fade">
<div v-if="show">Content</div>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<script>
import { fade } from 'svelte/transition';
let show = $state(true);
</script>
{#if show}
<div transition:fade={{ duration: 500 }}>
Content
</div>
{/if}
Migration Checklist
Key Differences to Remember
- No
.value: In Svelte, state is accessed directly without .value
- Scoped by Default: Styles are scoped without a
scoped attribute
- No Setup Function: No need for
<script setup> - it’s the default
- True Compilation: Svelte compiles to vanilla JavaScript
- Simpler Syntax: Less boilerplate, more intuitive
- Built-in Animations: Transitions and animations are built-in
Next Steps