Svelte from Beginner to Expert: A Learning Path
Svelte has emerged as one of the most exciting frontend frameworks in recent years. Unlike React or Vue, which do most of their work in the browser at runtime, Svelte shifts that work into a compile step that happens when you build your app. This fundamental difference makes Svelte applications faster, smaller, and often simpler to write. This tutorial walks you through everything you need to know to go from a complete beginner to an expert Svelte developer.
What Is Svelte?
Svelte is a component-based JavaScript framework created by Rich Harris. It allows developers to build user interfaces using a syntax that closely resembles standard HTML, CSS, and JavaScript. The key distinction is that Svelte is a compiler, not a runtime framework. When you build your application, Svelte transforms your components into highly optimized vanilla JavaScript that surgically updates the DOM when state changes.
This compile-time approach means your users never download a large framework runtime. The bundle only contains the code your application actually needs, which results in smaller payloads and faster load times.
Why Svelte Matters
- Smaller bundles: No virtual DOM, no runtime framework overhead.
- Better performance: Direct DOM updates instead of diffing.
- Less boilerplate: Reactivity is built into the language, not added through hooks or observables.
- Gentle learning curve: If you know HTML, CSS, and JavaScript, you already know most of Svelte.
- Excellent developer experience: Built-in scoped styles, transitions, animations, and stores.
Setting Up Your First Svelte Project
The easiest way to start a new Svelte project is using the official scaffolding tool, Vite. Vite provides a fast development server and an optimized build pipeline.
npm create vite@latest my-svelte-app -- --template svelte
cd my-svelte-app
npm install
npm run dev
Once the dev server is running, open your browser to the displayed URL. You should see a default Svelte welcome page. The project structure is intentionally minimal, with your components living in the src directory.
Svelte Component Basics
A Svelte component is a single .svelte file that contains three sections: markup, a script tag for logic, and a style tag for CSS. Let us look at a simple example.
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
<style>
h1 {
color: tomato;
}
</style>
The curly braces in the markup indicate a JavaScript expression. Svelte evaluates these expressions and inserts the result into the DOM. When the name variable changes, Svelte automatically updates the DOM.
Reactivity in Svelte
Reactivity in Svelte is triggered by assignment. When you assign a new value to a variable that is referenced in the markup, Svelte updates the affected parts of the DOM. This is a much simpler model than the hook-based approach in React.
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
For reactive declarations that depend on other values, Svelte provides the $: label syntax. Any statement prefixed with $: will re-run whenever its dependencies change.
<script>
let a = 1;
let b = 2;
$: sum = a + b;
$: console.log(`The sum is ${sum}`);
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>Sum: {sum}</p>
Props and Component Composition
Components can receive data from their parents through props. In Svelte, props are declared using the export keyword inside the script block.
<!-- Child.svelte -->
<script>
export let title = 'Default Title';
export let items = [];
</script>
<h2>{title}</h2>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
const fruits = ['Apple', 'Banana', 'Cherry'];
</script>
<Child title="Fruit List" items={fruits} />
Two-Way Binding
Svelte supports two-way binding using the bind: directive. This is particularly useful for form inputs.
<script>
let name = '';
let agree = false;
let color = '#ff0000';
</script>
<input bind:value={name} placeholder="Your name">
<input type="checkbox" bind:checked={agree}>
<input type="color" bind:value={color}>
<p>Hello, {name || 'stranger'}!</p>
<p>You {agree ? 'agree' : 'do not agree'}.</p>
<p>Your color: {color}</p>
Conditional Rendering and Loops
Svelte provides clean syntax for conditional rendering and list iteration. The {#if} block handles conditionals, while {#each} handles loops.
<script>
let user = { name: 'Alice', loggedIn: true };
let todos = [
{ id: 1, text: 'Learn Svelte', done: true },
{ id: 2, text: 'Build an app', done: false },
{ id: 3, text: 'Deploy to production', done: false }
];
</script>
{#if user.loggedIn}
<p>Welcome back, {user.name}!</p>
{:else}
<p>Please log in.</p>
{/if}
<ul>
{#each todos as todo, i}
<li class:done={todo.done}>
{i + 1}. {todo.text}
{#if todo.done}โ{/if}
</li>
{:else}
<li>No todos yet.</li>
{/each}
</ul>
<style>
.done { text-decoration: line-through; opacity: 0.6; }
</style>
Event Handling
Event listeners are attached using the on: directive. Svelte also supports event modifiers like preventDefault, stopPropagation, and once.
<script>
function handleClick(event) {
alert('Button clicked!');
}
function handleSubmit(event) {
event.preventDefault();
console.log('Form submitted');
}
function handleKeydown(event) {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
}
</script>
<button on:click={handleClick}>Click me</button>
<form on:submit={handleSubmit}>
<input on:keydown={handleKeydown} placeholder="Type and press Enter">
<button type="submit">Submit</button>
</form>
<button on:click|once={() => alert('This only fires once')}>
One-time button
</button>
Component Events
Child components can dispatch custom events using createEventDispatcher. Parents listen to these events using the same on: directive.
<!-- Child.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function notify() {
dispatch('notify', {
message: 'Something happened!',
timestamp: Date.now()
});
}
</script>
<button on:click={notify}>Notify Parent</button>
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
function handleNotify(event) {
console.log(event.detail.message);
console.log(event.detail.timestamp);
}
</script>
<Child on:notify={handleNotify} />
State Management with Stores
For state that needs to be shared across many components, Svelte provides stores. A store is simply an object with a subscribe method. Svelte includes two writable store types: writable and readable, plus a derived store for computed values.
<!-- stores.js -->
import { writable, derived } from 'svelte/store';
export const count = writable(0);
export const doubled = derived(count, $count => $count * 2);
export const reset = () => count.set(0);
<!-- Counter.svelte -->
<script>
import { count, doubled, reset } from './stores.js';
function increment() {
count.update(n => n + 1);
}
</script>
<h1>Count: {$count}</h1>
<p>Doubled: {$doubled}</p>
<button on:click={increment}>+</button>
<button on:click={reset}>Reset</button>
The $ prefix is Svelte shorthand for subscribing to a store. It automatically subscribes when the component mounts and unsubscribes when it unmounts, preventing memory leaks.
Creating a Custom Store
Any object that implements the subscribe contract can be used as a store. This lets you create stores with custom logic.
<script>
import { writable } from 'svelte/store';
function createTodoStore() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
add: (text) => update(todos => [...todos, { id: Date.now(), text, done: false }]),
toggle: (id) => update(todos => todos.map(t => t.id === id ? { ...t, done: !t.done } : t)),
remove: (id) => update(todos => todos.filter(t => t.id !== id)),
clear: () => set([])
};
}
const todos = createTodoStore();
let newText = '';
</script>
<form on:submit|preventDefault={() => { todos.add(newText); newText = ''; }}>
<input bind:value={newText} placeholder="New todo">
<button type="submit">Add</button>
</form>
<ul>
{#each $todos as todo (todo.id)}
<li>
<input type="checkbox" bind:checked={todo.done}>
<span class:done={todo.done}>{todo.text}</span>
<button on:click={() => todos.remove(todo.id)}>x</button>
</li>
{/each}
</ul>
<style>
.done { text-decoration: line-through; }
</style>
Lifecycle Methods
Svelte provides four lifecycle functions: onMount, onDestroy, beforeUpdate, and afterUpdate. These let you run code at specific points in a component's life.
<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
let data = [];
let timer;
onMount(async () => {
const response = await fetch('https://api.example.com/data');
data = await response.json();
timer = setInterval(() => console.log('tick'), 1000);
});
onDestroy(() => {
clearInterval(timer);
});
beforeUpdate(() => {
console.log('DOM is about to update');
});
afterUpdate(() => {
console.log('DOM just updated');
});
</script>
<ul>
{#each data as item}
<li>{item.name}</li>
{/each}
</ul>
Transitions and Animations
One of Svelte's standout features is its built-in transition system. You can animate elements entering and leaving the DOM with minimal code.
<script>
import { fade, fly, slide, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = true;
let items = [1, 2, 3];
</script>
<label>
<input type="checkbox" bind:checked={visible}>
Toggle
</label>
{#if visible}
<p transition:fade>I fade in and out</p>
<p in:fly={{ x: 100, duration: 500 }} out:scale>I fly in and scale out</p>
{/if}
<button on:click={() => items = [...items, items.length + 1]}>Add</button>
<button on:click={() => items = items.slice(0, -1)}>Remove</button>
{#each items as item (item)}
<div transition:slide={{ duration: 300, easing: quintOut }}>
Item {item}
</div>
{/each}
Actions
Actions are functions that run when an element is added to the DOM. They are perfect for integrating third-party libraries or adding reusable behaviors like tooltips, lazy loading, or drag and drop.
<script>
function longpress(node, duration) {
let timer;
const handleMousedown = () => {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent('longpress'));
}, duration);
};
const handleMouseup = () => {
clearTimeout(timer);
};
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('mouseup', handleMouseup);
return {
update(newDuration) {
duration = newDuration;
},
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('mouseup', handleMouseup);
}
};
}
</script>
<button use:longpress={500} on:longpress={() => alert('Long pressed!')}>
Press and hold me
</button>
Slots and Content Projection
Slots allow you to pass content into a component from its parent. This is similar to children in React or ng-content in Angular.
<!-- Card.svelte -->
<div class="card">
<div class="card-header">
<slot name="header">Default Header</slot>
</div>
<div class="card-body">
<slot>Default body content</slot>
</div>
<div class="card-footer">
<slot name="footer" />
</div>
</div>
<style>
.card { border: 1px solid #ccc; border-radius: 8px; overflow: hidden; }
.card-header, .card-footer { background: #f5f5f5; padding: 12px; }
.card-body { padding: 16px; }
</style>
<!-- App.svelte -->
<script>
import Card from './Card.svelte';
</script>
<Card>
<svelte:fragment slot="header">
<h3>My Card Title</h3>
</svelte:fragment>
<p>This is the main content of the card.</p>
<p>You can put anything here.</p>
<div slot="footer">
<button>Save</button>
<button>Cancel</button>
</div>
</Card>
Context API
For sharing state deeply through a component tree without prop drilling, Svelte offers the Context API. Context is set by a parent component and accessed by any descendant.
<!-- Parent.svelte -->
<script>
import { setContext } from 'svelte';
import Child from './Child.svelte';
setContext('theme', {
color: 'dark',
toggle: () => console.log('toggle theme')
});
</script>
<Child />
<-- DeepChild.svelte -->
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>
<p>Current theme: {theme.color}</p>
<button on:click={theme.toggle}>Toggle Theme</button>
Special Elements
Svelte includes several special elements that give you control over rendering behavior.
<script>
import BrowserWarning from './BrowserWarning.svelte';
import ServerOnly from './ServerOnly.svelte';
</script>
<!-- Render only on the client -->
<svelte:head>
<title>My Svelte App</title>
<meta name="description" content="Built with Svelte">
</svelte:head>
<!-- Access window dimensions -->
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<!-- Access document body -->
<svelte:body on:mouseenter={handleMouseEnter} />
<!-- Dynamic component rendering -->
<svelte:component this={currentComponent} {...props} />
<!-- Self-rendering component -->
<svelte:self />
SvelteKit: The Full-Stack Framework
Once you are comfortable with Svelte components, the next step is SvelteKit. SvelteKit is the official application framework built on top of Svelte. It provides routing, server-side rendering, API endpoints, and deployment adapters.
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
SvelteKit uses file-based routing. Each file in src/routes becomes a route in your application.
<!-- src/routes/+page.svelte -->
<script>
export let data;
</script>
<h1>{data.title}</h1>
<p>{data.description}</p>
// src/routes/+page.server.js
export async function load() {
return {
title: 'Welcome to SvelteKit',
description: 'Server-side loaded data'
};
}
Layouts in SvelteKit
Layout components wrap every page in their directory and below. This is useful for navigation, headers, and footers.
<!-- src/routes/+layout.svelte -->
<script>
import { page } from '$app/stores';
let { children } = $props();
</script>
<nav>
<a href="/" class:active={$page.url.pathname === '/'}>Home</a>
<a href="/about" class:active={$page.url.pathname === '/about'}>About</a>
<a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>Blog</a>
</nav>
<main>
{@render children()}
</main>
<style>
nav { display: flex; gap: 16px; padding: 16px; background: #333; }
nav a { color: white; text-decoration: none; }
nav a.active { text-decoration: underline; }
</style>
Best Practices
Keep Components Small and Focused
Each component should do one thing well. If a component grows beyond 200 lines, consider splitting it into smaller pieces. This improves readability, testability, and reusability.
Use Stores for Shared State
Do not pass props through many layers. If more than two components need the same state, lift it into a store. This keeps your component interfaces clean and avoids prop drilling.
Leverage Scoped Styles
Svelte scopes styles to components by default. Use this to your advantage. Avoid global styles unless absolutely necessary, and prefer class-based styling over inline styles for maintainability.
Use Reactive Statements Wisely
The $: syntax is powerful but can lead to confusing code if overused. Keep reactive statements simple and avoid chaining too many together. If logic becomes complex, extract it into a function or a derived store.
Handle Errors Gracefully
In SvelteKit, use error boundaries and the error store to handle unexpected errors. Always validate data on the server before sending it to the client.
// src/routes/+error.svelte
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error.message}</h1>
<a href="/">Go home</a>
Optimize Bundle Size
Use dynamic imports for large dependencies that are only needed on certain routes. SvelteKit automatically code-splits by route, but you can further optimize with manual imports.
<script>
let chart;
async function loadChart() {
const { default: Chart } = await import('chart.js');
chart = new Chart(/* ... */);
}
</script>
<button on:click={loadChart}>Load Chart</button>
Write Tests
Use Vitest and Testing Library to test your components. Focus on user interactions rather than implementation details.
import { render, fireEvent } from '@testing-library/svelte';
import Counter from './Counter.svelte';
test('increments when clicked', async () => {
const { getByText } = render(Counter);
const button = getByText('Clicked 0 times');
await fireEvent.click(button);
expect(button.textContent).toBe('Clicked 1 time');
});
Use TypeScript
Svelte has excellent TypeScript support. Type your props, stores, and API responses to catch errors at compile time.
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
export let user: User;
function updateUser(field: keyof User, value: string): void {
user = { ...user, [field]: value };
}
</script>
<input value={user.name} on:input={(e) => updateUser('name', e.target.value)} />
<input value={user.email} on:input={(e) => updateUser('email', e.target.value)} />
Advanced Patterns
Higher-Order Components
While Svelte does not have HOCs in the React sense, you can achieve similar composition using wrapper components and slots or context.
Reactive Context with Stores
Combine the Context API with stores to create reactive, scoped state that updates across the component tree.
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
const cart = writable([]);
setContext('cart', {
subscribe: cart.subscribe,
add: (item) => cart.update(items => [...items, item]),
remove: (id) => cart.update(items => items.filter(i => i.id !== id)),
clear: () => cart.set([])
});
</script>
Using Svelte in Existing Projects
Svelte can be embedded into existing applications. You can mount a Svelte component into any DOM element using the client-side API.
import App from './App.svelte';
const app = new App({
target: document.getElementById('svelte-root'),
props: {
initialData: window.__INITIAL_DATA__
}
});
export default app;
Conclusion
Svelte offers a refreshingly simple approach to building web applications. Its compile-time strategy eliminates the need for a virtual DOM, resulting in smaller bundles and better performance. By starting with the basics of components, reactivity, and props, then progressing through stores, lifecycle methods, transitions, and finally SvelteKit for full-stack development, you can build a solid foundation as a Svelte developer. The key to mastery is practice: build small projects, experiment with the patterns covered here, and gradually take on more complex applications. With its gentle learning curve, excellent documentation, and growing ecosystem, Svelte is a framework worth investing your time in. Whether you are building a small widget or a large-scale application, Svelte provides the tools you need to ship fast, maintainable, and delightful user experiences.