svelte-ninja
This skill should be used when the user mentions "Svelte", "SvelteKit", "runes", "$state", "$derived", "$effect", "$props", discusses component patterns, reactive state, routing, load functions, form actions, or needs help with Svelte/SvelteKit code. Addresses Svelte 5 patterns using runes, SvelteKit conventions, and best practices.
SKILL.md
| Name | svelte-ninja |
| Description | This skill should be used when the user mentions "Svelte", "SvelteKit", "runes", "$state", "$derived", "$effect", "$props", discusses component patterns, reactive state, routing, load functions, form actions, or needs help with Svelte/SvelteKit code. Addresses Svelte 5 patterns using runes, SvelteKit conventions, and best practices. |
name: svelte-ninja description: This skill should be used when the user mentions "Svelte", "SvelteKit", "runes", "$state", "$derived", "$effect", "$props", discusses component patterns, reactive state, routing, load functions, form actions, or needs help with Svelte/SvelteKit code. Addresses Svelte 5 patterns using runes, SvelteKit conventions, and best practices. version: 1.0.0
Svelte/SvelteKit Patterns
Comprehensive guide to Svelte 5 and SvelteKit development patterns. Emphasizes runes-based reactivity ($state, $derived, $effect, $props), component composition, SvelteKit routing, data loading, form handling, and performance optimization.
When This Skill Applies
Use this skill when:
- Building Svelte components
- Managing reactive state with runes
- Implementing SvelteKit routes and pages
- Creating load functions or form actions
- Handling component composition
- Optimizing Svelte/SvelteKit performance
- Questions about Svelte 5 patterns or SvelteKit conventions
Svelte 5 Fundamentals
Runes Overview
Runes are compiler instructions (marked with $) that enable explicit reactivity:
$state- Reactive state$derived- Computed values$effect- Side effects$props- Component props$bindable- Two-way bindable props
Key principle: Reactivity is explicit, not implicit. Works in .js, .ts, and .svelte files.
Reactive State with $state
Basic State
<script>
let count = $state(0);
function increment() {
count++; // Just a number, no wrapper needed
}
</script>
<button onclick={increment}>
Clicks: {count}
</button>
Key points:
$statecreates reactive state- State is the value itself (not
.valueorgetCount()) - Updates trigger UI re-renders
Deep Reactivity
<script>
let todos = $state([
{ id: 1, text: 'Learn Svelte 5', done: false }
]);
function toggle(id) {
const todo = todos.find(t => t.id === id);
todo.done = !todo.done; // Deep reactivity works
}
function addTodo(text) {
todos.push({ id: Date.now(), text, done: false });
// Array methods trigger reactivity
}
</script>
Deep reactivity:
- Objects and arrays are automatically proxied
- Nested mutations trigger updates
- Array methods (
.push,.splice, etc.) work reactively
State in .svelte.js Files
// counter.svelte.js
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get count() { return count; },
increment: () => count++,
reset: () => count = initial
};
}
<!-- App.svelte -->
<script>
import { createCounter } from './counter.svelte.js';
const counter = createCounter(5);
</script>
<button onclick={counter.increment}>
Count: {counter.count}
</button>
Benefits:
- Share state logic across components
- Testable outside Svelte components
- No store boilerplate needed
Derived State with $derived
Basic Derivations
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let isEven = $derived(count % 2 === 0);
</script>
<p>{count} doubled is {doubled}</p>
<p>Count is {isEven ? 'even' : 'odd'}</p>
Key points:
- Automatically recalculates when dependencies change
- Memoized (only recalculates when needed)
- Must be free of side-effects
Complex Derivations with $derived.by
<script>
let numbers = $state([1, 2, 3, 4, 5]);
let stats = $derived.by(() => {
const total = numbers.reduce((a, b) => a + b, 0);
const average = total / numbers.length;
return { total, average };
});
</script>
<p>Total: {stats.total}, Average: {stats.average}</p>
Use $derived.by when:
- Logic doesn't fit in short expression
- Need multiple statements
- Complex calculations required
$derived vs $effect
$derived - Computing values (pure, returns value):
<script>
let count = $state(0);
let doubled = $derived(count * 2); // ✓ Good
</script>
$effect - Side effects (impure, no return value):
<script>
let count = $state(0);
$effect(() => {
console.log('Count changed:', count); // ✓ Good
});
</script>
Side Effects with $effect
Basic Effects
<script>
let count = $state(0);
$effect(() => {
// Runs on mount and whenever count changes
document.title = `Count: ${count}`;
});
</script>
When $effect runs:
- Initially when component mounts
- Whenever dependencies change
- After DOM updates (unlike Svelte 4's
$:)
Effect Cleanup
<script>
let intervalId = $state(null);
let elapsed = $state(0);
$effect(() => {
const id = setInterval(() => {
elapsed++;
}, 1000);
// Cleanup runs when effect re-runs or component unmounts
return () => clearInterval(id);
});
</script>
$effect.pre (Before DOM Update)
<script>
let messages = $state([]);
let div;
$effect.pre(() => {
const isAtBottom =
div.scrollHeight - div.scrollTop === div.clientHeight;
if (isAtBottom) {
// After DOM updates, scroll to bottom
$effect(() => {
div.scrollTop = div.scrollHeight;
});
}
});
</script>
Common $effect Patterns
Local storage sync:
<script>
let preferences = $state(JSON.parse(
localStorage.getItem('prefs') || '{}'
));
$effect(() => {
localStorage.setItem('prefs', JSON.stringify(preferences));
});
</script>
API calls:
<script>
let userId = $state('123');
let user = $state(null);
$effect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => user = data);
});
</script>
Avoid $effect for:
- Derived values (use
$derived) - DOM manipulation Svelte handles (bindings, directives)
- Setting document.title (use
<svelte:head>)
Component Props with $props
Basic Props
<!-- Button.svelte -->
<script>
let { label, variant = 'primary' } = $props();
</script>
<button class="btn btn--{variant}">
{label}
</button>
Usage:
<Button label="Click me" variant="secondary" />
Props with TypeScript
<script lang="ts">
interface Props {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
onclick?: () => void;
}
let { label, variant = 'primary', onclick }: Props = $props();
</script>
<button class="btn btn--{variant}" {onclick}>
{label}
</button>
Rest Props
<script>
let { label, ...rest } = $props();
</script>
<button {...rest}>
{label}
</button>
Bindable Props with $bindable
<!-- Input.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input bind:value />
Usage:
<script>
let text = $state('');
</script>
<Input bind:value={text} />
<p>You typed: {text}</p>
Component Patterns
Slot Patterns
Basic slot:
<!-- Card.svelte -->
<div class="card">
<slot />
</div>
Named slots:
<!-- Modal.svelte -->
<div class="modal">
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
Usage:
<Modal>
<svelte:fragment slot="header">
<h2>Title</h2>
</svelte:fragment>
<p>Content here</p>
<svelte:fragment slot="footer">
<button>Close</button>
</svelte:fragment>
</Modal>
Slot props:
<!-- List.svelte -->
<script>
let { items } = $props();
</script>
<ul>
{#each items as item}
<li>
<slot {item} />
</li>
{/each}
</ul>
Usage:
<List items={users}>
{#snippet children({ item })}
<strong>{item.name}</strong>
{/snippet}
</List>
Composition Patterns
Compound components:
<!-- Tabs.svelte -->
<script>
let { children } = $props();
let activeTab = $state(0);
export function setActive(index) {
activeTab = index;
}
</script>
<div class="tabs">
{@render children({ activeTab, setActive })}
</div>
Higher-order components:
// withAuth.js
export function withAuth(Component) {
return (props) => {
const { user } = useAuth();
if (!user) return 'Please log in';
return new Component({ ...props, user });
};
}
SvelteKit Routing
File-based Routing
src/routes/
├── +page.svelte # /
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ └── [slug]/
│ └── +page.svelte # /blog/:slug
└── (app)/
├── dashboard/
│ └── +page.svelte # /dashboard
└── settings/
└── +page.svelte # /settings
Route groups (app): Don't affect URL, useful for layouts
Dynamic Routes
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
// src/routes/blog/[slug]/+page.js
export async function load({ params }) {
const post = await fetchPost(params.slug);
return { post };
}
Optional Parameters
src/routes/archive/[[year]]/[[month]]/+page.svelte
Matches:
/archive/archive/2024/archive/2024/12
Data Loading
+page.js Load Functions
// src/routes/blog/[slug]/+page.js
export async function load({ params, fetch }) {
const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json());
return {
post
};
}
Load function context:
params- Route parametersfetch- Enhanced fetch (credentials, relative URLs)url- URL objectroute- Route infoparent- Parent load data
+page.server.js Server Load
// src/routes/dashboard/+page.server.js
import { db } from '$lib/server/database';
export async function load({ locals }) {
const user = locals.user;
const stats = await db.query('SELECT * FROM stats WHERE user_id = $1', [user.id]);
return {
stats
};
}
Server-only:
- Access to database
- Access to environment variables
- Runs on server, never exposed to client
Streaming with Promises
// +page.js
export async function load({ fetch }) {
const quick = fetch('/api/quick').then(r => r.json());
const slow = fetch('/api/slow').then(r => r.json());
return {
quick: await quick, // Wait for this
slow // Stream this
};
}
<!-- +page.svelte -->
<script>
let { data } = $props();
</script>
<div>{data.quick.title}</div>
{#await data.slow}
<p>Loading...</p>
{:then slow}
<p>{slow.content}</p>
{/await}
Form Actions
Basic Form Actions
// src/routes/login/+page.server.js
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
const user = await authenticate(email, password);
if (!user) {
return { success: false, error: 'Invalid credentials' };
}
cookies.set('session', user.sessionId, { path: '/' });
return { success: true };
}
};
<!-- src/routes/login/+page.svelte -->
<script>
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<form method="POST" use:enhance>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button>Log in</button>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
</form>
Named Actions
// +page.server.js
export const actions = {
create: async ({ request }) => {
// Handle create
},
update: async ({ request }) => {
// Handle update
},
delete: async ({ request }) => {
// Handle delete
}
};
<form method="POST" action="?/create">
<!-- create form -->
</form>
<form method="POST" action="?/update">
<!-- update form -->
</form>
Progressive Enhancement
<script>
import { enhance } from '$app/forms';
</script>
<form
method="POST"
use:enhance={({ formData, cancel }) => {
// Run before submission
if (!confirm('Are you sure?')) {
cancel();
}
return async ({ result, update }) => {
// Run after response
if (result.type === 'success') {
await update();
alert('Success!');
}
};
}}
>
<!-- form fields -->
</form>
Performance Optimization
Lazy Loading Components
<script>
import { onMount } from 'svelte';
let HeavyComponent;
onMount(async () => {
const module = await import('./HeavyComponent.svelte');
HeavyComponent = module.default;
});
</script>
{#if HeavyComponent}
<svelte:component this={HeavyComponent} />
{/if}
Virtualizing Long Lists
<script>
let items = $state(Array.from({ length: 10000 }, (_, i) => i));
let scrollTop = $state(0);
const itemHeight = 50;
const visibleCount = 20;
let visibleItems = $derived(() => {
const start = Math.floor(scrollTop / itemHeight);
return items.slice(start, start + visibleCount);
});
</script>
<div class="viewport" bind:scrollTop>
<div style="height: {items.length * itemHeight}px">
<div style="transform: translateY({Math.floor(scrollTop / itemHeight) * itemHeight}px)">
{#each visibleItems as item}
<div class="item">{item}</div>
{/each}
</div>
</div>
</div>
Memoizing Expensive Calculations
<script>
let data = $state([/* large dataset */]);
// Automatically memoized
let processed = $derived.by(() => {
return data
.filter(/* expensive filter */)
.map(/* expensive transform */)
.sort(/* expensive sort */);
});
</script>
Avoiding Unnecessary Re-renders
<script>
let todos = $state([
{ id: 1, text: 'Task 1', done: false }
]);
// Each todo is keyed, only changed todos re-render
</script>
{#each todos as todo (todo.id)}
<TodoItem {todo} />
{/each}
Common Patterns
Form Validation
<script>
let email = $state('');
let password = $state('');
let errors = $derived.by(() => {
const errs = {};
if (!email.includes('@')) errs.email = 'Invalid email';
if (password.length < 8) errs.password = 'Too short';
return errs;
});
let isValid = $derived(Object.keys(errors).length === 0);
</script>
<form>
<input bind:value={email} />
{#if errors.email}<span class="error">{errors.email}</span>{/if}
<input type="password" bind:value={password} />
{#if errors.password}<span class="error">{errors.password}</span>{/if}
<button disabled={!isValid}>Submit</button>
</form>
Modal Management
<script>
let isOpen = $state(false);
$effect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
});
</script>
<button onclick={() => isOpen = true}>Open Modal</button>
{#if isOpen}
<div class="modal-backdrop" onclick={() => isOpen = false}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<slot />
<button onclick={() => isOpen = false}>Close</button>
</div>
</div>
{/if}
Debounced Input
<script>
let searchQuery = $state('');
let debouncedQuery = $state('');
$effect(() => {
const timeout = setTimeout(() => {
debouncedQuery = searchQuery;
}, 300);
return () => clearTimeout(timeout);
});
// Use debouncedQuery for API calls
$effect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`);
}
});
</script>
<input bind:value={searchQuery} placeholder="Search..." />
Anti-Patterns
Don't: Use $effect for Derived Values
<!-- ✗ Bad -->
<script>
let count = $state(0);
let doubled = $state(0);
$effect(() => {
doubled = count * 2; // Wrong! Use $derived
});
</script>
<!-- ✓ Good -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
Don't: Mutate Props Directly
<!-- ✗ Bad -->
<script>
let { user } = $props();
function updateName() {
user.name = 'New Name'; // Wrong! Props are read-only
}
</script>
<!-- ✓ Good -->
<script>
let { user, onUpdate } = $props();
function updateName() {
onUpdate({ ...user, name: 'New Name' });
}
</script>
Don't: Create Unnecessary $state
<!-- ✗ Bad -->
<script>
let count = $state(0);
let doubled = $state(0);
let isEven = $state(false);
function increment() {
count++;
doubled = count * 2; // Derived!
isEven = count % 2 === 0; // Derived!
}
</script>
<!-- ✓ Good -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let isEven = $derived(count % 2 === 0);
</script>
Success Criteria
Svelte/SvelteKit code is well-structured when:
- Runes used appropriately ($state for state, $derived for computed, $effect for side effects)
- Components are composable and reusable
- Load functions fetch data efficiently
- Forms use progressive enhancement
- Performance optimized (lazy loading, memoization)
- TypeScript types are accurate
- Code is maintainable and follows Svelte 5 conventions