svelte

Svelte 5 - Compiler-First Reactive Framework

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "svelte" with this command: npx skills add bobmatnyc/claude-mpm-skills/bobmatnyc-claude-mpm-skills-svelte

Svelte 5 - Compiler-First Reactive Framework

Overview

Svelte is a compiler-based reactive UI framework that shifts work from runtime to build time. Unlike React/Vue, Svelte compiles components to highly optimized vanilla JavaScript with minimal overhead. Svelte 5 introduces Runes API for explicit, fine-grained reactivity.

Key Features:

  • Runes API: $state, $derived, $effect for explicit reactivity

  • Zero runtime overhead: Compiles to vanilla JS

  • Built-in state management: No external libraries needed

  • SvelteKit: Full-stack framework with SSR/SSG/SPA

  • Write less code: Simple, readable component syntax

  • Exceptional performance: Small bundles, fast runtime

Installation:

Create new SvelteKit project

npm create svelte@latest my-app cd my-app npm install npm run dev

Or Svelte only (no SvelteKit)

npm create vite@latest my-app -- --template svelte-ts

Svelte 5 Runes API (Modern Approach)

State Management with $state

<script lang="ts"> // Reactive state - automatically tracks changes let count = $state(0); let user = $state({ name: 'Alice', age: 30 });

// Arrays and objects are deeply reactive let todos = $state<Todo[]>([]);

function addTodo(text: string) { todos.push({ id: Date.now(), text, done: false }); // No need for todos = [...todos] like React! }

function increment() { count++; // Triggers reactivity } </script>

<button onclick={increment}> Clicked {count} times </button>

Computed Values with $derived

<script lang="ts"> let firstName = $state('John'); let lastName = $state('Doe');

// Automatically updates when dependencies change let fullName = $derived(${firstName} ${lastName}); let greeting = $derived(Hello, ${fullName});

// Complex derivations let items = $state([1, 2, 3, 4, 5]); let total = $derived(items.reduce((sum, n) => sum + n, 0)); let average = $derived(total / items.length); </script>

<p>{greeting}</p> <p>Average: {average.toFixed(2)}</p>

Side Effects with $effect

<script lang="ts"> let count = $state(0); let logs = $state<string[]>([]);

// Runs when dependencies change $effect(() => { console.log(Count is now: ${count}); logs.push(Count changed to ${count});

// Optional cleanup function
return () => {
  console.log('Cleaning up previous effect');
};

});

// Effect with external subscriptions $effect(() => { const interval = setInterval(() => { count++; }, 1000);

return () => clearInterval(interval);

}); </script>

Component Props with $props

<script lang="ts"> interface Props { title: string; count?: number; onUpdate?: (value: number) => void; }

// Type-safe props with defaults let { title, count = 0, onUpdate } = $props<Props>();

// Props are read-only, but can derive from them let displayTitle = $derived(title.toUpperCase()); </script>

<h1>{displayTitle}</h1> <p>Count: {count}</p> <button onclick={() => onUpdate?.(count + 1)}> Increment </button>

Two-Way Binding with $bindable

<!-- Child: SearchInput.svelte --> <script lang="ts"> interface Props { value: string; placeholder?: string; }

// Allows parent to bind to this prop let { value = $bindable(''), placeholder = 'Search...' } = $props<Props>(); </script>

<input bind:value {placeholder} type="search" />

<!-- Parent.svelte --> <script lang="ts"> import SearchInput from './SearchInput.svelte';

let query = $state(''); let results = $derived(query ? search(query) : []); </script>

<SearchInput bind:value={query} /> <p>Found {results.length} results for "{query}"</p>

Component Patterns

Basic Component Structure

<!-- Counter.svelte --> <script lang="ts"> let count = $state(0); let doubled = $derived(count * 2);

function increment() { count++; }

function decrement() { count--; } </script>

<div class="counter"> <button onclick={decrement}>-</button> <span>Count: {count} (Doubled: {doubled})</span> <button onclick={increment}>+</button> </div>

<style> .counter { display: flex; gap: 1rem; align-items: center; }

button { padding: 0.5rem 1rem; font-size: 1.2rem; } </style>

Conditional Rendering

<script lang="ts"> let loggedIn = $state(false); let user = $state<User | null>(null); let loading = $state(true); </script>

{#if loading} <p>Loading...</p> {:else if loggedIn && user} <p>Welcome, {user.name}!</p> {:else} <p>Please log in</p> {/if}

Lists and Keyed Each Blocks

<script lang="ts"> interface Todo { id: number; text: string; done: boolean; }

let todos = $state<Todo[]>([ { id: 1, text: 'Learn Svelte', done: true }, { id: 2, text: 'Build app', done: false } ]);

function toggle(id: number) { const todo = todos.find(t => t.id === id); if (todo) todo.done = !todo.done; } </script>

<ul> {#each todos as todo (todo.id)} <li class:done={todo.done}> <input type="checkbox" checked={todo.done} onchange={() => toggle(todo.id)} /> {todo.text} </li> {/each} </ul>

<style> .done { text-decoration: line-through; opacity: 0.6; } </style>

SvelteKit Framework

Project Structure

my-app/ ├── src/ │ ├── routes/ │ │ ├── +page.svelte # Home page │ │ ├── +page.ts # Universal load │ │ ├── +page.server.ts # Server load │ │ ├── +layout.svelte # Shared layout │ │ ├── about/ │ │ │ └── +page.svelte # /about │ │ └── blog/ │ │ ├── +page.svelte # /blog │ │ └── [slug]/ │ │ └── +page.svelte # /blog/my-post │ ├── lib/ │ │ ├── components/ │ │ ├── stores/ │ │ └── utils/ │ └── app.html ├── static/ # Static assets └── svelte.config.js

Load Functions (Data Fetching)

// src/routes/blog/[slug]/+page.ts import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params, fetch }) => { const response = await fetch(/api/posts/${params.slug}); const post = await response.json();

return { post }; };

<!-- src/routes/blog/[slug]/+page.svelte --> <script lang="ts"> import type { PageData } from './$types';

let { data } = $props<{ data: PageData }>(); </script>

<article> <h1>{data.post.title}</h1> <div>{@html data.post.content}</div> </article>

Server-Only Load Functions

// src/routes/admin/+page.server.ts import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => { if (!locals.user?.isAdmin) { throw redirect(303, '/login'); }

const users = await db.users.findMany();

return { users }; };

Form Actions

// src/routes/login/+page.server.ts import { fail, redirect } from '@sveltejs/kit'; import type { Actions } from './$types'; import { z } from 'zod';

const schema = z.object({ email: z.string().email(), password: z.string().min(8) });

export const actions = { default: async ({ request, cookies }) => { const formData = await request.formData(); const data = Object.fromEntries(formData);

const result = schema.safeParse(data);
if (!result.success) {
  return fail(400, {
    errors: result.error.flatten().fieldErrors
  });
}

const user = await authenticateUser(result.data);
if (!user) {
  return fail(401, { message: 'Invalid credentials' });
}

cookies.set('session', user.sessionToken, { path: '/' });
throw redirect(303, '/dashboard');

} } satisfies Actions;

<!-- src/routes/login/+page.svelte --> <script lang="ts"> import type { ActionData } from './$types';

let { form } = $props<{ form?: ActionData }>(); </script>

<form method="POST"> <input name="email" type="email" required /> {#if form?.errors?.email} <span class="error">{form.errors.email[0]}</span> {/if}

<input name="password" type="password" required /> {#if form?.errors?.password} <span class="error">{form.errors.password[0]}</span> {/if}

{#if form?.message} <p class="error">{form.message}</p> {/if}

<button type="submit">Log In</button> </form>

State Management Patterns

Runes-Based Store (Modern)

// src/lib/stores/cart.svelte.ts interface CartItem { id: number; name: string; price: number; quantity: number; }

function createCart() { let items = $state<CartItem[]>([]);

let total = $derived( items.reduce((sum, item) => sum + item.price * item.quantity, 0) );

let itemCount = $derived( items.reduce((sum, item) => sum + item.quantity, 0) );

return { get items() { return items; }, get total() { return total; }, get itemCount() { return itemCount; },

addItem(item: Omit&#x3C;CartItem, 'quantity'>) {
  const existing = items.find(i => i.id === item.id);
  if (existing) {
    existing.quantity++;
  } else {
    items.push({ ...item, quantity: 1 });
  }
},

removeItem(id: number) {
  items = items.filter(i => i.id !== id);
},

clear() {
  items = [];
}

}; }

export const cart = createCart();

<!-- Usage --> <script lang="ts"> import { cart } from '$lib/stores/cart.svelte'; </script>

<div> <p>Items: {cart.itemCount}</p> <p>Total: ${cart.total.toFixed(2)}</p>

<button onclick={() => cart.addItem({ id: 1, name: 'Widget', price: 9.99 })}> Add Widget </button> </div>

Legacy Svelte Store (Svelte 4 Style)

// src/lib/stores/user.ts import { writable, derived } from 'svelte/store';

interface User { id: number; name: string; email: string; }

function createUserStore() { const { subscribe, set, update } = writable<User | null>(null);

return { subscribe, login: (user: User) => set(user), logout: () => set(null), updateName: (name: string) => update(u => u ? { ...u, name } : null) }; }

export const user = createUserStore();

// Derived store export const isLoggedIn = derived(user, $user => $user !== null);

<!-- Usage with $ prefix --> <script lang="ts"> import { user, isLoggedIn } from '$lib/stores/user'; </script>

{#if $isLoggedIn} <p>Welcome, {$user?.name}!</p> {:else} <p>Please log in</p> {/if}

Animations and Transitions

Built-in Transitions

<script lang="ts"> import { fade, slide, fly, scale } from 'svelte/transition'; import { quintOut } from 'svelte/easing';

let visible = $state(true); </script>

<button onclick={() => visible = !visible}> Toggle </button>

{#if visible} <div transition:fade={{ duration: 300 }}> Fades in and out </div>

<div transition:slide={{ duration: 300 }}> Slides in and out </div>

<div transition:fly={{ y: 200, duration: 500, easing: quintOut }}> Flies in from bottom </div> {/if}

Custom Transitions

// src/lib/transitions.ts export function blur(node: HTMLElement, { duration = 300 }) { return { duration, css: (t: number) => opacity: ${t}; filter: blur(${(1 - t) * 10}px); }; }

<script lang="ts"> import { blur } from '$lib/transitions'; let show = $state(true); </script>

{#if show} <div transition:blur={{ duration: 500 }}> Custom blur transition </div> {/if}

Animate Directive (FLIP Animations)

<script lang="ts"> import { flip } from 'svelte/animate'; import { quintOut } from 'svelte/easing';

let items = $state([1, 2, 3, 4, 5]);

function shuffle() { items = items.sort(() => Math.random() - 0.5); } </script>

<button onclick={shuffle}>Shuffle</button>

<ul> {#each items as item (item)} <li animate:flip={{ duration: 500, easing: quintOut }}> {item} </li> {/each} </ul>

Advanced Features

Actions (Element Behaviors)

// src/lib/actions.ts export function clickOutside(node: HTMLElement, callback: () => void) { const handleClick = (event: MouseEvent) => { if (!node.contains(event.target as Node)) { callback(); } };

document.addEventListener('click', handleClick, true);

return { destroy() { document.removeEventListener('click', handleClick, true); } }; }

<script lang="ts"> import { clickOutside } from '$lib/actions';

let showMenu = $state(false); </script>

<button onclick={() => showMenu = !showMenu}> Menu </button>

{#if showMenu} <div class="menu" use:clickOutside={() => showMenu = false}

&#x3C;ul>
  &#x3C;li>Item 1&#x3C;/li>
  &#x3C;li>Item 2&#x3C;/li>
&#x3C;/ul>

</div> {/if}

Slots and Component Composition

<!-- Card.svelte --> <script lang="ts"> let { title } = $props<{ title?: string }>(); </script>

<div class="card"> <header> {#if title} <h2>{title}</h2> {:else} <slot name="header">Default Header</slot> {/if} </header>

<main> <slot>Default content</slot> </main>

<footer> <slot name="footer" /> </footer> </div>

<!-- Usage --> <Card title="My Card"> <p>This is the main content</p>

<div slot="footer"> <button>Action</button> </div> </Card>

Special Elements

<script lang="ts"> let Component = $state<typeof import('./A.svelte').default | typeof import('./B.svelte').default>(A); let tag = $state<'div' | 'span'>('div'); </script>

<!-- Dynamic component --> <svelte:component this={Component} />

<!-- Dynamic HTML element --> <svelte:element this={tag}> Content </svelte:element>

<!-- Window events --> <svelte:window onresize={() => console.log('Window resized')} onkeydown={(e) => console.log(e.key)} />

<!-- Document head --> <svelte:head> <title>My Page</title> <meta name="description" content="Description" /> </svelte:head>

Migration from React/Vue

React to Svelte 5

React Svelte 5 Notes

useState(0)

$state(0)

Direct state

useMemo(() => x * 2, [x])

$derived(x * 2)

Auto-tracked

useEffect(() => {...}, [x])

$effect(() => {...})

Auto deps

props.name

let { name } = $props()

Destructure

{show && <div />}

{#if show}<div />{/if}

Explicit

Vue 3 to Svelte 5

Vue 3 Svelte 5 Notes

ref(0)

$state(0)

Direct state

computed(() => x * 2)

$derived(x * 2)

Similar

watch(() => x, ...)

$effect(() => {...})

Auto-tracked

defineProps<{ name: string }>()

let { name } = $props<Props>()

Type-safe

v-if="show"

{#if show}

Block syntax

Performance Best Practices

  • Use $derived over $effect when only computed values are needed

  • Avoid unnecessary $effect - only for side effects, not computations

  • Leverage compiler optimizations - Svelte does most work at build time

  • Use keyed each blocks - {#each items as item (item.id)}

  • Lazy load components - const Component = await import('./Heavy.svelte')

  • SSR for initial load - Use SvelteKit's SSR capabilities

  • Optimize bundles - Use adapters for deployment targets

Testing

Unit Tests with Vitest

// Counter.test.ts import { render, fireEvent } from '@testing-library/svelte'; import { expect, test } from 'vitest'; import Counter from './Counter.svelte';

test('increments counter on click', async () => { const { getByText } = render(Counter);

const button = getByText('+'); await fireEvent.click(button);

expect(getByText(/Count: 1/)).toBeInTheDocument(); });

E2E with Playwright

// tests/login.test.ts import { expect, test } from '@playwright/test';

test('user can log in', async ({ page }) => { await page.goto('/login');

await page.fill('input[name="email"]', 'user@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]');

await expect(page).toHaveURL('/dashboard'); await expect(page.locator('text=Welcome')).toBeVisible(); });

Deployment

Adapters

// svelte.config.js import adapter from '@sveltejs/adapter-vercel'; // or adapter-node, adapter-static

export default { kit: { adapter: adapter() } };

Available Adapters:

  • adapter-auto : Auto-detect platform

  • adapter-vercel : Vercel deployment

  • adapter-node : Node.js server

  • adapter-static : Static site generation

  • adapter-cloudflare : Cloudflare Pages/Workers

  • adapter-netlify : Netlify deployment

Resources

Summary

  • Svelte 5 uses Runes API ($state, $derived, $effect) for explicit reactivity

  • Compiler-first - shifts work to build time for minimal runtime overhead

  • SvelteKit provides full-stack capabilities with SSR/SSG/SPA modes

  • Write less code - simpler syntax than React/Vue

  • Exceptional performance - small bundles, fast runtime

  • Migration-friendly - Svelte 4 and 5 can coexist

  • Type-safe - First-class TypeScript support

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pydantic

No summary provided by upstream source.

Repository SourceNeeds Review
General

playwright-e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwind-css

No summary provided by upstream source.

Repository SourceNeeds Review