Svelte Development Skill
This skill provides comprehensive guidance for building modern Svelte applications using reactivity runes (Svelte 5), components, stores, lifecycle hooks, transitions, and animations based on official Svelte documentation.
When to Use This Skill
Use this skill when:
Building high-performance web applications with minimal JavaScript overhead Creating single-page applications (SPAs) with reactive UI Developing interactive user interfaces with compile-time optimization Building embedded widgets and components with small bundle sizes Implementing real-time dashboards and data visualizations Creating progressive web apps (PWAs) with excellent performance Developing component libraries with native reactivity Building server-side rendered applications with SvelteKit Migrating from frameworks with virtual DOM to compiled approach Creating accessible and performant web applications Core Concepts Reactivity with Runes (Svelte 5)
Svelte 5 introduces runes, a new way to declare reactive state with better TypeScript support and clearer semantics.
$state Rune:
<script> let count = $state(0); let user = $state({ name: 'Alice', age: 30 }); function increment() { count++; } function updateAge() { user.age++; } </script>$derived Rune:
<script> let count = $state(0); let doubled = $derived(count * 2); let quadrupled = $derived(doubled * 2); // Complex derived values let users = $state([ { name: 'Alice', active: true }, { name: 'Bob', active: false }, { name: 'Charlie', active: true } ]); let activeUsers = $derived(users.filter(u => u.active)); let activeCount = $derived(activeUsers.length); </script>Count: {count}
Doubled: {doubled}
Quadrupled: {quadrupled}
Active users: {activeCount}
$effect Rune:
<script> let count = $state(0); let name = $state('Alice'); // Effect runs when dependencies change $effect(() => { console.log(`Count is now ${count}`); document.title = `Count: ${count}`; }); // Effect with cleanup $effect(() => { const interval = setInterval(() => { count++; }, 1000); return () => { clearInterval(interval); }; }); // Conditional effects $effect(() => { if (count > 10) { console.log('Count exceeded 10!'); } }); </script>$props Rune:
<script> // Type-safe props in Svelte 5 let { name, age = 18, onClick } = $props(); // With TypeScript interface Props { name: string; age?: number; onClick?: () => void; } let { name, age = 18, onClick }: Props = $props(); </script>{name}
Age: {age}
{#if onClick} {/if}Components
Components are the building blocks of Svelte applications. Each component is a single file with script, markup, and styles.
Basic Component Structure:
<script> // Component logic let name = $state('World'); let count = $state(0); function handleClick() { count++; } </script>Hello {name}!
Count: {count}
Component Props:
<script> let { title, description, imageUrl, onClick } = $props(); </script>{title}
{description}
Component Events:
<script> import { createEventDispatcher } from 'svelte'; let { variant = 'primary', disabled = false } = $props(); const dispatch = createEventDispatcher(); function handleClick() { dispatch('click', { timestamp: Date.now() }); } </script><button class="btn {variant}" {disabled} on:click={handleClick}
<style> .btn { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; } .primary { background: #ff3e00; color: white; } .secondary { background: #676778; color: white; } </style> <script> import Button from './Button.svelte'; function handleButtonClick(event) { console.log('Clicked at:', event.detail.timestamp); } </script>
Slots and Composition:
<script> let { isOpen = false, onClose } = $props(); </script>{#if isOpen}
{/if}
<style> .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: white; border-radius: 8px; padding: 2rem; max-width: 500px; width: 90%; position: relative; } .close-btn { position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 2rem; cursor: pointer; } </style> <script> import Modal from './Modal.svelte'; let isModalOpen = $state(false); </script>Custom Title
This is custom modal content.
Stores
Stores are observable values that can be shared across components.
Writable Store:
// stores.js import { writable } from 'svelte/store';
export const count = writable(0);
export const user = writable({ name: 'Guest', loggedIn: false });
export const todos = writable([]);
// Custom store with methods function createCounter() { const { subscribe, set, update } = writable(0);
return { subscribe, increment: () => update(n => n + 1), decrement: () => update(n => n - 1), reset: () => set(0) }; }
export const counter = createCounter();
Using Stores in Components:
<script> import { count, user, counter } from './stores.js'; // Auto-subscription with $ $: console.log('Count changed:', $count); function increment() { count.update(n => n + 1); } function login() { user.set({ name: 'Alice', loggedIn: true }); } </script>Count: {$count}
Welcome, {$user.name}!
{#if !$user.loggedIn}
Counter: {$counter}
Readable Store:
// stores.js import { readable } from 'svelte/store';
export const time = readable(new Date(), (set) => { const interval = setInterval(() => { set(new Date()); }, 1000);
return () => clearInterval(interval); });
export const mousePosition = readable({ x: 0, y: 0 }, (set) => { const handleMouseMove = (e) => { set({ x: e.clientX, y: e.clientY }); };
window.addEventListener('mousemove', handleMouseMove);
return () => { window.removeEventListener('mousemove', handleMouseMove); }; });
Derived Store:
// stores.js import { writable, derived } from 'svelte/store';
export const todos = writable([ { id: 1, text: 'Buy milk', done: false }, { id: 2, text: 'Walk dog', done: true }, { id: 3, text: 'Code review', done: false } ]);
export const completedTodos = derived( todos, $todos => $todos.filter(t => t.done) );
export const activeTodos = derived( todos, $todos => $todos.filter(t => !t.done) );
export const todoStats = derived( todos, $todos => ({ total: $todos.length, completed: $todos.filter(t => t.done).length, active: $todos.filter(t => !t.done).length }) );
// Derived from multiple stores export const firstName = writable('Alice'); export const lastName = writable('Smith');
export const fullName = derived(
[firstName, lastName],
([$firstName, $lastName]) => ${$firstName} ${$lastName}
);
Custom Store with Complex Logic:
// stores/cart.js import { writable, derived } from 'svelte/store';
function createCart() { const { subscribe, set, update } = writable([]);
return { subscribe, addItem: (item) => update(items => { const existing = items.find(i => i.id === item.id); if (existing) { return items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ); } return [...items, { ...item, quantity: 1 }]; }), removeItem: (id) => update(items => items.filter(i => i.id !== id) ), updateQuantity: (id, quantity) => update(items => items.map(i => i.id === id ? { ...i, quantity } : i) ), clear: () => set([]) }; }
export const cart = createCart();
export const cartTotal = derived( cart, $cart => $cart.reduce((sum, item) => sum + item.price * item.quantity, 0) );
export const cartItemCount = derived( cart, $cart => $cart.reduce((count, item) => count + item.quantity, 0) );
Lifecycle Hooks
Lifecycle hooks let you run code at specific points in a component's lifecycle.
onMount:
<script> import { onMount } from 'svelte'; let data = $state([]); let loading = $state(true); let error = $state(null); onMount(async () => { try { const response = await fetch('/api/data'); if (!response.ok) throw new Error('Failed to fetch'); data = await response.json(); } catch (err) { error = err.message; } finally { loading = false; } }); // onMount with cleanup onMount(() => { const interval = setInterval(() => { console.log('Tick'); }, 1000); return () => { clearInterval(interval); }; }); </script>{#if loading}
Loading...
{:else if error}
Error: {error}
{:else}
-
{#each data as item}
- {item.name} {/each}
{/if}
onDestroy:
<script> import { onDestroy } from 'svelte'; const subscription = eventSource.subscribe(data => { console.log(data); }); onDestroy(() => { subscription.unsubscribe(); }); // Multiple cleanup operations onDestroy(() => { console.log('Component is being destroyed'); }); </script>beforeUpdate and afterUpdate:
<script> import { beforeUpdate, afterUpdate } from 'svelte'; let div; let autoscroll = $state(true); beforeUpdate(() => { if (div) { const scrollableDistance = div.scrollHeight - div.offsetHeight; autoscroll = div.scrollTop > scrollableDistance - 20; } }); afterUpdate(() => { if (autoscroll) { div.scrollTo(0, div.scrollHeight); } }); </script>tick:
<script> import { tick } from 'svelte'; let text = $state(''); let textarea; async function handleKeydown(event) { if (event.key === 'Tab') { event.preventDefault(); const { selectionStart, selectionEnd, value } = textarea; text = value.slice(0, selectionStart) + '\t' + value.slice(selectionEnd); // Wait for DOM to update await tick(); // Set cursor position textarea.selectionStart = textarea.selectionEnd = selectionStart + 1; } } </script>
Transitions and Animations
Svelte provides built-in transitions and animations for smooth UI effects.
Built-in Transitions:
<script> import { fade, fly, slide, scale, blur } from 'svelte/transition'; import { quintOut } from 'svelte/easing'; let visible = $state(true); </script>{#if visible}
{/if}
In and Out Transitions:
<script> import { fade, fly } from 'svelte/transition'; let visible = $state(true); </script>{#if visible}
{/if}
Custom Transitions:
<script> import { cubicOut } from 'svelte/easing'; function typewriter(node, { speed = 1 }) { const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE; if (!valid) return {}; const text = node.textContent; const duration = text.length / (speed * 0.01); return { duration, tick: t => { const i = Math.trunc(text.length * t); node.textContent = text.slice(0, i); } }; } function spin(node, { duration }) { return { duration, css: t => { const eased = cubicOut(t); return ` transform: scale(${eased}) rotate(${eased * 360}deg); opacity: ${eased}; `; } }; } let visible = $state(false); </script>{#if visible}
This text will type out character by character
{/if}
Animations:
<script> import { flip } from 'svelte/animate'; import { quintOut } from 'svelte/easing'; let todos = $state([ { id: 1, text: 'Buy milk' }, { id: 2, text: 'Walk dog' }, { id: 3, text: 'Code review' } ]); function shuffle() { todos = todos.sort(() => Math.random() - 0.5); } </script>-
{#each todos as todo (todo.id)}
- {todo.text} {/each}
Deferred Transitions:
<script> import { quintOut } from 'svelte/easing'; import { crossfade } from 'svelte/transition'; const [send, receive] = crossfade({ duration: d => Math.sqrt(d * 200), fallback(node, params) { const style = getComputedStyle(node); const transform = style.transform === 'none' ? '' : style.transform; return { duration: 600, easing: quintOut, css: t => ` transform: ${transform} scale(${t}); opacity: ${t} ` }; } }); let todos = $state([ { id: 1, text: 'Buy milk', done: false }, { id: 2, text: 'Walk dog', done: true } ]); function toggleDone(id) { todos = todos.map(t => t.id === id ? { ...t, done: !t.done } : t ); } </script>Todo
{#each todos.filter(t => !t.done) as todo (todo.id)}Done
{#each todos.filter(t => t.done) as todo (todo.id)}Bindings
Svelte provides powerful two-way binding capabilities.
Input Bindings:
<script> let name = $state(''); let age = $state(0); let message = $state(''); let selected = $state(''); let checked = $state(false); let group = $state([]); </script>Hello {name}!
Age: {age}
Message length: {message.length}
Selected: {selected}
Checked: {checked}
Apple Banana Orange
Selected: {group.join(', ')}
Component Bindings:
<script> let { value = '' } = $props(); </script> <script> import Input from './Input.svelte'; let name = $state(''); </script>Name: {name}
Element Bindings:
<script> let canvas; let video; let div; let clientWidth = $state(0); let clientHeight = $state(0); let offsetWidth = $state(0); onMount(() => { const ctx = canvas.getContext('2d'); // Draw on canvas }); </script>Contenteditable Bindings:
<script> let html = $state('Edit me!
'); </script>{html}
API Reference Runes (Svelte 5)
$state(initialValue)
Creates reactive state Returns a reactive variable Mutations automatically trigger updates
$derived(expression)
Creates derived reactive value Automatically tracks dependencies Recomputes when dependencies change
$effect(callback)
Runs side effects when dependencies change Can return cleanup function Automatically tracks dependencies
$props()
Declares component props Supports destructuring and defaults Type-safe with TypeScript Store Functions
writable(initialValue, start?)
Creates writable store Returns { subscribe, set, update } Optional start function for setup
readable(initialValue, start)
Creates read-only store Returns { subscribe } Requires start function
derived(stores, callback, initialValue?)
Creates derived store Depends on one or more stores Automatically updates
get(store)
Gets current value without subscription Use sparingly (prefer $store syntax) Lifecycle Functions
onMount(callback)
Runs after component first renders Can return cleanup function Good for data fetching, subscriptions
onDestroy(callback)
Runs before component is destroyed Use for cleanup operations
beforeUpdate(callback)
Runs before DOM updates Access previous state
afterUpdate(callback)
Runs after DOM updates Good for DOM manipulation
tick()
Returns promise that resolves after state changes Ensures DOM is updated Transition Functions
fade(node, params)
Fades element in/out Params: { delay, duration, easing }
fly(node, params)
Flies element in/out Params: { delay, duration, easing, x, y, opacity }
slide(node, params)
Slides element in/out Params: { delay, duration, easing }
scale(node, params)
Scales element in/out Params: { delay, duration, easing, start, opacity }
blur(node, params)
Blurs element in/out Params: { delay, duration, easing, amount, opacity }
crossfade(params)
Creates send/receive transition pair Good for moving elements between lists Animation Functions
flip(node, animation, params)
Animates position changes Use with each blocks Params: { delay, duration, easing } Workflow Patterns Component Composition
Container/Presenter Pattern:
<script> import TodoList from './TodoList.svelte'; import { todos } from './stores.js'; function addTodo(text) { todos.update(list => [...list, { id: Date.now(), text, done: false }]); } function toggleTodo(id) { todos.update(list => list.map(t => t.id === id ? { ...t, done: !t.done } : t )); } function deleteTodo(id) { todos.update(list => list.filter(t => t.id !== id)); } </script>-
{#each todos as todo}
- onToggle(todo.id)} /> {todo.text} {/each}
State Management
Context API Pattern:
<script> import { setContext } from 'svelte'; import { writable } from 'svelte/store'; const user = writable({ name: 'Alice', role: 'admin' }); const theme = writable('light'); setContext('user', user); setContext('theme', theme); </script>Welcome, {$user.name}!
Role: {$user.role}
Form Validation
Form with Validation:
<script> let formData = $state({ email: '', password: '', confirmPassword: '' }); let errors = $state({}); let touched = $state({}); let isSubmitting = $state(false); function validateEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); } function validateForm() { const newErrors = {}; if (!formData.email) { newErrors.email = 'Email is required'; } else if (!validateEmail(formData.email)) { newErrors.email = 'Invalid email address'; } if (!formData.password) { newErrors.password = 'Password is required'; } else if (formData.password.length < 8) { newErrors.password = 'Password must be at least 8 characters'; } if (formData.password !== formData.confirmPassword) { newErrors.confirmPassword = 'Passwords do not match'; } return newErrors; } function handleBlur(field) { touched[field] = true; errors = validateForm(); } async function handleSubmit() { touched = { email: true, password: true, confirmPassword: true }; errors = validateForm(); if (Object.keys(errors).length === 0) { isSubmitting = true; try { await submitForm(formData); // Success } catch (error) { errors.submit = error.message; } finally { isSubmitting = false; } } } </script> <style> .field { margin-bottom: 1rem; } input.error { border-color: red; } .error-message { color: red; font-size: 0.875rem; margin-top: 0.25rem; } </style>Data Fetching
Fetch with Loading States:
<script> import { onMount } from 'svelte'; let data = $state([]); let loading = $state(true); let error = $state(null); async function fetchData() { loading = true; error = null; try { const response = await fetch('/api/data'); if (!response.ok) throw new Error('Failed to fetch'); data = await response.json(); } catch (err) { error = err.message; } finally { loading = false; } } onMount(fetchData); </script>{#if loading}
{:else if error}
Error: {error}
{:else}
-
{#each data as item}
- {item.name} {/each}
{/if}
Best Practices 1. Use Runes for Reactivity (Svelte 5)
Prefer runes over legacy reactive declarations:
<script> let count = $state(0); let doubled = $derived(count * 2); $effect(() => { console.log(`Count: ${count}`); }); </script> <script> let count = 0; $: doubled = count * 2; $: { console.log(`Count: ${count}`); } </script>- Component Organization
Keep components focused and single-purpose:
<script> let { variant = 'primary', onClick } = $props(); </script> <script> // Button that also handles data fetching, validation, etc. </script>- Store Usage
Use stores for shared state, local state for component-specific data:
<script> import { user } from './stores.js'; // Shared state let localCount = $state(0); // Component-specific </script> <script> import { count } from './stores.js'; // Only used in one component </script>- Accessibility
Always include proper ARIA attributes and keyboard support:
<button on:click={handleClick} aria-label="Close dialog" aria-pressed={isPressed}
Close
Enter keywords to search
- Performance Optimization
Use keyed each blocks for lists:
{#each items as item (item.id)}
{#each items as item}
- TypeScript Integration
Use TypeScript for type safety:
<script lang="ts"> interface User { name: string; age: number; email?: string; } interface Props { user: User; onUpdate?: (user: User) => void; } let { user, onUpdate }: Props = $props(); </script>- CSS Scoping
Leverage Svelte's scoped styles:
<style> /* Scoped to this component by default */ .container { padding: 1rem; } /* Global styles when needed */ :global(body) { margin: 0; } /* Combining scoped and global */ .container :global(p) { color: blue; } </style>- Event Modifiers
Use event modifiers for cleaner code: