Table of Contents
- What is Svelte?
- Setting Up Your Development Environment
- Core Svelte Concepts
- Hands-On Project: Building a To-Do App
- Advanced Concepts
- Testing Svelte Components
- Deploying Your Svelte App
- Conclusion
- References
What is Svelte?
Svelte is a component-based frontend compiler (not a framework) created by Rich Harris in 2016. Unlike frameworks like React or Vue, which run in the browser and use a virtual DOM to update the UI, Svelte works at build time. It compiles your components into highly optimized vanilla JavaScript, eliminating the need for a runtime library to manage state or DOM updates.
Key Benefits of Svelte:
- Performance: No virtual DOM overhead; updates are direct and efficient.
- Simplicity: Minimal boilerplate; HTML, CSS, and JavaScript live together in components.
- Reactivity by Default: Declare state and let Svelte handle updates automatically.
- Small Bundle Sizes: Compiled code is lightweight, leading to faster load times.
Setting Up Your Development Environment
To start building with Svelte, you’ll need Node.js (v16.0 or later) installed. We’ll use SvelteKit—the official full-stack framework for Svelte—to streamline setup, routing, and deployment.
Step 1: Create a New SvelteKit Project
Open your terminal and run:
npm create svelte@latest my-svelte-app
Step 2: Configure Your Project
Follow the prompts to set up your project:
- Choose a “Skeleton project” (minimal setup).
- Select TypeScript (optional but recommended for type safety).
- Add ESLint and Prettier for code quality.
Step 3: Install Dependencies and Run
cd my-svelte-app
npm install
npm run dev
Your app will run at http://localhost:5173. Open it in your browser to see the default SvelteKit page.
Core Svelte Concepts
Components: The Building Blocks
Svelte apps are built with components—self-contained files with .svelte extensions that combine HTML, CSS, and JavaScript. A basic component looks like this:
<!-- src/lib/Counter.svelte -->
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<style>
button {
padding: 0.5rem 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
<script>: Contains component logic (state, functions).- Markup: Defines the component’s structure.
<style>: Scoped CSS (only affects this component).
Reactivity: How Svelte Handles State
Svelte’s reactivity is compile-time and declarative. When you update a variable, Svelte automatically updates the DOM—no setState or useState needed.
Basic Reactivity:
<script>
let name = 'World';
</script>
<input bind:value={name} />
<p>Hello {name}!</p>
Here, bind:value={name} creates a two-way binding: updating the input updates name, and vice versa.
Reactive Declarations with $::
For derived state (values computed from other variables), use $: to mark expressions as reactive:
<script>
let a = 1;
let b = 2;
$: sum = a + b; // Re-runs when a or b changes
</script>
<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<p>Sum: {sum}</p>
Props: Passing Data Between Components
Props let you pass data from a parent to a child component. Declare props in the child with export let:
<!-- src/lib/Greeting.svelte (Child) -->
<script>
export let name; // Prop from parent
export let greeting = 'Hello'; // Default value
</script>
<p>{greeting}, {name}!</p>
Use the child component in a parent:
<!-- src/routes/+page.svelte (Parent) -->
<script>
import Greeting from '$lib/Greeting.svelte';
</script>
<Greeting name="Alice" /> <!-- Renders: "Hello, Alice!" -->
<Greeting name="Bob" greeting="Hi" /> <!-- Renders: "Hi, Bob!" -->
Events: Handling User Interactions
Svelte uses the on: directive to handle events (e.g., on:click, on:submit).
Example: Form Submission
<script>
let username = '';
function handleSubmit(event) {
event.preventDefault(); // Prevent default form behavior
alert(`Hello, ${username}!`);
}
</script>
<form on:submit={handleSubmit}>
<input
type="text"
bind:value={username}
placeholder="Enter your name"
required
/>
<button type="submit">Submit</button>
</form>
Event Modifiers:
Svelte provides modifiers to simplify common event patterns:
on:click|preventDefault: Equivalent toevent.preventDefault().on:submit|stopPropagation: Stops event bubbling.on:keydown|enter: Triggers only on Enter key press.
Conditionals & Loops: Dynamic Rendering
Conditionals with {#if}:
<script>
let isLoggedIn = false;
</script>
{#if isLoggedIn}
<p>Welcome back!</p>
{:else}
<p>Please log in.</p>
{/if}
<button on:click={() => isLoggedIn = !isLoggedIn}>
{isLoggedIn ? 'Log Out' : 'Log In'}
</button>
Loops with {#each}:
Render lists using {#each items as item}:
<script>
let fruits = ['Apple', 'Banana', 'Cherry'];
</script>
<ul>
{#each fruits as fruit (fruit)} <!-- (fruit) is a unique key -->
<li>{fruit}</li>
{/each}
</ul>
The (fruit) key helps Svelte efficiently update the DOM when the list changes.
Building a Hands-On Project: To-Do App
Let’s build a to-do app to apply what we’ve learned. We’ll create a component that lets users add, display, and delete todos, with data persistence via local storage.
Step 1: Setting Up the Project
If you haven’t already, create a new SvelteKit project (as shown earlier) and navigate to the src/routes/+page.svelte file—this is our app’s main page.
Step 2: Creating the To-Do Component
First, create a TodoApp.svelte component in src/lib:
<!-- src/lib/TodoApp.svelte -->
<script>
// State will go here
</script>
<div class="todo-app">
<h1>Todo App</h1>
<!-- Input and todo list will go here -->
</div>
<style>
.todo-app {
max-width: 500px;
margin: 2rem auto;
padding: 1rem;
font-family: Arial, sans-serif;
}
</style>
Step 3: Adding State and User Input
Add state for todos and a new todo input. Use two-way binding to sync the input with the state:
<script>
let todos = []; // Array to hold todos
let newTodoText = ''; // Tracks input value
function addTodo() {
if (!newTodoText.trim()) return; // Ignore empty input
todos = [...todos, {
id: Date.now(), // Unique ID
text: newTodoText,
completed: false
}];
newTodoText = ''; // Clear input
}
</script>
<div class="todo-app">
<h1>Todo App</h1>
<div class="input-container">
<input
type="text"
bind:value={newTodoText}
placeholder="Add a new todo..."
on:keydown|enter={addTodo} <!-- Add on Enter -->
/>
<button on:click={addTodo}>Add</button>
</div>
</div>
<style>
/* Add input styles */
.input-container {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 0.5rem 1rem;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
Step 4: Displaying and Managing To-Dos
Add logic to display todos, mark them as complete, and delete them:
<script>
// ... (previous code: todos, newTodoText, addTodo)
function toggleTodo(id) {
todos = todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
}
</script>
<!-- ... (input container) -->
<ul class="todo-list">
{#each todos as todo (todo.id)}
<li class:completed={todo.completed}>
<input
type="checkbox"
checked={todo.completed}
on:change={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button on:click={() => deleteTodo(todo.id)}>×</button>
</li>
{/each}
</ul>
<style>
/* Add todo list styles */
.todo-list {
list-style: none;
padding: 0;
gap: 0.5rem;
display: flex;
flex-direction: column;
}
.todo-list li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid #eee;
border-radius: 4px;
}
.completed span {
text-decoration: line-through;
color: #888;
}
.todo-list button {
margin-left: auto;
background: #dc3545;
padding: 0.25rem 0.5rem;
}
</style>
Step 5: Persisting Data with Local Storage
Use localStorage to save todos so they persist across page refreshes. Add reactive declarations to load and save todos:
<script>
// Load todos from localStorage on initial load
let todos = JSON.parse(localStorage.getItem('todos')) || [];
let newTodoText = '';
// Save todos to localStorage whenever todos change
$: {
localStorage.setItem('todos', JSON.stringify(todos));
}
// ... (addTodo, toggleTodo, deleteTodo functions remain the same)
</script>
Using the TodoApp Component
Import TodoApp into src/routes/+page.svelte to render it:
<!-- src/routes/+page.svelte -->
<script>
import TodoApp from '$lib/TodoApp.svelte';
</script>
<TodoApp />
Run npm run dev—your todo app should now work!
Advanced Concepts
Lifecycle Functions
Svelte provides lifecycle functions to run code at specific stages of a component’s life:
onMount: Runs after the component is added to the DOM.onDestroy: Runs before the component is removed from the DOM.beforeUpdate/afterUpdate: Run before/after the component updates.
Example with onMount:
<script>
import { onMount } from 'svelte';
let data;
onMount(async () => {
const response = await fetch('https://api.example.com/data');
data = await response.json();
});
</script>
{#if data}
<pre>{JSON.stringify(data, null, 2)}</pre>
{:else}
<p>Loading...</p>
{/if}
Stores: Managing Global State
For state shared across components, use stores. Svelte provides three store types:
- Writable: Read/write state.
- Readable: Read-only state.
- Derived: Computed from other stores.
Example: Writable Store for Todos
Create a store in src/lib/stores.js:
// src/lib/stores.js
import { writable } from 'svelte/store';
// Load todos from localStorage
const storedTodos = JSON.parse(localStorage.getItem('todos')) || [];
export const todos = writable(storedTodos);
// Save to localStorage when todos change
todos.subscribe(($todos) => {
localStorage.setItem('todos', JSON.stringify($todos));
});
Use the store in TodoApp.svelte:
<script>
import { todos } from '$lib/stores';
let newTodoText = '';
function addTodo() {
if (!newTodoText.trim()) return;
todos.update(($todos) => [...$todos, { id: Date.now(), text: newTodoText, completed: false }]);
newTodoText = '';
}
// ... (toggleTodo and deleteTodo use todos.update)
</script>
<!-- In the markup, access store values with $todos -->
{#each $todos as todo (todo.id)}
<!-- ... -->
{/each}
Transitions & Animations
Svelte has built-in support for transitions and animations via the svelte/transition package. Add fade animations when todos are added or removed:
<script>
import { fade, slide } from 'svelte/transition';
// ... (other imports)
</script>
{#each $todos as todo (todo.id)}
<li
class:completed={todo.completed}
in:fade={{ duration: 300 }}
out:slide={{ duration: 300 }}
>
<!-- ... -->
</li>
{/each}
Testing Svelte Components
Test Svelte components with Vitest (built into SvelteKit) and Testing Library. Create a test file src/lib/TodoApp.test.js:
import { render, screen, fireEvent } from '@testing-library/svelte';
import TodoApp from './TodoApp.svelte';
test('adds a new todo', async () => {
render(TodoApp);
const input = screen.getByPlaceholderText('Add a new todo...');
const button = screen.getByText('Add');
fireEvent.input(input, { target: { value: 'Learn Svelte' } });
fireEvent.click(button);
expect(screen.getByText('Learn Svelte')).toBeInTheDocument();
});
Run tests with:
npm run test
Deploying Your Svelte App
SvelteKit apps are easy to deploy. For static hosting (e.g., Netlify, Vercel), use the static adapter:
- Install the adapter:
npm install -D @sveltejs/adapter-static
- Update
svelte.config.js:
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter()
}
};
- Build the app:
npm run build
- Deploy the
builddirectory to your hosting platform.
Conclusion
Svelte’s compile-time approach makes it a powerful tool for building fast, interactive UIs with minimal boilerplate. In this tutorial, we covered core concepts like components, reactivity, props, and events, then built a functional todo app with state persistence, transitions, and testing.
To deepen your skills, explore SvelteKit’s routing, server-side rendering, and API endpoints. The Svelte community is growing rapidly, with plenty of resources to help you learn more!