Svelte/SvelteKit
General Principles
Section titled “General Principles”Write reactive, performant Svelte applications that leverage the framework’s compiler-based approach and SvelteKit’s full-stack capabilities.
Component Structure
Section titled “Component Structure”Component Organization
Section titled “Component Organization”Structure components with clear sections:
<script lang="ts"> // 1. Imports import { onMount } from 'svelte'; import Button from '$lib/components/Button.svelte';
// 2. Props (with types) interface Props { title: string; count?: number; }
let { title, count = 0 }: Props = $props();
// 3. State let isLoading = $state(false); let items = $state<Item[]>([]);
// 4. Derived state let total = $derived(items.length); let isEmpty = $derived(items.length === 0);
// 5. Functions function handleClick() { count++; }
// 6. Effects $effect(() => { console.log('Count changed:', count); });
// 7. Lifecycle onMount(() => { fetchItems(); });</script>
<!-- 8. Template --><div class="container"> <h1>{title}</h1> <p>Count: {count}</p>
{#if isEmpty} <p>No items</p> {:else} <ul> {#each items as item (item.id)} <li>{item.name}</li> {/each} </ul> {/if}
<Button onclick={handleClick}> Increment </Button></div>
<!-- 9. Styles --><style> .container { padding: 1rem; }
h1 { color: var(--primary); }</style>Runes (Svelte 5)
Section titled “Runes (Svelte 5)”State with $state
Section titled “State with $state”Use $state rune for reactive state:
<script lang="ts"> // Primitive state let count = $state(0);
// Object state let user = $state({ name: 'John', email: 'john@example.com' });
// Array state let items = $state<string[]>([]);
// State with type inference let data = $state<User | null>(null);
function updateUser() { // Mutate directly - no need for assignments user.name = 'Jane'; items.push('new item'); }</script>Derived State with $derived
Section titled “Derived State with $derived”Use $derived for computed values:
<script lang="ts"> let count = $state(0);
// Simple derived let doubled = $derived(count * 2);
// Derived with logic let status = $derived( count === 0 ? 'empty' : count < 10 ? 'low' : 'high' );
// Derived from multiple sources let firstName = $state('John'); let lastName = $state('Doe'); let fullName = $derived(`${firstName} ${lastName}`);</script>Effects with $effect
Section titled “Effects with $effect”Use $effect for side effects:
<script lang="ts"> let count = $state(0);
// Basic effect $effect(() => { console.log('Count changed:', count); });
// Effect with cleanup $effect(() => { const interval = setInterval(() => { count++; }, 1000);
return () => { clearInterval(interval); }; });
// Pre-effect (runs before DOM updates) $effect.pre(() => { console.log('Before update'); });</script>Props with $props
Section titled “Props with $props”Use $props for component props:
<script lang="ts"> interface Props { title: string; subtitle?: string; count?: number; onclick?: () => void; }
// Destructure with defaults let { title, subtitle = 'Default subtitle', count = 0, onclick }: Props = $props();
// Rest props let { class: className, ...rest }: Props = $props();</script>
<div class={className} {...rest}> <h1>{title}</h1> {#if subtitle} <h2>{subtitle}</h2> {/if}</div>Reactivity
Section titled “Reactivity”Event Handlers
Section titled “Event Handlers”Use modern event handler syntax:
<script lang="ts"> function handleClick(event: MouseEvent) { console.log('Clicked', event); }
function handleInput(event: Event) { const target = event.target as HTMLInputElement; console.log(target.value); }</script>
<!-- Modern syntax --><button onclick={handleClick}>Click</button>
<!-- Inline handler --><button onclick={() => console.log('Clicked')}>Click</button>
<!-- With event modifiers --><button onclick|preventDefault|stopPropagation={handleClick}> Click</button>
<input type="text" oninput={handleInput} />Bindings
Section titled “Bindings”Use two-way bindings appropriately:
<script lang="ts"> let value = $state(''); let checked = $state(false); let selected = $state(''); let files = $state<FileList | null>(null);
let element = $state<HTMLDivElement>();</script>
<!-- Input binding --><input type="text" bind:value />
<!-- Checkbox binding --><input type="checkbox" bind:checked />
<!-- Select binding --><select bind:value={selected}> <option>A</option> <option>B</option></select>
<!-- File binding --><input type="file" bind:files />
<!-- Element binding --><div bind:this={element}>Content</div>Control Flow
Section titled “Control Flow”Conditional Rendering
Section titled “Conditional Rendering”Use {#if} blocks with proper structure:
<script lang="ts"> let status = $state<'loading' | 'success' | 'error'>('loading'); let user = $state<User | null>(null);</script>
<!-- Simple condition -->{#if user} <p>Welcome, {user.name}!</p>{/if}
<!-- If-else -->{#if status === 'loading'} <Spinner />{:else if status === 'error'} <Error />{:else} <Content />{/if}
<!-- Await block -->{#await fetchData()} <Spinner />{:then data} <Content {data} />{:catch error} <Error {error} />{/await}Lists and Iterations
Section titled “Lists and Iterations”Use {#each} with keys:
<script lang="ts"> let items = $state([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ]);</script>
<!-- With key -->{#each items as item (item.id)} <div>{item.name}</div>{/each}
<!-- With index -->{#each items as item, i (item.id)} <div>{i}: {item.name}</div>{/each}
<!-- With else -->{#each items as item (item.id)} <div>{item.name}</div>{:else} <p>No items</p>{/each}SvelteKit Routing
Section titled “SvelteKit Routing”File-Based Routing
Section titled “File-Based Routing”Organize routes with clear structure:
src/routes/├── +page.svelte # /├── +layout.svelte # Root layout├── +layout.ts # Layout load├── about/│ └── +page.svelte # /about├── blog/│ ├── +page.svelte # /blog│ ├── +page.ts # Blog page load│ └── [slug]/│ ├── +page.svelte # /blog/:slug│ └── +page.ts # Post page load└── api/ └── posts/ └── +server.ts # /api/postsLoad Functions
Section titled “Load Functions”Use typed load functions:
// +page.ts (universal load)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 };};// +page.server.ts (server-only load)import type { PageServerLoad } from './$types';import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, locals }) => { const post = await db.getPost(params.slug);
if (!post) { throw error(404, 'Post not found'); }
return { post, user: locals.user };};Form Actions
Section titled “Form Actions”Implement progressive enhancement with actions:
import type { Actions, PageServerLoad } from './$types';import { fail, redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async () => { return { posts: await db.getPosts() };};
export const actions: Actions = { create: async ({ request }) => { const data = await request.formData(); const title = data.get('title');
if (!title) { return fail(400, { error: 'Title is required', title }); }
const post = await db.createPost({ title });
throw redirect(303, `/blog/${post.slug}`); },
delete: async ({ request }) => { const data = await request.formData(); const id = data.get('id');
await db.deletePost(id);
return { success: true }; }};<script lang="ts"> import { enhance } from '$app/forms'; import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();</script>
<form method="POST" action="?/create" use:enhance> <input type="text" name="title" value={form?.title ?? ''} />
{#if form?.error} <p class="error">{form.error}</p> {/if}
<button type="submit">Create</button></form>
{#each data.posts as post} <form method="POST" action="?/delete" use:enhance> <input type="hidden" name="id" value={post.id} /> <button type="submit">Delete</button> </form>{/each}State Management
Section titled “State Management”Stores
Section titled “Stores”Use stores for shared state:
import { writable, derived, readonly } from 'svelte/store';
// Writable storeexport const count = writable(0);
// Derived storeexport const doubled = derived(count, $count => $count * 2);
// Custom storefunction 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();<script lang="ts"> import { count, counter } from '$lib/stores/counter';</script>
<!-- Auto-subscribe with $ --><p>Count: {$count}</p><p>Counter: {$counter}</p>
<button onclick={() => counter.increment()}>+</button><button onclick={() => counter.decrement()}>-</button>Context API
Section titled “Context API”Share data with component descendants:
<script lang="ts"> import { setContext } from 'svelte';
const theme = $state({ mode: 'dark' });
setContext('theme', theme);</script>
<slot /><script lang="ts"> import { getContext } from 'svelte';
const theme = getContext<{ mode: string }>('theme');</script>
<div class={theme.mode}> Theme: {theme.mode}</div>Snippets (Svelte 5)
Section titled “Snippets (Svelte 5)”Reusable Template Blocks
Section titled “Reusable Template Blocks”Use snippets for template reuse:
<script lang="ts"> let items = $state([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ]);</script>
{#snippet itemCard(item: Item)} <div class="card"> <h3>{item.name}</h3> <p>ID: {item.id}</p> </div>{/snippet}
{#each items as item (item.id)} {@render itemCard(item)}{/each}Snippet Props
Section titled “Snippet Props”Pass snippets as props:
<script lang="ts"> import type { Snippet } from 'svelte';
interface Props { title: string; children: Snippet; actions?: Snippet; }
let { title, children, actions }: Props = $props();</script>
<div class="card"> <h2>{title}</h2> <div class="content"> {@render children()} </div> {#if actions} <div class="actions"> {@render actions()} </div> {/if}</div><!-- Usage --><Card title="My Card"> {#snippet children()} <p>Card content</p> {/snippet}
{#snippet actions()} <button>Save</button> <button>Cancel</button> {/snippet}</Card>Styling
Section titled “Styling”Component Styles
Section titled “Component Styles”Use scoped styles by default:
<div class="container"> <h1 class="title">Hello</h1></div>
<style> .container { padding: var(--spacing-md); }
.title { color: var(--color-primary); font-size: 2rem; }
/* Target slotted content */ :global(.slotted-class) { margin: 1rem; }</style>Class Directives
Section titled “Class Directives”Use class directives for conditional classes:
<script lang="ts"> let isActive = $state(false); let isPrimary = $state(true);</script>
<!-- class: directive --><button class:active={isActive} class:primary={isPrimary}> Button</button>
<!-- Shorthand when variable matches class name --><div class:loading>Content</div>Style Directives
Section titled “Style Directives”Use style directives for dynamic styles:
<script lang="ts"> let color = $state('#ff0000'); let size = $state(16);</script>
<div style:color style:font-size="{size}px"> Styled content</div>Performance
Section titled “Performance”Lazy Loading
Section titled “Lazy Loading”Lazy load components when needed:
<script lang="ts"> let showHeavyComponent = $state(false);</script>
<button onclick={() => showHeavyComponent = true}> Load Component</button>
{#if showHeavyComponent} {#await import('./HeavyComponent.svelte')} <p>Loading...</p> {:then { default: HeavyComponent }} <HeavyComponent /> {/await}{/if}Virtual Lists
Section titled “Virtual Lists”Use virtual lists for large datasets:
<script lang="ts"> import { VirtualList } from 'svelte-virtual';
let items = $state(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })));</script>
<VirtualList items={items} let:item> <div>{item.name}</div></VirtualList>Best Practices
Section titled “Best Practices”✅ Use runes ($state, $derived, $effect) in Svelte 5
✅ Type all component props with TypeScript
✅ Use {#key} for proper reactivity
✅ Implement progressive enhancement with form actions
✅ Use snippets for template reuse
✅ Keep components small and focused
✅ Use stores for shared state
✅ Leverage SvelteKit’s load functions
✅ Use proper key in {#each} blocks
✅ Implement proper error handling
Don’ts
Section titled “Don’ts”❌ Mutate props directly ❌ Use reactive statements ($:) in Svelte 5 (use runes) ❌ Create deeply nested component trees ❌ Fetch data in components (use load functions) ❌ Skip TypeScript types ❌ Use {#each} without keys ❌ Ignore accessibility ❌ Mix server and client logic incorrectly ❌ Create unnecessary reactivity
Configuration
Section titled “Configuration”SvelteKit Config
Section titled “SvelteKit Config”import adapter from '@sveltejs/adapter-auto';import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default { preprocess: vitePreprocess(), kit: { adapter: adapter(), alias: { $lib: 'src/lib', $components: 'src/lib/components' } }};TypeScript Config
Section titled “TypeScript Config”{ "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "allowJs": true, "checkJs": true }}Testing
Section titled “Testing”Component Testing
Section titled “Component Testing”Test components with Vitest and Testing Library:
import { render, screen } from '@testing-library/svelte';import { expect, test } from 'vitest';import Button from './Button.svelte';
test('Button renders with text', () => { render(Button, { props: { children: 'Click me' } });
expect(screen.getByRole('button')).toHaveTextContent('Click me');});