
Svelte5 Best Practices
- 5k installs
- 5 repo stars
- Updated June 25, 2026
- ejirocodes/agent-skills
svelte5-best-practices covers Svelte 5 runes, snippets, events, TypeScript, SvelteKit, migration, and performance patterns.
About
The svelte5-best-practices skill guides writing and reviewing Svelte 5 components and SvelteKit applications with modern runes, snippets, and event patterns. Quick reference links cover runes, snippets, events, TypeScript, migration, SvelteKit, and performance reference files. Essential patterns show $state and $derived reactivity, $props and $bindable for two-way binding, snippets with render replacing slots, onclick instead of on:click, and callback props replacing createEventDispatcher. Common mistakes list using let without $state, $effect for derived values, deprecated on:click and createEventDispatcher, slot instead of snippets, missing $bindable, module-level SSR state leaks, and sequential awaits in load functions instead of Promise.all. Triggers include Svelte components, runes like $state $derived $effect $props, snippets, SvelteKit load and form actions, Svelte 4 to 5 migration, and performance optimization. Progressive disclosure via references for deep dives on runes, snippets, events, typescript, migration, sveltekit, and performance topics.
- Svelte 5 runes: $state, $derived, $effect, $props, $bindable, $inspect patterns.
- Snippets replace slots with {#snippet} and {@render} syntax.
- Events use onclick; callback props replace createEventDispatcher.
- Eight common mistakes including SSR state leaks and sequential load awaits.
- Reference files for runes, snippets, SvelteKit, migration, and performance.
Svelte5 Best Practices by the numbers
- 5,028 all-time installs (skills.sh)
- +189 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #104 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
svelte5-best-practices capabilities & compatibility
- Capabilities
- runes reactive state and derived patterns · snippet based composition replacing slots · callback props event handling · sveltekit load and form action guidance · migration and performance reference routing
- Use cases
- frontend · refactoring
What svelte5-best-practices says it does
Use when writing, reviewing, or refactoring Svelte 5 components and SvelteKit applications.
npx skills add https://github.com/ejirocodes/agent-skills --skill svelte5-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5k |
|---|---|
| repo stars | ★ 5 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 25, 2026 |
| Repository | ejirocodes/agent-skills ↗ |
How do I write correct Svelte 5 components with runes, snippets, and SvelteKit best practices?
Apply Svelte 5 runes, snippets, events, TypeScript props, SvelteKit data loading, migration, and performance patterns.
Who is it for?
Frontend developers building or migrating to Svelte 5 and SvelteKit with TypeScript.
Skip if: Skip when the project remains on Svelte 4 without migration plans and no Svelte 5 runes.
When should I use this skill?
User writes Svelte 5 components, mentions runes, snippets, SvelteKit load, or Svelte 4 migration.
What you get
Svelte 5 components using runes, snippets, callback props, and SvelteKit patterns without common migration pitfalls.
- Svelte 5 component patterns
- Migration guidance references
- SvelteKit load optimizations
By the numbers
- 8 documented common mistakes
- 7 reference topic files
- onclick replaces on:click in Svelte 5
Files
Svelte 5 Best Practices
Quick Reference
| Topic | When to Use | Reference |
|---|---|---|
| Runes | $state, $derived, $effect, $props, $bindable, $inspect | runes.md |
| Snippets | Replacing slots, {#snippet}, {@render} | snippets.md |
| Events | onclick handlers, callback props, context API | events.md |
| TypeScript | Props typing, generic components | typescript.md |
| Migration | Svelte 4 to 5, stores to runes | migration.md |
| SvelteKit | Load functions, form actions, SSR, page typing | sveltekit.md |
| Performance | Universal reactivity, avoiding over-reactivity, streaming | performance.md |
Essential Patterns
Reactive State
<script>
let count = $state(0); // Reactive state
let doubled = $derived(count * 2); // Computed value
</script>Component Props
<script>
let { name, count = 0 } = $props();
let { value = $bindable() } = $props(); // Two-way binding
</script>Snippets (replacing slots)
<script>
let { children, header } = $props();
</script>
{@render header?.()}
{@render children()}Event Handlers
<!-- Svelte 5: use onclick, not on:click -->
<button onclick={() => count++}>Click</button>Callback Props (replacing createEventDispatcher)
<script>
let { onclick } = $props();
</script>
<button onclick={() => onclick?.({ data })}>Click</button>Common Mistakes
1. Using `let` without `$state` - Variables are not reactive without $state() 2. Using `$effect` for derived values - Use $derived instead 3. Using `on:click` syntax - Use onclick in Svelte 5 4. Using `createEventDispatcher` - Use callback props instead 5. Using `<slot>` - Use snippets with {@render} 6. Forgetting `$bindable()` - Required for bind: to work 7. Setting module-level state in SSR - Causes cross-request leaks 8. Sequential awaits in load functions - Use Promise.all for parallel requests
Svelte 5 Events Reference
Table of Contents
---
Event Handler Syntax
Svelte 5 replaces on:click directive syntax with standard HTML attribute syntax onclick.
Basic Event Handlers
Svelte 4:
<button on:click={handleClick}>Click</button>
<input on:input={handleInput} />
<form on:submit={handleSubmit}>...</form>Svelte 5:
<button onclick={handleClick}>Click</button>
<input oninput={handleInput} />
<form onsubmit={handleSubmit}>...</form>Event Modifiers Migration
Event modifiers no longer exist. Use wrapper functions:
Svelte 4:
<form on:submit|preventDefault={handleSubmit}>...</form>
<button on:click|stopPropagation={handleClick}>...</button>Svelte 5:
<script>
function handleSubmit(event) {
event.preventDefault();
// ... handle form
}
function handleClick(event) {
event.stopPropagation();
// ... handle click
}
</script>
<form onsubmit={handleSubmit}>...</form>
<button onclick={handleClick}>...</button>Capture, Passive, and NonPassive
<!-- Capture phase -->
<div onclickcapture={handleCapture}>
<button onclick={handleClick}>Click</button>
</div>
<!-- Passive listener -->
<div ontouchstartpassive={handleTouch}>...</div>
<!-- Non-passive -->
<div ontouchmovenonpassive={(e) => e.preventDefault()}>...</div>Inline Handlers
<button onclick={() => count++}>Count: {count}</button>
<input oninput={(e) => name = e.target.value} />Event Handler Shorthand
<script>
function onclick(event) {
console.log('Clicked!', event);
}
</script>
<button {onclick}>Click</button>Spreading Event Handlers
<script>
let handlers = {
onclick: () => console.log('clicked'),
onmouseenter: () => console.log('entered'),
onmouseleave: () => console.log('left')
};
</script>
<button {...handlers}>Hover or Click</button>Multiple Handlers for Same Event
In Svelte 5, combine logic into one handler:
<script>
function handleClick(event) {
handler1(event);
handler2(event);
}
</script>
<button onclick={handleClick}>...</button>TypeScript Event Typing
<script lang="ts">
function handleClick(event: MouseEvent) {
console.log(event.clientX, event.clientY);
}
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
console.log(target.value);
}
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
const formData = new FormData(event.currentTarget as HTMLFormElement);
}
</script>---
Callback Props Pattern
Svelte 5 deprecates createEventDispatcher in favor of callback props for component-to-parent communication.
Basic Event Pattern
Svelte 4:
<!-- Button.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch('click', { timestamp: Date.now() })}>Click</button>
<!-- Parent.svelte -->
<Button on:click={(e) => console.log(e.detail)} />Svelte 5:
<!-- Button.svelte -->
<script>
let { onclick } = $props();
</script>
<button onclick={() => onclick?.({ timestamp: Date.now() })}>Click</button>
<!-- Parent.svelte -->
<Button onclick={(data) => console.log(data)} />Multiple Callbacks
<!-- Dialog.svelte -->
<script>
let { onconfirm, oncancel, onclose } = $props();
</script>
<dialog>
<button onclick={() => onconfirm?.()}>Confirm</button>
<button onclick={() => oncancel?.()}>Cancel</button>
<button onclick={() => onclose?.()}>X</button>
</dialog>
<!-- Parent.svelte -->
<Dialog
onconfirm={() => save()}
oncancel={() => reset()}
onclose={() => visible = false}
/>Typed Callbacks with TypeScript
<script lang="ts">
interface Props {
value?: string;
onsearch?: (query: string) => void;
onchange?: (value: string) => void;
onclear?: () => void;
}
let { value = '', onsearch, onchange, onclear }: Props = $props();
function handleInput(e: Event) {
const newValue = (e.target as HTMLInputElement).value;
value = newValue;
onchange?.(newValue);
}
</script>Forwarding Native Events
<script lang="ts">
import type { MouseEventHandler } from 'svelte/elements';
interface Props {
onclick?: MouseEventHandler<HTMLButtonElement>;
children: import('svelte').Snippet;
}
let { onclick, children, ...rest }: Props = $props();
</script>
<button {onclick} {...rest}>{@render children()}</button>---
Context API
Context functions must be called synchronously during component initialization.
Correct Context Usage
<script>
import { setContext, getContext } from 'svelte';
// These run during initialization - CORRECT
setContext('theme', 'dark');
const theme = getContext('theme');
// INCORRECT - in $effect or callback
// $effect(() => { setContext('theme', 'dark'); }); // ERROR
</script>Reactive Context Values
Pass $state objects for reactive context:
<!-- Provider.svelte -->
<script>
import { setContext } from 'svelte';
let theme = $state('light');
setContext('theme', {
get current() { return theme; },
toggle() { theme = theme === 'light' ? 'dark' : 'light'; }
});
let { children } = $props();
</script>
{@render children()}
<!-- Consumer.svelte -->
<script>
import { getContext } from 'svelte';
const themeContext = getContext('theme');
</script>
<p>Theme: {themeContext.current}</p>
<button onclick={themeContext.toggle}>Toggle</button>Type-Safe Context Pattern
// context.ts
import { setContext, getContext } from 'svelte';
const THEME_KEY = Symbol('theme');
interface ThemeContext {
current: 'light' | 'dark';
toggle: () => void;
}
export function setThemeContext(context: ThemeContext) {
setContext(THEME_KEY, context);
}
export function getThemeContext(): ThemeContext {
const context = getContext<ThemeContext>(THEME_KEY);
if (!context) throw new Error('Theme context not found');
return context;
}Avoid Generic Keys
<script>
// Option 1: Symbol
const USER_KEY = Symbol('user');
setContext(USER_KEY, userData);
// Option 2: Unique string
setContext('myapp:user', userData);
// Option 3: Object key
const userKey = {};
setContext(userKey, userData);
</script>hasContext Check
<script>
import { hasContext, getContext } from 'svelte';
const hasTheme = hasContext('theme');
const theme = hasTheme ? getContext('theme') : { current: 'light' };
</script>Context Boundaries
Context flows down the component tree:
<!-- Parent.svelte -->
<script>
import { getContext, setContext } from 'svelte';
const level = getContext('level'); // 0
setContext('level', level + 1); // Override for children
</script>
<Child /> <!-- Will see level = 1 -->Svelte 4 to Svelte 5 Migration Reference
Table of Contents
---
Migrating Reactive Statements
Svelte 5 replaces $: with $derived (for values) and $effect (for side effects).
Computed Values: $: to $derived
Svelte 4:
<script>
let count = 0;
$: doubled = count * 2;
$: quadrupled = doubled * 2;
</script>Svelte 5:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
</script>Complex Computed Values: $derived.by
Svelte 4:
<script>
let items = [];
let filter = 'all';
$: filteredItems = {
if (filter === 'all') return items;
if (filter === 'active') return items.filter(i => !i.done);
return items.filter(i => i.done);
};
</script>Svelte 5:
<script>
let items = $state([]);
let filter = $state('all');
let filteredItems = $derived.by(() => {
if (filter === 'all') return items;
if (filter === 'active') return items.filter(i => !i.done);
return items.filter(i => i.done);
});
</script>Side Effects: $: to $effect
Svelte 4:
<script>
let count = 0;
$: console.log('Count changed:', count);
$: document.title = `Count: ${count}`;
</script>Svelte 5:
<script>
let count = $state(0);
$effect(() => { console.log('Count changed:', count); });
$effect(() => { document.title = `Count: ${count}`; });
</script>Conditional Side Effects
Svelte 4:
<script>
let value;
$: if (value > 100) { alert('Value too high!'); }
</script>Svelte 5:
<script>
let value = $state(0);
$effect(() => {
if (value > 100) { alert('Value too high!'); }
});
</script>Props Migration
Svelte 4:
<script>
export let name;
export let count = 0;
$: greeting = `Hello, ${name}!`;
</script>Svelte 5:
<script>
let { name, count = 0 } = $props();
let greeting = $derived(`Hello, ${name}!`);
</script>Effect with Cleanup
Svelte 4:
<script>
import { onDestroy } from 'svelte';
let count = 0;
let interval;
$: {
clearInterval(interval);
interval = setInterval(() => console.log(count), 1000);
}
onDestroy(() => clearInterval(interval));
</script>Svelte 5:
<script>
let count = $state(0);
$effect(() => {
const interval = setInterval(() => console.log(count), 1000);
return () => clearInterval(interval); // Cleanup built-in
});
</script>Migration Cheat Sheet
| Svelte 4 Pattern | Svelte 5 Replacement |
|---|---|
let x = 0 (in component) | let x = $state(0) |
export let prop | let { prop } = $props() |
$: derived = expr | let derived = $derived(expr) |
$: { complex } (value) | let x = $derived.by(() => { ... }) |
$: console.log(x) | $effect(() => console.log(x)) |
$: if (x) { ... } | $effect(() => { if (x) { ... } }) |
$: document.title = x | $effect(() => { document.title = x }) |
Automated Migration
npx sv migrate svelte-5---
Migrating Stores to Runes
Svelte 5 runes can replace most store use cases with simpler, more direct reactive state.
Local Component State
Svelte 4:
<script>
import { writable } from 'svelte/store';
const count = writable(0);
function increment() { count.update(n => n + 1); }
</script>
<button on:click={increment}>Count: {$count}</button>Svelte 5:
<script>
let count = $state(0);
function increment() { count++; }
</script>
<button onclick={increment}>Count: {count}</button>Shared State Across Components
Svelte 4 (stores.ts):
import { writable } from 'svelte/store';
export const user = writable(null);
export const theme = writable('light');Svelte 5 (state.svelte.ts):
export const user = $state<User | null>(null);
export const theme = $state({ current: 'light' as 'light' | 'dark' });
export function setTheme(newTheme: 'light' | 'dark') {
theme.current = newTheme;
}<script>
import { theme, setTheme } from './state.svelte';
</script>
<p>Theme: {theme.current}</p>
<button onclick={() => setTheme('dark')}>Dark Mode</button>Derived Store to $derived
Svelte 4:
import { writable, derived } from 'svelte/store';
export const items = writable([]);
export const completedCount = derived(items, $items =>
$items.filter(i => i.done).length
);Svelte 5:
// state.svelte.ts
export const items = $state<Item[]>([]);
export function getCompletedCount() {
return items.filter(i => i.done).length;
}<script>
import { items } from './state.svelte';
let completedCount = $derived(items.filter(i => i.done).length);
</script>Custom Store to Reactive Class
Svelte 4:
function 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();Svelte 5:
// counter.svelte.ts
class Counter {
value = $state(0);
increment() { this.value++; }
decrement() { this.value--; }
reset() { this.value = 0; }
}
export const counter = new Counter();<script>
import { counter } from './counter.svelte';
</script>
<p>{counter.value}</p>
<button onclick={() => counter.increment()}>+</button>Async State Pattern
Svelte 5:
// api.svelte.ts
export const state = $state({
data: null as Data | null,
loading: false,
error: null as Error | null
});
export async function fetchData() {
state.loading = true;
state.error = null;
try {
const res = await fetch('/api/data');
state.data = await res.json();
} catch (e) {
state.error = e as Error;
} finally {
state.loading = false;
}
}When to Still Use Stores
1. Library interop - Libraries that expect Svelte stores 2. Legacy code - Gradual migration 3. Observable patterns - When you need the subscribe API 4. Server-side rendering - Stores have built-in SSR handling
Store to Rune Cheat Sheet
| Store Pattern | Rune Replacement |
|---|---|
writable(value) | $state(value) |
$store (auto-subscribe) | Direct access to $state value |
store.set(x) | state = x |
store.update(fn) | Direct mutation or reassignment |
derived(store, fn) | $derived(fn()) in component |
readable(value, start) | $state + $effect for setup |
Svelte 5 Performance Reference
Table of Contents
- Universal Reactivity
- Avoiding Over-Reactivity
- Preventing Load Waterfalls
- Streaming Non-Critical Data
- Component Testing
---
Universal Reactivity
Svelte 5 allows runes in .svelte.js or .svelte.ts files for shared state outside components.
Shared Counter State
// counter.svelte.ts
export const counter = $state({ count: 0 });
export function increment() { counter.count++; }
export function decrement() { counter.count--; }
export function reset() { counter.count = 0; }<script>
import { counter, increment } from './counter.svelte';
</script>
<p>Count: {counter.count}</p>
<button onclick={increment}>+</button>Object State with Getters
// user.svelte.ts
const state = $state<User>({ firstName: '', lastName: '', email: '' });
export const user = {
get firstName() { return state.firstName; },
set firstName(v: string) { state.firstName = v; },
get fullName() { return `${state.firstName} ${state.lastName}`; },
get isValid() { return state.firstName && state.email.includes('@'); }
};Reactive Class Pattern
// todo.svelte.ts
class TodoStore {
items = $state<Todo[]>([]);
filter = $state<'all' | 'active' | 'completed'>('all');
get filtered() {
switch (this.filter) {
case 'active': return this.items.filter(t => !t.done);
case 'completed': return this.items.filter(t => t.done);
default: return this.items;
}
}
add(text: string) {
this.items.push({ id: Date.now(), text, done: false });
}
}
export const todos = new TodoStore();Important Notes
1. File extension matters: Must use .svelte.js or .svelte.ts 2. No $derived in module scope: Use getters for derived values 3. SSR caution: Avoid initializing browser-only state at module level
---
Avoiding Over-Reactivity
Common mistakes with runes can cause unnecessary re-renders, infinite loops, or degraded performance.
Anti-Pattern 1: Using $effect to Set Derived Values
WRONG:
<script>
let count = $state(0);
let doubled = $state(0);
$effect(() => { doubled = count * 2; }); // Anti-pattern!
</script>CORRECT:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>Anti-Pattern 2: Circular Dependencies
WRONG:
<script>
let a = $state(1);
let b = $state(2);
$effect(() => {
if (a > 10) b = 0;
if (b > 10) a = 0; // Triggers the effect again!
});
</script>CORRECT: Use separate effects or event handlers
Anti-Pattern 3: Not Using untrack
WRONG:
<script>
let count = $state(0);
let log = $state([]);
$effect(() => {
log.push(`Count is ${count}`); // log change triggers re-run!
});
</script>CORRECT:
<script>
import { untrack } from 'svelte';
let count = $state(0);
let log = $state([]);
$effect(() => {
untrack(() => { log.push(`Count is ${count}`); });
});
</script>Anti-Pattern 4: Heavy Computations in $derived
WRONG:
<script>
let items = $state([/* thousands */]);
let filter = $state('');
let filtered = $derived(
items.filter(item => JSON.stringify(item).includes(filter))
);
</script>CORRECT: Debounce expensive computations
<script>
let filter = $state('');
let debouncedFilter = $state('');
$effect(() => {
const timeout = setTimeout(() => { debouncedFilter = filter; }, 300);
return () => clearTimeout(timeout);
});
let filtered = $derived(
items.filter(item => item.name.includes(debouncedFilter))
);
</script>Anti-Pattern 5: Effect for DOM Manipulation
WRONG:
<script>
let visible = $state(false);
let element;
$effect(() => {
if (element) element.style.display = visible ? 'block' : 'none';
});
</script>CORRECT:
{#if visible}<div>Content</div>{/if}
<!-- Or -->
<div class:hidden={!visible}>Content</div>Performance Tips
1. Use $derived over $effect for computed values 2. Debounce expensive reactive computations 3. Use untrack to prevent unnecessary dependencies 4. Group related state into objects 5. Let Svelte handle DOM updates
---
Preventing Load Waterfalls
Sequential API calls multiply latency. Use parallel requests and streaming.
Anti-Pattern: Waterfall
WRONG (3 seconds total):
export const load = async ({ fetch }) => {
const user = await fetch('/api/user').then(r => r.json()); // 1s
const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json()); // 1s
const comments = await fetch('/api/comments').then(r => r.json()); // 1s
return { user, posts, comments };
};Pattern 1: Parallel with Promise.all
CORRECT (1 second total):
export const load = async ({ fetch }) => {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
};Pattern 2: Partial Dependencies
export const load = async ({ fetch }) => {
const user = await fetch('/api/user').then(r => r.json());
const [posts, followers] = await Promise.all([
fetch(`/api/users/${user.id}/posts`).then(r => r.json()),
fetch(`/api/users/${user.id}/followers`).then(r => r.json())
]);
return { user, posts, followers };
};Pattern 3: Streaming Non-Critical Data
export const load = async ({ fetch }) => {
const user = await fetch('/api/user').then(r => r.json());
return {
user,
recommendations: fetch('/api/recommendations').then(r => r.json()),
analytics: fetch('/api/analytics').then(r => r.json())
};
};Performance Comparison
| Pattern | 3 APIs x 1s each |
|---|---|
| Sequential | 3 seconds |
| Parallel (Promise.all) | 1 second |
| Streaming | 0s initial, streams rest |
---
Streaming Non-Critical Data
Return promises from load functions to stream non-essential data after initial page render.
Basic Streaming
WRONG (blocks on slow data):
export const load = async ({ fetch }) => {
const user = await fetch('/api/user').then(r => r.json()); // 100ms
const analytics = await fetch('/api/analytics').then(r => r.json()); // 2000ms
return { user, analytics }; // Page blocked for 2.1 seconds
};CORRECT (stream slow data):
export const load = async ({ fetch }) => {
const user = await fetch('/api/user').then(r => r.json());
const analytics = fetch('/api/analytics').then(r => r.json()); // Don't await
return {
user, // Available in 100ms
analytics // Streams when ready
};
};Component Handling
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<header><h1>Welcome, {data.user.name}</h1></header>
<aside>
{#await data.analytics}
<div class="skeleton">Loading analytics...</div>
{:then analytics}
<div class="analytics">
<p>Page views: {analytics.views}</p>
</div>
{:catch}
<div class="error">Failed to load analytics</div>
{/await}
</aside>When to Stream
| Data Type | Stream? | Reason |
|---|---|---|
| User info | No | Critical for layout |
| Main content | No | Users came for this |
| Analytics | Yes | Not user-facing |
| Recommendations | Yes | Supplementary |
| Comments | Maybe | Important but can load later |
---
Component Testing
Svelte 5 components with runes require proper Vitest configuration.
Option 1: Vitest Browser Mode (Recommended)
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
test: {
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium'
}
}
});Option 2: JSDOM with @testing-library/svelte
// 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: {
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,ts}'],
globals: true
}
});Basic Component Test
import { render, screen, fireEvent } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments count when clicked', async () => {
render(Counter);
const button = screen.getByRole('button');
expect(button).toHaveTextContent('Count: 0');
await fireEvent.click(button);
expect(button).toHaveTextContent('Count: 1');
});Testing Props
test('renders with custom name', () => {
render(Greeting, { props: { name: 'Alice' } });
expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();
});Testing Callbacks
import { vi } from 'vitest';
test('calls onclick callback when clicked', async () => {
const handleClick = vi.fn();
render(Button, { props: { onclick: handleClick } });
await fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});Testing Async Components
import { waitFor } from '@testing-library/svelte';
test('loads and displays user data', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ name: 'Alice' })
});
render(UserProfile, { props: { userId: '123' } });
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});Svelte 5 Runes Reference
Table of Contents
- $state - Reactive State
- $derived - Computed Values
- $effect - Side Effects
- $props - Component Props
- $bindable - Two-Way Binding
- $inspect - Debugging
---
$state - Reactive State
In Svelte 5, plain let declarations are no longer automatically reactive. Use $state() for reactive state.
Basic Usage
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
Clicks: {count}
</button>Object and Array State
Objects and arrays are deeply reactive by default:
<script>
let user = $state({ name: 'Alice', age: 30 });
let items = $state(['apple', 'banana']);
</script>
<button onclick={() => user.age++}>Age: {user.age}</button>
<button onclick={() => items.push('cherry')}>Items: {items.length}</button>Class State
<script>
class Counter {
count = $state(0);
increment() { this.count++; }
}
const counter = new Counter();
</script>
<button onclick={() => counter.increment()}>{counter.count}</button>Raw State (Opt-out of Deep Reactivity)
<script>
let items = $state.raw([1, 2, 3]);
// This WON'T trigger an update:
items.push(4);
// This WILL trigger an update:
items = [...items, 4];
</script>---
$derived - Computed Values
Replaces $: reactive statements for computed values.
Basic Usage
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
</script>Complex Derivations with $derived.by
<script>
let items = $state([1, 2, 3, 4, 5]);
let filter = $state('even');
let filteredItems = $derived.by(() => {
if (filter === 'even') return items.filter(n => n % 2 === 0);
return items.filter(n => n % 2 !== 0);
});
</script>Deriving from Props
<script>
let { firstName, lastName } = $props();
let fullName = $derived(`${firstName} ${lastName}`);
</script>$derived vs $effect
Use $derived for computing values, $effect for side effects:
<script>
let count = $state(0);
// CORRECT: Use $derived for computed values
let doubled = $derived(count * 2);
// INCORRECT: Don't use $effect to set derived values
// let doubled;
// $effect(() => { doubled = count * 2; }); // Anti-pattern!
</script>---
$effect - Side Effects
Runs code when component mounts and when dependencies change. Requires cleanup for subscriptions.
Basic Effect with Cleanup
<script>
let count = $state(0);
$effect(() => {
const interval = setInterval(() => count++, 1000);
return () => clearInterval(interval); // Cleanup
});
</script>DOM Event Listeners
<script>
let mouseX = $state(0);
let mouseY = $state(0);
$effect(() => {
function handleMouseMove(event) {
mouseX = event.clientX;
mouseY = event.clientY;
}
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
});
</script>External Subscriptions
<script>
let { userId } = $props();
let userData = $state(null);
$effect(() => {
const unsubscribe = database.subscribe(`users/${userId}`, (data) => {
userData = data;
});
return () => unsubscribe();
});
</script>$effect.pre for Pre-DOM Updates
<script>
let div;
let messages = $state([]);
$effect.pre(() => {
if (div) {
const shouldScroll = div.scrollTop + div.clientHeight >= div.scrollHeight - 20;
if (shouldScroll) {
tick().then(() => { div.scrollTop = div.scrollHeight; });
}
}
});
</script>Untracked Dependencies
<script>
import { untrack } from 'svelte';
let count = $state(0);
let logCount = $state(0);
$effect(() => {
// Only runs when count changes, not logCount
console.log(count, untrack(() => logCount));
});
</script>---
$props - Component Props
Replaces export let for declaring component props.
Basic Usage
<script>
let { name, count = 0, disabled = false } = $props();
</script>Rest Props Pattern
<script>
let { class: className, children, ...restProps } = $props();
</script>
<div class={className} {...restProps}>
{@render children?.()}
</div>Renaming Reserved Words
<script>
let {
class: className, // 'class' is reserved
for: htmlFor, // 'for' is reserved
...rest
} = $props();
</script>TypeScript Props
<script lang="ts">
interface Props {
name: string;
count?: number;
onClick?: (event: MouseEvent) => void;
}
let { name, count = 0, onClick }: Props = $props();
</script>Props with Children
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title: string;
children: Snippet;
footer?: Snippet;
}
let { title, children, footer }: Props = $props();
</script>
<article>
<h1>{title}</h1>
<main>{@render children()}</main>
{#if footer}<footer>{@render footer()}</footer>{/if}
</article>---
$bindable - Two-Way Binding
Props must be explicitly marked with $bindable() to support bind:.
Basic Usage
<!-- Input.svelte -->
<script>
let { value = $bindable() } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script>
import Input from './Input.svelte';
let name = $state('');
</script>
<Input bind:value={name} />Bindable with Default Value
<script>
let { value = $bindable('default') } = $props();
</script>Multiple Bindable Props
<script>
let {
value = $bindable(''),
checked = $bindable(false),
selected = $bindable(null)
} = $props();
</script>TypeScript with Bindable
<script lang="ts">
interface Props {
value: string;
disabled?: boolean;
}
let { value = $bindable(''), disabled = false }: Props = $props();
</script>---
$inspect - Debugging
Development-only debugging that logs values when they change. Stripped in production.
Basic Usage
<script>
let count = $state(0);
$inspect(count); // Logs every time count changes
</script>Multiple Values
<script>
let name = $state('Alice');
let age = $state(30);
$inspect(name, age);
</script>Custom Logging with $inspect.with
<script>
let user = $state({ name: 'Alice', age: 30 });
$inspect.with((type, ...values) => {
if (type === 'init') console.log('Initial value:', values);
else console.log('Updated to:', values);
}, user);
</script>Debugging with Breakpoints
<script>
let data = $state({ x: 0, y: 0 });
$inspect.with((type, value) => {
if (type === 'update' && value.x > 100) debugger;
}, data);
</script>Svelte 5 Snippets Reference
Table of Contents
---
Replacing Slots with Snippets
Svelte 5 replaces <slot> with snippets - a more powerful and type-safe composition primitive.
Default Content (children)
Svelte 4:
<div class="card"><slot /></div>Svelte 5:
<script>
let { children } = $props();
</script>
<div class="card">{@render children?.()}</div>Named Slots to Named Snippets
Svelte 4:
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
<!-- Usage -->
<Layout>
<h1 slot="header">Title</h1>
<p>Content</p>
<span slot="footer">Footer</span>
</Layout>Svelte 5:
<script>
let { header, children, footer } = $props();
</script>
<header>{@render header?.()}</header>
<main>{@render children?.()}</main>
<footer>{@render footer?.()}</footer>
<!-- Usage -->
<Layout>
{#snippet header()}<h1>Title</h1>{/snippet}
<p>Content</p>
{#snippet footer()}<span>Footer</span>{/snippet}
</Layout>Slot Props to Snippet Parameters
Svelte 4:
<ul>
{#each items as item}
<li><slot {item} /></li>
{/each}
</ul>
<List {items} let:item>
<span>{item.name}</span>
</List>Svelte 5:
<script>
let { items, children } = $props();
</script>
<ul>
{#each items as item}
<li>{@render children?.(item)}</li>
{/each}
</ul>
<List {items}>
{#snippet children(item)}
<span>{item.name}</span>
{/snippet}
</List>Slot Fallback to Snippet Fallback
<script>
let { children } = $props();
</script>
{#if children}
{@render children()}
{:else}
<p>Default content</p>
{/if}Defining Snippets Within Components
<script>
let items = $state([
{ id: 1, name: 'Item 1', type: 'normal' },
{ id: 2, name: 'Item 2', type: 'featured' }
]);
</script>
{#snippet normalItem(item)}
<li>{item.name}</li>
{/snippet}
{#snippet featuredItem(item)}
<li class="featured">*** {item.name} ***</li>
{/snippet}
<ul>
{#each items as item}
{#if item.type === 'featured'}
{@render featuredItem(item)}
{:else}
{@render normalItem(item)}
{/if}
{/each}
</ul>TypeScript Typing
<script lang="ts">
import type { Snippet } from 'svelte';
interface Item { id: number; name: string; }
interface Props {
header?: Snippet;
children: Snippet<[item: Item]>;
footer?: Snippet;
}
let { header, children, footer }: Props = $props();
</script>---
Rendering with @render
Snippets must be rendered using {@render}. Optional snippets need null-safe calling.
Basic Snippet Rendering
<script>
let { children } = $props();
</script>
<div>{@render children()}</div>Optional Snippets
Always use optional chaining for snippets that might not be provided:
<script>
let { children } = $props();
</script>
{@render children?.()}Conditional Rendering
<script>
let { header, children, footer } = $props();
</script>
{#if header}
<header>{@render header()}</header>
{/if}
<main>{@render children?.()}</main>
{#if footer}
<footer>{@render footer()}</footer>
{/if}Rendering with Arguments
<script>
let { items, itemTemplate } = $props();
</script>
<ul>
{#each items as item, index}
<li>{@render itemTemplate(item, index)}</li>
{/each}
</ul>
<!-- Usage -->
<List {items}>
{#snippet itemTemplate(item, index)}
<span>{index + 1}. {item.name}</span>
{/snippet}
</List>Rendering Multiple Times
Snippets can be rendered multiple times:
<script>
let { icon } = $props();
</script>
<button>
{@render icon?.()}
<span>Click me</span>
{@render icon?.()}
</button>Rendering Dynamic Snippets
<script>
let { normalView, editView, isEditing } = $props();
let currentView = $derived(isEditing ? editView : normalView);
</script>
{@render currentView?.()}TypeScript with @render
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
header?: Snippet;
row: Snippet<[data: { id: number; name: string }]>;
}
let { children, header, row }: Props = $props();
</script>
{@render header?.()}
{@render children()}
{@render row({ id: 1, name: 'Test' })}Passing Snippets to Child Components
<!-- Wrapper.svelte -->
<script>
import Inner from './Inner.svelte';
let { itemRenderer } = $props();
</script>
<Inner {itemRenderer} />
<!-- Inner.svelte -->
<script>
let { itemRenderer } = $props();
</script>
{#each items as item}
{@render itemRenderer?.(item)}
{/each}SvelteKit Patterns Reference
Table of Contents
---
Load Function Types
SvelteKit has two load function types: universal (+page.js) and server-only (+page.server.ts).
When to Use Each
| Use +page.server.ts | Use +page.js |
|---|---|
| Accessing secrets/credentials | Public APIs only |
| Database connections | Non-serializable data (functions, classes) |
| Server-only APIs | Client-side caching benefits |
| Sensitive business logic |
Security: Secrets in Server Load
WRONG (secrets exposed):
// +page.js - DANGEROUS: runs in browser!
export const load = async ({ fetch }) => {
const response = await fetch('https://api.stripe.com/charges', {
headers: { 'Authorization': `Bearer ${STRIPE_SECRET_KEY}` } // EXPOSED!
});
return { charges: await response.json() };
};CORRECT:
// +page.server.ts - only runs on server
import { STRIPE_SECRET_KEY } from '$env/static/private';
export const load = async ({ fetch }) => {
const response = await fetch('https://api.stripe.com/charges', {
headers: { 'Authorization': `Bearer ${STRIPE_SECRET_KEY}` }
});
return { charges: await response.json() };
};Serialization: Non-serializable in Universal Load
WRONG (server load can't serialize functions):
// +page.server.ts
export const load = async () => {
return {
formatDate: (date: Date) => date.toLocaleDateString(), // ERROR
parser: new DOMParser() // ERROR
};
};CORRECT:
// +page.js
export const load = async () => {
return {
formatDate: (date: Date) => date.toLocaleDateString(), // OK
parser: typeof window !== 'undefined' ? new DOMParser() : null
};
};Database Access
// +page.server.ts - CORRECT
import { db } from '$lib/server/database';
export const load = async () => {
const users = await db.query('SELECT * FROM users');
return { users };
};Environment Variables
// +page.server.ts - Private env vars
import { DATABASE_URL, API_SECRET } from '$env/static/private';
// +page.js - Only public env vars
import { PUBLIC_API_URL } from '$env/static/public';---
Page Props Typing
SvelteKit generates types in ./$types for full type safety with $props().
Basic Page Data Typing
<!-- +page.svelte -->
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<h1>{data.title}</h1>Page Data with Form Actions
<script lang="ts">
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
</script>
{#if form?.success}
<p class="success">{form.message}</p>
{/if}
{#if form?.error}
<p class="error">{form.error}</p>
{/if}Layout Data Typing
<!-- +layout.svelte -->
<script lang="ts">
import type { LayoutProps } from './$types';
let { data, children }: LayoutProps = $props();
</script>
<nav>
{#if data.user}<span>Welcome, {data.user.name}</span>{/if}
</nav>
{@render children()}Corresponding Load Function
// +page.server.ts
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async () => {
return {
title: 'My Page',
items: await fetchItems()
};
};
export const actions: Actions = {
default: async ({ request }) => {
return { success: true, message: 'Saved successfully' };
}
};Error Page
<!-- +error.svelte -->
<script lang="ts">
import { page } from '$app/state';
</script>
<h1>{page.status}: {page.error?.message}</h1>---
Form Actions Error Handling
SvelteKit distinguishes between validation errors (fail()) and unexpected errors (throw error()).
Use fail() for Validation Errors
WRONG:
import { error } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
if (!email?.toString().includes('@')) {
throw error(400, 'Invalid email'); // Shows error page!
}
}
};CORRECT:
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email')?.toString() ?? '';
if (!email.includes('@')) {
return fail(400, {
error: 'Invalid email address',
email // Return for repopulation
});
}
return { success: true };
}
};Use throw error() for Unexpected Errors
import { fail, error } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
// Validation - use fail()
if (!data.get('title')) {
return fail(400, { error: 'Title is required' });
}
try {
await database.save(data);
return { success: true };
} catch (e) {
// Unexpected error - use throw
throw error(500, 'Unable to save. Please try again later.');
}
}
};Structured Validation Responses
interface FormErrors {
email?: string;
password?: string;
}
export const actions = {
register: async ({ request }) => {
const data = await request.formData();
const errors: FormErrors = {};
if (!email) errors.email = 'Email is required';
if (password.length < 8) errors.password = 'Password must be at least 8 characters';
if (Object.keys(errors).length > 0) {
return fail(400, { errors, values: { email } });
}
return { success: true };
}
};Handle in Component
<script lang="ts">
import type { PageProps } from './$types';
import { enhance } from '$app/forms';
let { form }: PageProps = $props();
</script>
<form method="POST" action="?/register" use:enhance>
<label>
Email
<input name="email" value={form?.values?.email ?? ''} />
{#if form?.errors?.email}
<span class="error">{form.errors.email}</span>
{/if}
</label>
<button type="submit">Register</button>
</form>Error Response Summary
| Situation | Function | Result |
|---|---|---|
| Missing field | fail(400, {...}) | Form state preserved |
| Invalid format | fail(400, {...}) | Form state preserved |
| Not found | throw error(404, ...) | Error page |
| Server crash | throw error(500, ...) | Error page |
| Auth required | throw redirect(303, ...) | Redirect |
---
SSR State Isolation
Shared server state persists across requests, potentially leaking data between users.
Dangerous: Module-Level State
WRONG:
// +page.server.ts
let currentUser = null; // SHARED ACROSS ALL REQUESTS!
export const load = async ({ locals }) => {
currentUser = locals.user; // User B overwrites User A
return { user: currentUser };
};CORRECT:
export const load = async ({ locals }) => {
return { user: locals.user }; // Each request gets its own locals
};Dangerous: Global Stores
WRONG:
// stores.svelte.ts
export const user = $state<User | null>(null); // Server-side singleton!CORRECT: Use locals and return data from load
// +layout.server.ts
export const load = async ({ locals }) => {
return { user: locals.user };
};Safe Patterns
Pattern 1: Use locals
// hooks.server.ts
export const handle = async ({ event, resolve }) => {
event.locals.user = await authenticate(event);
event.locals.requestId = crypto.randomUUID();
return resolve(event);
};
// +page.server.ts
export const load = async ({ locals }) => {
return { user: locals.user };
};Pattern 2: Context for Component Trees
<!-- +layout.svelte -->
<script lang="ts">
import { setContext } from 'svelte';
import type { LayoutProps } from './$types';
let { data, children }: LayoutProps = $props();
setContext('user', {
get current() { return data.user; }
});
</script>
{@render children()}Pattern 3: Client-Only State
// stores.svelte.ts
import { browser } from '$app/environment';
function createClientStore() {
if (!browser) return { value: null };
const state = $state({ value: null });
return state;
}
export const clientState = createClientStore();SSR Safety Checklist
- [ ] No module-level
letvariables that store user data - [ ] No global
$statethat gets set during SSR - [ ] No singleton service classes with mutable state
- [ ] All user-specific data flows through
locals - [ ] All page data comes from load function returns
Svelte 5 TypeScript Reference
Table of Contents
---
Props Typing
$props() requires specific patterns for TypeScript typing.
Basic Props Typing
Inline typing:
<script lang="ts">
let { name, count = 0 }: { name: string; count?: number } = $props();
</script>Interface typing (recommended):
<script lang="ts">
interface Props {
name: string;
count?: number;
disabled?: boolean;
}
let { name, count = 0, disabled = false }: Props = $props();
</script>Children and Snippets Typing
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet; // Required children
header?: Snippet; // Optional snippet
row: Snippet<[data: RowData]>; // Snippet with parameter
cell?: Snippet<[value: string, index: number]>; // Multiple params
}
interface RowData { id: number; name: string; }
let { children, header, row, cell }: Props = $props();
</script>Callback Props Typing
<script lang="ts">
interface Props {
value: string;
onchange?: (value: string) => void;
onsubmit?: (data: FormData) => Promise<void>;
onclick?: (event: MouseEvent) => void;
}
let { value, onchange, onsubmit, onclick }: Props = $props();
</script>Rest Props with HTML Attributes
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
variant?: 'primary' | 'secondary';
loading?: boolean;
}
let { variant = 'primary', loading = false, ...rest }: Props = $props();
</script>
<button class={variant} disabled={loading} {...rest}>
{#if loading}Loading...{:else}{@render children?.()}{/if}
</button>Input Element Props
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props extends Omit<HTMLInputAttributes, 'value'> {
value?: string;
label: string;
error?: string;
}
let { value = $bindable(''), label, error, ...rest }: Props = $props();
</script>Union Type Props
<script lang="ts">
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface Props {
variant?: ButtonVariant;
size?: ButtonSize;
children: Snippet;
}
let { variant = 'primary', size = 'md', children }: Props = $props();
</script>
<button class="{variant} {size}">{@render children()}</button>Discriminated Union Props
<script lang="ts">
type Props =
| { type: 'link'; href: string; children: Snippet }
| { type: 'button'; onclick: () => void; children: Snippet };
let props: Props = $props();
</script>
{#if props.type === 'link'}
<a href={props.href}>{@render props.children()}</a>
{:else}
<button onclick={props.onclick}>{@render props.children()}</button>
{/if}---
Generic Components
Svelte 5 uses the generics attribute for type-safe reusable components.
Basic Generic Component
<!-- List.svelte -->
<script lang="ts" generics="T">
interface Props {
items: T[];
children: import('svelte').Snippet<[item: T, index: number]>;
}
let { items, children }: Props = $props();
</script>
<ul>
{#each items as item, index}
<li>{@render children(item, index)}</li>
{/each}
</ul>
<!-- Usage -->
<script lang="ts">
import List from './List.svelte';
interface User { id: number; name: string; }
let users: User[] = $state([{ id: 1, name: 'Alice' }]);
</script>
<List items={users}>
{#snippet children(user, index)}
<span>{index + 1}. {user.name}</span> <!-- Fully typed! -->
{/snippet}
</List>Multiple Type Parameters
<script lang="ts" generics="K, V">
interface Props {
entries: [K, V][];
renderKey: import('svelte').Snippet<[key: K]>;
renderValue: import('svelte').Snippet<[value: V]>;
}
let { entries, renderKey, renderValue }: Props = $props();
</script>
<dl>
{#each entries as [key, value]}
<dt>{@render renderKey(key)}</dt>
<dd>{@render renderValue(value)}</dd>
{/each}
</dl>Constrained Generics
<script lang="ts" generics="T extends { id: string | number }">
interface Props {
items: T[];
selected?: T;
onselect?: (item: T) => void;
children: import('svelte').Snippet<[item: T, isSelected: boolean]>;
}
let { items, selected, onselect, children }: Props = $props();
</script>
{#each items as item}
<div
class:selected={selected?.id === item.id}
onclick={() => onselect?.(item)}
>
{@render children(item, selected?.id === item.id)}
</div>
{/each}Generic with Default Type
<script lang="ts" generics="T = Record<string, unknown>">
interface Props {
data: T[];
columns: (keyof T)[];
}
let { data, columns }: Props = $props();
</script>Generic Select Component
<script lang="ts" generics="T">
interface Props {
options: T[];
value?: T;
getLabel: (option: T) => string;
getValue: (option: T) => string;
onchange?: (selected: T | undefined) => void;
placeholder?: string;
}
let {
options,
value = $bindable(),
getLabel,
getValue,
onchange,
placeholder = 'Select...'
}: Props = $props();
function handleChange(e: Event) {
const selectedValue = (e.target as HTMLSelectElement).value;
const selected = options.find(o => getValue(o) === selectedValue);
value = selected;
onchange?.(selected);
}
</script>
<select onchange={handleChange}>
<option value="">{placeholder}</option>
{#each options as option}
<option
value={getValue(option)}
selected={value && getValue(value) === getValue(option)}
>
{getLabel(option)}
</option>
{/each}
</select>Generic Async Component
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
interface Props {
promise: Promise<T>;
loading?: Snippet;
error?: Snippet<[error: Error]>;
children: Snippet<[data: T]>;
}
let { promise, loading, error, children }: Props = $props();
</script>
{#await promise}
{#if loading}{@render loading()}{:else}<p>Loading...</p>{/if}
{:then data}
{@render children(data)}
{:catch err}
{#if error}{@render error(err)}{:else}<p>Error: {err.message}</p>{/if}
{/await}Related skills
How it compares
svelte5-best-practices covers Svelte 5 runes and SvelteKit, not generic React or Vue patterns.
FAQ
Who is svelte5-best-practices for?
Frontend developers writing or reviewing Svelte 5 components and SvelteKit applications.
When should I use svelte5-best-practices?
When using runes, migrating from Svelte 4, implementing snippets, or optimizing SvelteKit load functions.
Is svelte5-best-practices safe to install?
Review the Security Audits panel on this page before installing in production.