--- name: svelte-development description: Svelte 5 development with runes ($state, $derived, $effect), SvelteKit full-stack framework, and modern reactive patterns. Use when building Svelte applications, implementing fine-grained reactivity, or working with SvelteKit routing and server functions. --- # Svelte 5 Development Comprehensive guide for building modern Svelte applications with runes and SvelteKit. ## Stack Overview | Tool | Purpose | Version | | ---------- | -------------------- | ------- | | Svelte 5 | Core framework | 5.0+ | | SvelteKit | Full-stack framework | 2.0+ | | TypeScript | Type safety | 5.0+ | | Vite | Build tool | 5.0+ | | Vitest | Testing | 1.0+ | --- ## Svelte 5 Runes (Core Reactivity) ### The Rune System Runes are compile-time macros prefixed with `$` that provide explicit, fine-grained reactivity. | Rune | Purpose | Replaces | | ----------- | --------------------- | --------------------------- | | `$state` | Reactive state | `let` declarations | | `$derived` | Computed values | `$:` reactive statements | | `$effect` | Side effects | `$:` side effect statements | | `$props` | Component props | `export let` | | `$bindable` | Two-way binding props | `export let` with bind: | ### Basic Component Structure ```svelte

{title}

Count: {localCount} (Doubled: {doubled})

``` ### State Patterns ```typescript // Primitives let count = $state(0); let name = $state(""); // Objects (deeply reactive) let user = $state({ name: "John", email: "john@example.com", preferences: { theme: "dark" }, }); // Arrays let items = $state([]); // Direct mutation works! user.name = "Jane"; // Reactive user.preferences.theme = "light"; // Reactive (deep) items.push({ id: 1, name: "New" }); // Reactive // Frozen state (shallow reactivity) let frozenList = $state.frozen([1, 2, 3]); ``` ### Derived Values ```typescript // Simple derived let doubled = $derived(count * 2); // Complex derived (use $derived.by for multi-line) let stats = $derived.by(() => { const total = items.reduce((sum, i) => sum + i.value, 0); const average = items.length ? total / items.length : 0; return { total, average, count: items.length }; }); // Derived from multiple sources let summary = $derived(`${user.name} has ${items.length} items`); ``` ### Effects ```typescript // Basic effect (runs when dependencies change) $effect(() => { console.log(`Count is now: ${count}`); }); // Effect with cleanup $effect(() => { const interval = setInterval(() => { count++; }, 1000); // Cleanup function (returned) return () => clearInterval(interval); }); // Pre-effect (runs before DOM updates) $effect.pre(() => { console.log("About to update DOM"); }); // Root effect (doesn't track dependencies) $effect.root(() => { // Manual dependency management }); ``` --- ## Component Patterns ### Props with Defaults and Spreading ```svelte ``` ### Two-Way Binding with $bindable ```svelte

Hello, {name}!

``` ### Snippets (Replacing Slots) ```svelte
{title}
{@render children()}
{#if footer}
{@render footer()}
{/if}

Card content goes here

{#snippet footer()} {/snippet}
``` ### Snippets with Parameters ```svelte {#if items.length === 0} {#if empty} {@render empty()} {:else}

No items

{/if} {:else} {/if} {#snippet row(user, i)} {i + 1}. {user.name} {/snippet} {#snippet empty()}

No users found

{/snippet}
``` --- ## Shared State with .svelte.ts Files ### Creating Shared State ```typescript // lib/stores/counter.svelte.ts export function createCounter(initial = 0) { let count = $state(initial); return { get count() { return count; }, increment() { count++; }, decrement() { count--; }, reset() { count = initial; }, }; } // Singleton instance export const counter = createCounter(); ``` ### Class-Based State ```typescript // lib/stores/user.svelte.ts export class UserStore { user = $state(null); loading = $state(false); error = $state(null); isLoggedIn = $derived(!!this.user); async login(email: string, password: string) { this.loading = true; this.error = null; try { const response = await fetch("/api/auth/login", { method: "POST", body: JSON.stringify({ email, password }), headers: { "Content-Type": "application/json" }, }); if (!response.ok) throw new Error("Login failed"); this.user = await response.json(); } catch (e) { this.error = e instanceof Error ? e.message : "Unknown error"; throw e; } finally { this.loading = false; } } logout() { this.user = null; } } export const userStore = new UserStore(); ``` ### Using Shared State ```svelte

Count: {counter.count}

{#if userStore.isLoggedIn}

Welcome, {userStore.user?.name}

{:else} {/if} ``` --- ## SvelteKit (Full-Stack) ### Project Structure ``` sveltekit-app/ ├── src/ │ ├── routes/ # File-based routing │ │ ├── +page.svelte # / │ │ ├── +page.server.ts # Server load function │ │ ├── +layout.svelte # Root layout │ │ ├── about/ │ │ │ └── +page.svelte # /about │ │ ├── users/ │ │ │ ├── +page.svelte # /users │ │ │ └── [id]/ │ │ │ ├── +page.svelte # /users/:id │ │ │ └── +page.server.ts │ │ └── api/ │ │ └── users/ │ │ └── +server.ts # /api/users │ ├── lib/ # $lib alias │ │ ├── components/ │ │ └── stores/ │ └── app.html ├── static/ └── svelte.config.js ``` ### Load Functions ```typescript // routes/users/+page.server.ts import type { PageServerLoad } from './$types' import { error } from '@sveltejs/kit' export const load: PageServerLoad = async ({ fetch, params }) => { const response = await fetch('/api/users') if (!response.ok) { throw error(response.status, 'Failed to load users') } const users = await response.json() return { users } } // routes/users/+page.svelte

Users

{#each data.users as user}

{user.name}

{/each} ``` ### Form Actions ```typescript // routes/login/+page.server.ts import type { Actions } from "./$types"; import { fail, redirect } from "@sveltejs/kit"; export const actions: Actions = { default: async ({ request, cookies }) => { const formData = await request.formData(); const email = formData.get("email") as string; const password = formData.get("password") as string; // Validation if (!email || !password) { return fail(400, { email, missing: true }); } // Authentication const user = await authenticate(email, password); if (!user) { return fail(401, { email, incorrect: true }); } // Set session cookie cookies.set("session", user.token, { path: "/" }); throw redirect(303, "/dashboard"); }, }; ``` ```svelte
{#if form?.missing}

All fields are required

{/if} {#if form?.incorrect}

Invalid credentials

{/if}
``` ### API Routes ```typescript // routes/api/users/+server.ts import type { RequestHandler } from "./$types"; import { json, error } from "@sveltejs/kit"; export const GET: RequestHandler = async ({ url }) => { const limit = Number(url.searchParams.get("limit")) || 10; const users = await prisma.user.findMany({ take: limit }); return json(users); }; export const POST: RequestHandler = async ({ request }) => { const body = await request.json(); if (!body.email || !body.name) { throw error(400, "Missing required fields"); } const user = await prisma.user.create({ data: body }); return json(user, { status: 201 }); }; // routes/api/users/[id]/+server.ts export const GET: RequestHandler = async ({ params }) => { const user = await prisma.user.findUnique({ where: { id: params.id }, }); if (!user) { throw error(404, "User not found"); } return json(user); }; ``` ### Middleware (Hooks) ```typescript // src/hooks.server.ts import type { Handle } from "@sveltejs/kit"; export const handle: Handle = async ({ event, resolve }) => { // Get session from cookie const session = event.cookies.get("session"); if (session) { const user = await validateSession(session); event.locals.user = user; } // Protected routes if (event.url.pathname.startsWith("/dashboard") && !event.locals.user) { return new Response("Redirect", { status: 303, headers: { Location: "/login" }, }); } return resolve(event); }; ``` --- ## Event Handling (Svelte 5) ### DOM Events (New Syntax) ```svelte name = e.currentTarget.value} onkeydown={(e) => e.key === 'Enter' && submit()} /> ``` ### Component Events (Callback Props) ```svelte console.log('Selected:', id)} onClose={() => console.log('Closed')} /> ``` --- ## Testing with Vitest ### Setup ```typescript // vitest.config.ts import { defineConfig } from "vitest/config"; import { svelte } from "@sveltejs/vite-plugin-svelte"; export default defineConfig({ plugins: [svelte({ hot: !process.env.VITEST })], test: { globals: true, environment: "jsdom", include: ["src/**/*.{test,spec}.{js,ts}"], }, }); ``` ### Component Testing ```typescript // tests/Counter.test.ts import { describe, it, expect } from "vitest"; import { render, screen, fireEvent } from "@testing-library/svelte"; import Counter from "$lib/components/Counter.svelte"; describe("Counter", () => { it("renders initial count", () => { render(Counter, { props: { initial: 5 } }); expect(screen.getByText("Count: 5")).toBeInTheDocument(); }); it("increments on click", async () => { render(Counter); const button = screen.getByRole("button", { name: /increment/i }); await fireEvent.click(button); expect(screen.getByText("Count: 1")).toBeInTheDocument(); }); }); ``` ### Testing Stores ```typescript // tests/stores/counter.test.ts import { describe, it, expect } from "vitest"; import { counter } from "$lib/stores/counter.svelte"; describe("counter store", () => { it("increments count", () => { counter.reset(); expect(counter.count).toBe(0); counter.increment(); expect(counter.count).toBe(1); counter.increment(); expect(counter.count).toBe(2); }); it("decrements count", () => { counter.reset(); counter.decrement(); expect(counter.count).toBe(-1); }); }); ``` --- ## Migration from Svelte 4 | Svelte 4 | Svelte 5 | | -------------------------- | ----------------------------------- | | `let count = 0` (reactive) | `let count = $state(0)` | | `$: doubled = count * 2` | `let doubled = $derived(count * 2)` | | `$: console.log(count)` | `$effect(() => console.log(count))` | | `export let value` | `let { value } = $props()` | | `on:click={handler}` | `onclick={handler}` | | `` | `{@render children()}` | | `createEventDispatcher()` | Callback props | ### Migration Script ```bash npx sv migrate svelte-5 ``` --- ## Anti-Patterns to Avoid | Anti-Pattern | Problem | Solution | | ---------------------------------- | ---------------------- | --------------------------- | | Using `$effect` for derived values | Unnecessary complexity | Use `$derived` | | Exporting `$state` directly | Breaks reactivity | Export getter/setter object | | Not using TypeScript | Missing type safety | Enable `lang="ts"` | | Using old slot syntax | Deprecated in Svelte 5 | Use snippets | | Using `on:event` syntax | Deprecated in Svelte 5 | Use `onevent` props | --- ## Performance Benefits | Metric | Improvement | | ------------------- | ----------------------------- | | Bundle size | 40-60% smaller than React/Vue | | Runtime performance | No virtual DOM overhead | | Time to interactive | Minimal JavaScript hydration | | Memory usage | Lower due to compiled output | --- ## Related Resources - [Svelte 5 Documentation](https://svelte.dev/docs) - [SvelteKit Documentation](https://svelte.dev/docs/kit) - [Svelte 5 Migration Guide](https://svelte.dev/docs/svelte/v5-migration-guide) - [Svelte Testing Docs](https://svelte.dev/docs/svelte/testing) - [Joy of Code Tutorials](https://joyofcode.xyz/) --- ## When to Use This Skill - Building new Svelte 5 applications with runes - Migrating from Svelte 4 to Svelte 5 - Full-stack development with SvelteKit - Creating reactive shared state - Implementing form actions and API routes - Testing Svelte components with Vitest