
Svelte
- 66 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
svelte is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- svelte
- AI & Agent Building
- AI-coding skill
Svelte by the numbers
- 66 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,006 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill svelteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 66 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Svelte
Overview
Svelte is a compiler-based UI framework that shifts work from runtime to build time, producing minimal JavaScript with no virtual DOM. Svelte 5 introduces runes for explicit, fine-grained reactivity. SvelteKit is the full-stack framework built on Svelte, providing file-based routing, server-side rendering, and deployment adapters.
When to use: Full-stack web apps, static sites, progressive enhancement, SSR/SSG, projects needing small bundle sizes, migration from Svelte 4 to 5.
When NOT to use: React/Vue ecosystem lock-in, projects requiring extensive third-party component libraries only available for other frameworks, teams with no Svelte experience on tight deadlines.
Quick Reference
| Pattern | API / Syntax | Key Points |
|---|---|---|
| Reactive state | let count = $state(0) | Replaces let reactivity from Svelte 4 |
| Derived state | const double = $derived(count * 2) | Replaces $: reactive declarations |
| Complex derivation | const value = $derived.by(() => { ... }) | Multi-statement derived computations |
| Side effects | $effect(() => { ... }) | Runs after DOM update, auto-tracks dependencies |
| Component props | let { name, age = 25 } = $props() | Replaces export let, supports defaults |
| Bindable props | let { value = $bindable() } = $props() | Two-way binding with bind:value |
| Debug inspection | $inspect(value) | Dev-only logging, stripped in production |
| Snippets | {#snippet name(params)}...{/snippet} | Replaces slots, reusable template blocks |
| Render snippet | {@render name(args)} | Invoke a snippet with arguments |
| Event handling | <button onclick={handler}> | Properties replace on:click directive |
| Each blocks | {#each items as item (item.id)}...{/each} | Keyed iteration for efficient updates |
| Await blocks | {#await promise}...{:then}...{:catch}... | Inline async rendering |
| Server load | export function load({ params }) in +page.server.ts | Runs server-side only, accesses DB/secrets |
| Universal load | export function load({ fetch }) in +page.ts | Runs on server and client |
| Form actions | export const actions in +page.server.ts | Progressive enhancement with use:enhance |
| Layout | +layout.svelte / +layout.server.ts | Shared UI and data across child routes |
| Server hooks | handle() in src/hooks.server.ts | Request middleware, auth, redirects |
| Error page | +error.svelte | Per-route error boundaries |
| Adapters | adapter-auto, adapter-node, adapter-static | Deploy to Vercel, Node, static hosting |
| API routes | +server.ts with GET, POST, etc. | Standalone endpoints, not tied to pages |
| Page options | export const prerender = true | Per-route SSR, CSR, prerender control |
| Shared state | $state() in .svelte.ts modules | Replaces writable stores for cross-component state |
| Raw state | $state.raw(data) | Opts out of deep proxying for large datasets |
Svelte 4 to 5 Migration
| Svelte 4 (Legacy) | Svelte 5 (Current) |
|---|---|
let count = 0 (reactive) | let count = $state(0) |
$: double = count * 2 | const double = $derived(count * 2) |
$: { sideEffect() } | $effect(() => { sideEffect() }) |
export let name | let { name } = $props() |
<slot /> | {#snippet children()}{/snippet} + {@render} |
on:click={handler} | onclick={handler} |
createEventDispatcher() | Callback props: let { onclick } = $props() |
import { writable } from 'svelte/store' | $state() in .svelte.ts modules |
$store auto-subscription | Direct value access from rune-based state |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using $state on non-primitives without care | $state deeply proxies objects; use $state.raw() for large read-only data |
Destructuring $props() loses reactivity | Destructure at declaration only: let { x } = $props() |
Reading $effect dependencies conditionally | Ensure all tracked reads happen unconditionally |
Returning cleanup from $effect incorrectly | Return a function: $effect(() => { return () => cleanup() }) |
Mixing on:click and onclick in Svelte 5 | Use onclick exclusively in Svelte 5 components |
| Using stores in new Svelte 5 code | Use $state() in .svelte.ts modules for shared state |
Forgetting (key) in {#each} blocks | Always key: {#each items as item (item.id)} |
Exporting load from .svelte files | Load functions belong in +page.ts or +page.server.ts |
Not awaiting parent load in layouts | Use await parent() when child load depends on layout |
Using goto() in server load functions | Use redirect(303, '/path') from @sveltejs/kit |
Delegation
- Pattern discovery: Use
Exploreagent - Code review: Delegate to
code-revieweragent - Build configuration: Delegate to
Taskagent
If the tailwind skill is available, delegate Tailwind CSS utility class patterns and configuration to it.If the vitest-testing skill is available, delegate Svelte component unit testing patterns to it.If the playwright skill is available, delegate end-to-end testing of SvelteKit routes and form actions to it.If the drizzle-orm skill is available, delegate database schema and query patterns used in SvelteKit server load functions to it.If the vite skill is available, delegate Vite build configuration and plugin setup to it.References
- Runes and reactivity patterns ($state, $derived, $effect, $props)
- Snippets, rendering, and component composition
- SvelteKit routing, load functions, and layouts
- Form actions, progressive enhancement, and validation
- Hooks, middleware, and error handling
- Svelte 4 to 5 migration patterns
- SvelteKit adapters and deployment
Adapters and Deployment
Adapter Overview
SvelteKit adapters transform the build output for specific deployment targets.
| Adapter | Target | SSR | Static | Serverless |
|---|---|---|---|---|
@sveltejs/adapter-auto | Auto-detect (Vercel, etc.) | Yes | Yes | Yes |
@sveltejs/adapter-node | Node.js server | Yes | No | No |
@sveltejs/adapter-static | Static hosting (Netlify, S3) | No | Yes | No |
@sveltejs/adapter-vercel | Vercel | Yes | Yes | Yes |
@sveltejs/adapter-cloudflare | Cloudflare Pages/Workers | Yes | Yes | Yes |
adapter-auto (Default)
Automatically selects the best adapter for the deployment environment. Good for getting started.
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
},
};
export default config;adapter-node
Self-hosted Node.js server. Produces a standalone build/ directory.
npm install -D @sveltejs/adapter-node// svelte.config.js
import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
out: 'build',
precompress: true,
envPrefix: 'APP_',
}),
},
};
export default config;Run the server:
node buildEnvironment variables for adapter-node
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | Server port |
HOST | 0.0.0.0 | Server host |
ORIGIN | (required) | Canonical URL |
BODY_SIZE_LIMIT | 512K | Max request body size |
adapter-static
Fully pre-rendered static site. Every route must be prerenderable.
npm install -D @sveltejs/adapter-static// svelte.config.js
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html',
precompress: false,
strict: true,
}),
},
};
export default config;Enable prerendering for all pages:
// src/routes/+layout.ts
export const prerender = true;SPA mode with adapter-static
For single-page apps, set a fallback page:
// svelte.config.js
adapter: adapter({
fallback: 'index.html',
});// src/routes/+layout.ts
export const ssr = false;adapter-vercel
npm install -D @sveltejs/adapter-vercel// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
runtime: 'nodejs22.x',
regions: ['iad1'],
split: false,
}),
},
};
export default config;Per-route Vercel configuration
// src/routes/api/heavy/+server.ts
export const config = {
runtime: 'nodejs22.x',
maxDuration: 60,
};adapter-cloudflare
npm install -D @sveltejs/adapter-cloudflare// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>'],
},
}),
},
};
export default config;Access Cloudflare platform bindings:
// +page.server.ts
export const load = async ({ platform }) => {
const value = await platform.env.MY_KV.get('key');
return { value };
};Project Setup
Create a new SvelteKit project
npx sv create my-app
cd my-app
npm install
npm run devsvelte.config.js full example
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$components: 'src/lib/components',
$server: 'src/lib/server',
},
csrf: {
checkOrigin: true,
},
env: {
dir: '.',
publicPrefix: 'PUBLIC_',
},
},
};
export default config;Docker deployment with adapter-node
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "build"]Component Patterns
Snippets
Snippets replace slots in Svelte 5. They are reusable template blocks defined with {#snippet} and rendered with {@render}.
Basic snippet
{#snippet greeting(name)}
<p>Hello, {name}!</p>
{/snippet}
{@render greeting('Alice')}
{@render greeting('Bob')}Children snippet (replaces default slot)
Components receive children content as a children snippet prop.
<!-- Card.svelte -->
<script lang="ts">
import { type Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
<div class="card">
{@render children()}
</div><!-- Usage -->
<Card>
<p>This is the card content</p>
</Card>Named snippets (replaces named slots)
<!-- Dialog.svelte -->
<script lang="ts">
import { type Snippet } from 'svelte';
interface Props {
header: Snippet;
children: Snippet;
footer?: Snippet;
}
let { header, children, footer }: Props = $props();
</script>
<div class="dialog">
<header>{@render header()}</header>
<main>{@render children()}</main>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div><!-- Usage -->
<Dialog>
{#snippet header()}
<h2>Confirm Action</h2>
{/snippet}
<p>Are you sure?</p>
{#snippet footer()}
<button onclick={confirm}>Yes</button>
<button onclick={cancel}>No</button>
{/snippet}
</Dialog>Snippets with typed parameters
<script lang="ts">
import { type Snippet } from 'svelte';
interface Props<T> {
items: T[];
row: Snippet<[T, number]>;
empty?: Snippet;
}
let { items, row, empty }: Props<any> = $props();
</script>
{#if items.length === 0}
{#if empty}
{@render empty()}
{:else}
<p>No items</p>
{/if}
{:else}
{#each items as item, index (index)}
{@render row(item, index)}
{/each}
{/if}Each Blocks
Iterate over arrays. Always provide a key expression for efficient DOM updates.
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte 5', done: false },
{ id: 2, text: 'Build an app', done: false }
]);
</script>
{#each todos as todo (todo.id)}
<label>
<input type="checkbox" bind:checked={todo.done} />
<span class:done={todo.done}>{todo.text}</span>
</label>
{:else}
<p>No todos yet</p>
{/each}Destructured each
{#each users as { name, email, avatar } (email)}
<div class="user">
<img src={avatar} alt={name} />
<p>{name} ({email})</p>
</div>
{/each}Await Blocks
Handle promises directly in templates.
<script>
let promise = $state(fetch('/api/data').then((r) => r.json()));
</script>
{#await promise}
<p>Loading...</p>
{:then data}
<pre>{JSON.stringify(data, null, 2)}</pre>
{:catch error}
<p>Error: {error.message}</p>
{/await}Short form (no loading state)
{#await promise then data}
<p>{data.message}</p>
{/await}Conditional Rendering
{#if user.role === 'admin'}
<AdminPanel />
{:else if user.role === 'editor'}
<EditorPanel />
{:else}
<ViewerPanel />
{/if}Event Handling
Svelte 5 uses standard DOM property names. No more on: directive.
<script lang="ts">
let count = $state(0);
function handleClick(event: MouseEvent) {
count++;
}
</script>
<button onclick={handleClick}>Clicked {count} times</button>
<!-- Inline handler -->
<button onclick={() => count++}>Increment</button>
<!-- Multiple handlers via spread -->
<button onclick={(e) => { handleClick(e); logClick(e); }}>Both</button>Callback props (replaces createEventDispatcher)
<!-- ChildComponent.svelte -->
<script lang="ts">
let { onsubmit }: { onsubmit: (data: FormData) => void } = $props();
</script>
<form onsubmit={(e) => {
e.preventDefault();
onsubmit(new FormData(e.currentTarget));
}}>
<input name="email" type="email" />
<button>Submit</button>
</form><!-- Parent -->
<ChildComponent onsubmit={(data) => console.log(data.get('email'))} />Dynamic Components
<script>
import Home from './Home.svelte';
import About from './About.svelte';
const routes = { home: Home, about: About } as const;
let current = $state<keyof typeof routes>('home');
</script>
<svelte:component this={routes[current]} />
<nav>
<button onclick={() => current = 'home'}>Home</button>
<button onclick={() => current = 'about'}>About</button>
</nav>Special Elements
<!-- Reactive document title -->
<svelte:head>
<title>{pageTitle}</title>
<meta name="description" content={description} />
</svelte:head>
<!-- Window events -->
<svelte:window onkeydown={handleKeydown} bind:scrollY={y} />
<!-- Body classes -->
<svelte:body onmouseenter={handleMouseEnter} />
<!-- Self-referencing for recursive components -->
<svelte:self count={count - 1} />Bindings
<script>
let name = $state('');
let agreed = $state(false);
let selected = $state('a');
let textarea = $state('');
let inputEl: HTMLInputElement;
</script>
<input bind:value={name} />
<input type="checkbox" bind:checked={agreed} />
<select bind:value={selected}>
<option value="a">A</option>
<option value="b">B</option>
</select>
<textarea bind:value={textarea}></textarea>
<input bind:this={inputEl} />Form Actions
Basic Form Actions
Form actions handle POST requests in +page.server.ts. They work without JavaScript, providing progressive enhancement.
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email') as string;
const password = data.get('password') as string;
if (!email || !password) {
return fail(400, { email, missing: true });
}
const user = await authenticate(email, password);
if (!user) {
return fail(401, { email, incorrect: true });
}
cookies.set('session', user.token, { path: '/', httpOnly: true });
redirect(303, '/dashboard');
},
} satisfies Actions;<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
</script>
<form method="POST">
<label>
Email
<input name="email" type="email" value={form?.email ?? ''} />
</label>
<label>
Password
<input name="password" type="password" />
</label>
{#if form?.missing}
<p class="error">All fields are required</p>
{/if}
{#if form?.incorrect}
<p class="error">Invalid credentials</p>
{/if}
<button>Log in</button>
</form>Named Actions
Define multiple actions on a single page.
// src/routes/todos/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
create: async ({ request, locals }) => {
const data = await request.formData();
const text = data.get('text') as string;
if (!text?.trim()) {
return fail(400, { text, error: 'Text is required' });
}
await locals.db.todo.create({ data: { text, userId: locals.user.id } });
return { success: true };
},
delete: async ({ request, locals }) => {
const data = await request.formData();
const id = data.get('id') as string;
await locals.db.todo.delete({ where: { id, userId: locals.user.id } });
return { success: true };
},
toggle: async ({ request, locals }) => {
const data = await request.formData();
const id = data.get('id') as string;
const todo = await locals.db.todo.findUnique({ where: { id } });
if (todo) {
await locals.db.todo.update({
where: { id },
data: { done: !todo.done },
});
}
},
} satisfies Actions;<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();
</script>
<form method="POST" action="?/create">
<input name="text" value={form?.text ?? ''} />
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<button>Add</button>
</form>
{#each data.todos as todo (todo.id)}
<div>
<form method="POST" action="?/toggle" style="display:inline">
<input type="hidden" name="id" value={todo.id} />
<button>{todo.done ? 'Undo' : 'Done'}</button>
</form>
<span class:done={todo.done}>{todo.text}</span>
<form method="POST" action="?/delete" style="display:inline">
<input type="hidden" name="id" value={todo.id} />
<button>Delete</button>
</form>
</div>
{/each}Progressive Enhancement with use:enhance
The enhance action upgrades forms to use fetch instead of full page reloads while maintaining the same server-side logic.
<script>
import { enhance } from '$app/forms';
</script>
<!-- Basic: auto-invalidates load functions after submission -->
<form method="POST" action="?/create" use:enhance>
<input name="text" />
<button>Add</button>
</form>Custom enhance behavior
<script>
import { enhance } from '$app/forms';
let submitting = $state(false);
</script>
<form
method="POST"
action="?/create"
use:enhance={() => {
submitting = true;
return async ({ result, update }) => {
submitting = false;
if (result.type === 'success') {
showToast('Created successfully');
}
await update();
};
}}
>
<input name="text" />
<button disabled={submitting}>
{submitting ? 'Saving...' : 'Add'}
</button>
</form>Cancelling default behavior
<form
method="POST"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'redirect') {
goto(result.location);
}
// Omitting update() prevents auto-invalidation
};
}}
>Validation Patterns
Server-side validation with typed errors
// src/routes/register/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email') as string;
const password = data.get('password') as string;
const errors: Record<string, string> = {};
if (!email?.includes('@')) {
errors.email = 'Invalid email address';
}
if (!password || password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (Object.keys(errors).length > 0) {
return fail(400, { errors, email });
}
await createUser(email, password);
return { success: true };
},
} satisfies Actions;File uploads
<form method="POST" enctype="multipart/form-data" use:enhance>
<input type="file" name="avatar" accept="image/*" />
<button>Upload</button>
</form>// +page.server.ts
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const file = data.get('avatar') as File;
if (file.size > 5 * 1024 * 1024) {
return fail(400, { error: 'File too large (max 5MB)' });
}
const buffer = Buffer.from(await file.arrayBuffer());
await saveFile(buffer, file.name);
return { success: true };
},
} satisfies Actions;Hooks and Error Handling
Server Hooks (src/hooks.server.ts)
handle
Intercepts every request. Use for authentication, redirects, response headers, and middleware logic.
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session');
if (session) {
event.locals.user = await getUserFromSession(session);
}
if (event.url.pathname.startsWith('/dashboard') && !event.locals.user) {
redirect(303, '/login');
}
const response = await resolve(event);
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
return response;
};Sequencing multiple handlers
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit';
const auth: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session');
event.locals.user = session ? await getUserFromSession(session) : null;
return resolve(event);
};
const logger: Handle = async ({ event, resolve }) => {
const start = performance.now();
const response = await resolve(event);
const duration = performance.now() - start;
console.log(
`${event.request.method} ${event.url.pathname} ${response.status} ${duration.toFixed(0)}ms`,
);
return response;
};
const security: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
response.headers.set('X-Frame-Options', 'DENY');
return response;
};
export const handle = sequence(auth, logger, security);handleFetch
Intercepts fetch calls made in server load functions. Useful for proxying, adding auth headers, or routing internal requests.
// src/hooks.server.ts
import type { HandleFetch } from '@sveltejs/kit';
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
if (request.url.startsWith('https://api.internal.com')) {
request.headers.set('Authorization', `Bearer ${event.locals.apiToken}`);
}
return fetch(request);
};handleError
Catches unexpected errors. Use for logging, error reporting (e.g., Sentry), and shaping the error response.
// src/hooks.server.ts
import type { HandleServerError } from '@sveltejs/kit';
export const handleError: HandleServerError = async ({
error,
event,
status,
message,
}) => {
const errorId = crypto.randomUUID();
console.error(
`[${errorId}] ${event.request.method} ${event.url.pathname}`,
error,
);
return {
message: 'An unexpected error occurred',
errorId,
};
};Client Hooks (src/hooks.client.ts)
handleError (client-side)
Catches unhandled errors on the client.
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
export const handleError: HandleClientError = async ({
error,
event,
status,
message,
}) => {
const errorId = crypto.randomUUID();
console.error(`[${errorId}]`, error);
return {
message: 'Something went wrong',
errorId,
};
};Universal Hooks (src/hooks.ts)
reroute
Remap incoming URLs to different routes. Runs on both server and client.
// src/hooks.ts
import type { Reroute } from '@sveltejs/kit';
const translations: Record<string, string> = {
'/sobre': '/about',
'/contacto': '/contact',
'/tienda': '/shop',
};
export const reroute: Reroute = ({ url }) => {
return translations[url.pathname] ?? url.pathname;
};Error Pages
Per-route error boundaries
<!-- src/routes/blog/[slug]/+error.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message}</h1>
{#if $page.status === 404}
<p>This post could not be found.</p>
<a href="/blog">Back to blog</a>
{:else}
<p>Something went wrong loading this page.</p>
{/if}Root error page
<!-- src/routes/+error.svelte -->
<script>
import { page } from '$app/stores';
</script>
<div class="error-page">
<h1>{$page.status}</h1>
<p>{$page.error?.message ?? 'Unknown error'}</p>
<a href="/">Go home</a>
</div>Throwing Errors in Load Functions
// +page.server.ts
import { error, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals }) => {
if (!locals.user) {
redirect(303, '/login');
}
const post = await getPost(params.slug);
if (!post) {
error(404, { message: 'Not found' });
}
if (post.authorId !== locals.user.id) {
error(403, { message: 'Forbidden' });
}
return { post };
};Typing app.d.ts
Extend the App namespace to type locals, error, and pageData.
// src/app.d.ts
declare global {
namespace App {
interface Error {
message: string;
errorId?: string;
}
interface Locals {
user: { id: string; name: string; role: string } | null;
db: Database;
}
interface PageData {
user: { id: string; name: string } | null;
}
}
}
export {};Environment Variables
// Server-only (secrets, DB URLs)
import { DATABASE_URL, API_SECRET } from '$env/static/private';
// Public (exposed to client)
import { PUBLIC_API_URL } from '$env/static/public';
// Dynamic (runtime values)
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';Svelte 4 to 5 Migration Guide
Reactive State: let to $state
Svelte 4 made top-level let declarations reactive automatically. Svelte 5 requires explicit $state.
<!-- Svelte 4 -->
<script>
let count = 0;
let user = { name: 'Alice' };
</script><!-- Svelte 5 -->
<script>
let count = $state(0);
let user = $state({ name: 'Alice' });
</script>Reactive Declarations: $: to $derived / $effect
<!-- Svelte 4 -->
<script>
let count = 0;
$: double = count * 2;
$: isPositive = count > 0;
$: {
console.log('count changed:', count);
updateAnalytics(count);
}
</script><!-- Svelte 5 -->
<script>
let count = $state(0);
const double = $derived(count * 2);
const isPositive = $derived(count > 0);
$effect(() => {
console.log('count changed:', count);
updateAnalytics(count);
});
</script>Props: export let to $props
<!-- Svelte 4 -->
<script>
export let name;
export let age = 25;
export let variant = 'primary';
$$restProps; // access rest props
</script><!-- Svelte 5 -->
<script lang="ts">
let { name, age = 25, variant = 'primary', ...rest } = $props<{
name: string;
age?: number;
variant?: 'primary' | 'secondary';
}>();
</script>
<div {...rest}>{name}</div>Stores to Runes
Svelte 4 stores (writable, readable, derived) are replaced by runes in .svelte.ts modules.
// Svelte 4: src/lib/stores.ts
import { writable, derived } from 'svelte/store';
export const count = writable(0);
export const double = derived(count, ($count) => $count * 2);
// Usage in component:
// $count (auto-subscribe with $ prefix)// Svelte 5: src/lib/counter.svelte.ts
let count = $state(0);
const double = $derived(count * 2);
export function useCounter() {
return {
get count() {
return count;
},
get double() {
return double;
},
increment() {
count++;
},
reset() {
count = 0;
},
};
}<!-- Svelte 5 component -->
<script>
import { useCounter } from '$lib/counter.svelte';
const counter = useCounter();
</script>
<button onclick={counter.increment}>
{counter.count} (double: {counter.double})
</button>Why getters?
Returning count directly would capture the value at call time. Getters ensure the consumer always reads the current reactive value.
Slots to Snippets
<!-- Svelte 4: Card.svelte -->
<div class="card">
<header><slot name="header" /></header>
<slot />
<footer><slot name="footer" /></footer>
</div>
<!-- Svelte 4 usage -->
<Card>
<h2 slot="header">Title</h2>
<p>Content here</p>
<span slot="footer">Footer</span>
</Card><!-- Svelte 5: Card.svelte -->
<script lang="ts">
import { type Snippet } from 'svelte';
let { header, children, footer }: {
header?: Snippet;
children: Snippet;
footer?: Snippet;
} = $props();
</script>
<div class="card">
{#if header}
<header>{@render header()}</header>
{/if}
{@render children()}
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div>
<!-- Svelte 5 usage -->
<Card>
{#snippet header()}
<h2>Title</h2>
{/snippet}
<p>Content here</p>
{#snippet footer()}
<span>Footer</span>
{/snippet}
</Card>Events: on: to Properties
<!-- Svelte 4 -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch('submit', { value: 42 })}>
Submit
</button>
<!-- Svelte 4 parent -->
<Child on:submit={(e) => console.log(e.detail.value)} /><!-- Svelte 5 -->
<script lang="ts">
let { onsubmit }: { onsubmit: (value: number) => void } = $props();
</script>
<button onclick={() => onsubmit(42)}>
Submit
</button>
<!-- Svelte 5 parent -->
<Child onsubmit={(value) => console.log(value)} />Event Modifiers
Svelte 4 event modifiers (on:click|preventDefault|stopPropagation) are replaced by explicit wrapper functions.
<!-- Svelte 4 -->
<form on:submit|preventDefault={handleSubmit}>
<button on:click|once|capture={handleClick}><!-- Svelte 5 -->
<form onsubmit={(e) => {
e.preventDefault();
handleSubmit(e);
}}>
<!-- For once/capture, use addEventListener in $effect -->
<script>
let button: HTMLButtonElement;
$effect(() => {
button.addEventListener('click', handleClick, { once: true, capture: true });
});
</script>
<button bind:this={button}>Click</button>beforeUpdate / afterUpdate to $effect
<!-- Svelte 4 -->
<script>
import { beforeUpdate, afterUpdate } from 'svelte';
beforeUpdate(() => { /* before DOM update */ });
afterUpdate(() => { /* after DOM update */ });
</script><!-- Svelte 5 -->
<script>
$effect.pre(() => { /* before DOM update */ });
$effect(() => { /* after DOM update */ });
</script>Lifecycle: onMount, onDestroy
onMount and onDestroy still work in Svelte 5. However, $effect with a cleanup function can often replace onDestroy.
<script>
import { onMount } from 'svelte';
onMount(() => {
const interval = setInterval(() => tick(), 1000);
return () => clearInterval(interval);
});
// Or equivalently with $effect:
$effect(() => {
const interval = setInterval(() => tick(), 1000);
return () => clearInterval(interval);
});
</script>Migration CLI Tool
Svelte provides an automated migration tool that handles most mechanical changes.
npx sv migrate svelte-5This handles renaming on:event to onevent, converting export let to $props(), and other syntax changes. Review the output manually for stores-to-runes and slots-to-snippets migrations, which require structural changes.
Runes and Reactivity
$state
Declares reactive state. Primitives are tracked by value; objects and arrays are deeply proxied.
<script>
let count = $state(0);
let user = $state({ name: 'Alice', age: 30 });
let items = $state<string[]>([]);
</script>
<button onclick={() => count++}>Count: {count}</button>
<input bind:value={user.name} />$state.raw
Opts out of deep proxying. Use for large read-only datasets or objects passed to external libraries that do not expect proxies.
<script>
let rows = $state.raw(fetchedData);
function refresh(newData: Row[]) {
rows = newData;
}
</script>$state.snapshot
Creates a plain, non-reactive copy of proxied state. Useful for serialization or passing to APIs that reject proxies.
let form = $state({ name: '', email: '' });
async function submit() {
const data = $state.snapshot(form);
await fetch('/api', { method: 'POST', body: JSON.stringify(data) });
}$derived
Computes values that automatically update when dependencies change. Replaces $: reactive declarations.
<script>
let count = $state(0);
const double = $derived(count * 2);
const isEven = $derived(count % 2 === 0);
</script>
<p>{count} doubled is {double} and is {isEven ? 'even' : 'odd'}</p>$derived.by
For multi-statement derived computations.
<script>
let items = $state<{ price: number; qty: number }[]>([]);
const total = $derived.by(() => {
let sum = 0;
for (const item of items) {
sum += item.price * item.qty;
}
return sum;
});
</script>$effect
Runs side effects after the DOM updates. Automatically tracks reactive dependencies read during execution. Returns a cleanup function for teardown.
<script>
let query = $state('');
$effect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((r) => r.json())
.then((data) => {
results = data;
});
return () => controller.abort();
});
</script>$effect.pre
Runs before DOM updates. Use for reading DOM measurements that will change.
<script>
let div: HTMLDivElement;
$effect.pre(() => {
const scrollHeight = div?.scrollHeight;
});
</script>Avoiding infinite loops
Effects re-run when their dependencies change. Never write to a value you also read in the same effect.
<script>
let count = $state(0);
// WRONG: reads and writes count, creating an infinite loop
// $effect(() => { count = count + 1; });
// CORRECT: use $derived for transformations
const next = $derived(count + 1);
</script>$props
Declares component props. Replaces export let. Supports defaults, rest props, and TypeScript.
<script lang="ts">
interface Props {
name: string;
age?: number;
class?: string;
}
let { name, age = 25, class: className, ...rest } = $props<Props>();
</script>
<div class={className} {...rest}>
{name} is {age} years old
</div>$bindable
Marks a prop as two-way bindable. The parent can use bind: to sync the value.
<script lang="ts">
let { value = $bindable('') } = $props<{ value: string }>();
</script>
<input bind:value />Parent usage:
<script>
import TextInput from './TextInput.svelte';
let name = $state('');
</script>
<TextInput bind:value={name} />
<p>You typed: {name}</p>$inspect
Dev-only debugging rune. Logs values when they change. Automatically stripped in production builds.
<script>
let count = $state(0);
let user = $state({ name: 'Alice' });
$inspect(count);
$inspect(user);
$inspect(count).with(console.trace);
</script>Shared reactive state (.svelte.ts modules)
Create shared state by using $state in .svelte.ts or .svelte.js files. This replaces Svelte 4 stores.
// src/lib/counter.svelte.ts
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count++;
}
export function reset() {
count = 0;
}<script>
import { getCount, increment } from '$lib/counter.svelte';
</script>
<button onclick={increment}>Count: {getCount()}</button>Class-based shared state
// src/lib/todo-store.svelte.ts
export class TodoStore {
items = $state<{ id: string; text: string; done: boolean }[]>([]);
filter = $state<'all' | 'active' | 'done'>('all');
filtered = $derived.by(() => {
switch (this.filter) {
case 'active':
return this.items.filter((t) => !t.done);
case 'done':
return this.items.filter((t) => t.done);
default:
return this.items;
}
});
add(text: string) {
this.items.push({ id: crypto.randomUUID(), text, done: false });
}
toggle(id: string) {
const item = this.items.find((t) => t.id === id);
if (item) item.done = !item.done;
}
}<script>
import { TodoStore } from '$lib/todo-store.svelte';
const store = new TodoStore();
</script>
{#each store.filtered as todo (todo.id)}
<label>
<input type="checkbox" checked={todo.done} onchange={() => store.toggle(todo.id)} />
{todo.text}
</label>
{/each}SvelteKit Routing
File-Based Routing
Routes are defined by the directory structure under src/routes/.
src/routes/
├── +page.svelte → /
├── +layout.svelte → layout for all pages
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ ├── +page.server.ts → server load for /blog
│ └── [slug]/
│ ├── +page.svelte → /blog/:slug
│ └── +page.server.ts → server load for /blog/:slug
├── api/
│ └── users/
│ └── +server.ts → /api/users (API endpoint)
└── (auth)/
├── +layout.svelte → layout group (no URL segment)
├── login/
│ └── +page.svelte → /login
└── register/
└── +page.svelte → /registerRoute Parameters
src/routes/blog/[slug]/+page.svelte → /blog/hello-world
src/routes/[category]/[id]/+page.svelte → /electronics/42
src/routes/files/[...path]/+page.svelte → /files/a/b/c (rest param)
src/routes/[[lang]]/about/+page.svelte → /about or /en/about (optional)Parameter Matchers
Create type-safe route params by adding matchers in src/params/.
// src/params/integer.ts
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return /^\d+$/.test(param);
};src/routes/items/[id=integer]/+page.svelte → only matches /items/42Server Load Functions
Run exclusively on the server. Have access to databases, secrets, and file system. Defined in +page.server.ts or +layout.server.ts.
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, locals }) => {
const post = await locals.db.post.findUnique({
where: { slug: params.slug },
});
if (!post) {
error(404, { message: 'Post not found' });
}
return { post };
};<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>Universal Load Functions
Run on both server and client. Use SvelteKit's fetch for automatic cookie forwarding and relative URL resolution. Defined in +page.ts or +layout.ts.
// src/routes/blog/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts');
const posts: Post[] = await response.json();
return { posts };
};Layouts
Shared UI and data loading for nested routes. Every route inherits from its nearest +layout.svelte.
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from './$types';
import { type Snippet } from 'svelte';
let { data, children }: { data: LayoutData; children: Snippet } = $props();
</script>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
{#if data.user}
<span>{data.user.name}</span>
{/if}
</nav>
<main>
{@render children()}
</main>// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return { user: locals.user };
};Layout Groups
Group routes to share layouts without adding a URL segment. Wrap the group name in parentheses.
src/routes/
├── (marketing)/
│ ├── +layout.svelte → marketing layout (wide, no sidebar)
│ ├── +page.svelte → /
│ └── pricing/
│ └── +page.svelte → /pricing
└── (app)/
├── +layout.svelte → app layout (sidebar, auth required)
├── dashboard/
│ └── +page.svelte → /dashboard
└── settings/
└── +page.svelte → /settingsPage Options
Control rendering strategy per route.
// +page.ts or +page.server.ts
export const prerender = true;
export const ssr = true;
export const csr = true;
export const trailingSlash = 'never';| Option | Effect |
|---|---|
prerender | Generate static HTML at build time |
ssr = false | Client-side only rendering |
csr = false | No client-side JavaScript (static HTML) |
trailingSlash | 'never', 'always', or 'ignore' |
API Routes (+server.ts)
Standalone request handlers, not tied to a page.
// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, locals }) => {
const limit = Number(url.searchParams.get('limit') ?? 10);
const users = await locals.db.user.findMany({ take: limit });
return json(users);
};
export const POST: RequestHandler = async ({ request, locals }) => {
const body = await request.json();
const user = await locals.db.user.create({ data: body });
return json(user, { status: 201 });
};Using $page Store
Access current route info, URL, params, and data.
<script>
import { page } from '$app/stores';
</script>
<p>Current path: {$page.url.pathname}</p>
<p>Route param: {$page.params.slug}</p>Navigation
<script>
import { goto, invalidateAll } from '$app/navigation';
async function navigateAway() {
await goto('/dashboard');
}
async function refreshData() {
await invalidateAll();
}
</script>
<a href="/about">About (link)</a>
<button onclick={navigateAway}>Go to dashboard</button>Dependent Load Functions
Child load functions can access parent data via await parent().
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
const stats = await fetchStats(user.id);
return { stats };
};