Research-mind toolchains-javascript-frameworks-svelte
Svelte 5 - Compiler-First Reactive Framework
install
source · Clone the upstream repo
git clone https://github.com/MacPhobos/research-mind
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-javascript-frameworks-svelte" ~/.claude/skills/macphobos-research-mind-toolchains-javascript-frameworks-svelte && rm -rf "$T"
manifest:
.claude/skills/toolchains-javascript-frameworks-svelte/skill.mdsource content
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<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} > <ul> <li>Item 1</li> <li>Item 2</li> </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 |
|---|---|---|
| | Direct state |
| | Auto-tracked |
| | Auto deps |
| | Destructure |
| | Explicit |
Vue 3 to Svelte 5
| Vue 3 | Svelte 5 | Notes |
|---|---|---|
| | Direct state |
| | Similar |
| | Auto-tracked |
| | Type-safe |
| | 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:
: Auto-detect platformadapter-auto
: Vercel deploymentadapter-vercel
: Node.js serveradapter-node
: Static site generationadapter-static
: Cloudflare Pages/Workersadapter-cloudflare
: Netlify deploymentadapter-netlify
Resources
- Svelte Docs: https://svelte.dev/docs
- SvelteKit Docs: https://kit.svelte.dev/docs
- Svelte 5 Runes: https://svelte-5-preview.vercel.app/docs/runes
- Tutorial: https://learn.svelte.dev
- REPL: https://svelte.dev/repl
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