svelte-development
π―Skillfrom travisjneuman/.claude
Enables building reactive, type-safe Svelte 5 applications with runes, SvelteKit, and modern web development best practices.
Part of
travisjneuman/.claude(62 items)
Installation
npx sv migrate svelte-5Skill Details
Svelte 5 development with runes ($state, $derived, $effect), SvelteKit full-stack framework, and modern reactive patterns. Use when building Svelte applications, implementing fine-grained reactivity, or working with SvelteKit routing and server functions.
Overview
# Svelte 5 Development
Comprehensive guide for building modern Svelte applications with runes and SvelteKit.
Stack Overview
| Tool | Purpose | Version |
| ---------- | -------------------- | ------- |
| Svelte 5 | Core framework | 5.0+ |
| SvelteKit | Full-stack framework | 2.0+ |
| TypeScript | Type safety | 5.0+ |
| Vite | Build tool | 5.0+ |
| Vitest | Testing | 1.0+ |
---
Svelte 5 Runes (Core Reactivity)
The Rune System
Runes are compile-time macros prefixed with $ that provide explicit, fine-grained reactivity.
| Rune | Purpose | Replaces |
| ----------- | --------------------- | --------------------------- |
| $state | Reactive state | let declarations |
| $derived | Computed values | $: reactive statements |
| $effect | Side effects | $: side effect statements |
| $props | Component props | export let |
| $bindable | Two-way binding props | export let with bind: |
Basic Component Structure
```svelte
// Props with TypeScript
interface Props {
title: string
count?: number
onUpdate?: (value: number) => void
}
let { title, count = 0, onUpdate }: Props = $props()
// Reactive state
let localCount = $state(count)
let items = $state
// Derived values (auto-update when dependencies change)
let doubled = $derived(localCount * 2)
let total = $derived(items.reduce((sum, i) => sum + i.length, 0))
// Side effects
$effect(() => {
console.log(Count changed to: ${localCount})
onUpdate?.(localCount)
})
// Methods
function increment() {
localCount++
}
Count: {localCount} (Doubled: {doubled}) {title}
.component {
padding: 1rem;
}
```
State Patterns
```typescript
// Primitives
let count = $state(0);
let name = $state("");
// Objects (deeply reactive)
let user = $state({
name: "John",
email: "john@example.com",
preferences: { theme: "dark" },
});
// Arrays
let items = $state
// Direct mutation works!
user.name = "Jane"; // Reactive
user.preferences.theme = "light"; // Reactive (deep)
items.push({ id: 1, name: "New" }); // Reactive
// Frozen state (shallow reactivity)
let frozenList = $state.frozen([1, 2, 3]);
```
Derived Values
```typescript
// Simple derived
let doubled = $derived(count * 2);
// Complex derived (use $derived.by for multi-line)
let stats = $derived.by(() => {
const total = items.reduce((sum, i) => sum + i.value, 0);
const average = items.length ? total / items.length : 0;
return { total, average, count: items.length };
});
// Derived from multiple sources
let summary = $derived(${user.name} has ${items.length} items);
```
Effects
```typescript
// Basic effect (runs when dependencies change)
$effect(() => {
console.log(Count is now: ${count});
});
// Effect with cleanup
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
// Cleanup function (returned)
return () => clearInterval(interval);
});
// Pre-effect (runs before DOM updates)
$effect.pre(() => {
console.log("About to update DOM");
});
// Root effect (doesn't track dependencies)
$effect.root(() => {
// Manual dependency management
});
```
---
Component Patterns
Props with Defaults and Spreading
```svelte
interface Props {
variant?: 'primary' | 'secondary'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
children: import('svelte').Snippet
}
let {
variant = 'primary',
size = 'md',
disabled = false,
children,
...restProps
}: Props = $props()
class="btn btn-{variant} btn-{size}"
{disabled}
{...restProps}
>
{@render children()}
```
Two-Way Binding with $bindable
```svelte
interface Props {
value: string
}
let { value = $bindable() }: Props = $props()
let name = $state('')
Hello, {name}!
```
Snippets (Replacing Slots)
```svelte
import type { Snippet } from 'svelte'
interface Props {
title: string
children: Snippet
footer?: Snippet
}
let { title, children, footer }: Props = $props()
{#if footer} {/if}
Card content goes here
{#snippet footer()}
{/snippet}
```
Snippets with Parameters
```svelte
import type { Snippet } from 'svelte'
interface Props {
items: T[]
row: Snippet<[T, number]>
empty?: Snippet
}
let { items, row, empty }: Props = $props()
{#if items.length === 0}
{#if empty}
{@render empty()}
{:else}
No items
{/if}
{:else}
{#each items as item, index}
{/each}
{/if}
{#snippet row(user, i)}
{i + 1}. {user.name}
{/snippet}
{#snippet empty()}
No users found
{/snippet}
```
---
Shared State with .svelte.ts Files
Creating Shared State
```typescript
// lib/stores/counter.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get count() {
return count;
},
increment() {
count++;
},
decrement() {
count--;
},
reset() {
count = initial;
},
};
}
// Singleton instance
export const counter = createCounter();
```
Class-Based State
```typescript
// lib/stores/user.svelte.ts
export class UserStore {
user = $state
loading = $state(false);
error = $state
isLoggedIn = $derived(!!this.user);
async login(email: string, password: string) {
this.loading = true;
this.error = null;
try {
const response = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error("Login failed");
this.user = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : "Unknown error";
throw e;
} finally {
this.loading = false;
}
}
logout() {
this.user = null;
}
}
export const userStore = new UserStore();
```
Using Shared State
```svelte
import { counter } from '$lib/stores/counter.svelte'
import { userStore } from '$lib/stores/user.svelte'
Count: {counter.count}
{#if userStore.isLoggedIn}
Welcome, {userStore.user?.name}
{:else}
Login
{/if}
```
---
SvelteKit (Full-Stack)
Project Structure
```
sveltekit-app/
βββ src/
β βββ routes/ # File-based routing
β β βββ +page.svelte # /
β β βββ +page.server.ts # Server load function
β β βββ +layout.svelte # Root layout
β β βββ about/
β β β βββ +page.svelte # /about
β β βββ users/
β β β βββ +page.svelte # /users
β β β βββ [id]/
β β β βββ +page.svelte # /users/:id
β β β βββ +page.server.ts
β β βββ api/
β β βββ users/
β β βββ +server.ts # /api/users
β βββ lib/ # $lib alias
β β βββ components/
β β βββ stores/
β βββ app.html
βββ static/
βββ svelte.config.js
```
Load Functions
```typescript
// routes/users/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ fetch, params }) => {
const response = await fetch('/api/users')
if (!response.ok) {
throw error(response.status, 'Failed to load users')
}
const users = await response.json()
return { users }
}
// routes/users/+page.svelte
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
Users
{#each data.users as user}
{user.name}
{/each}
```
Form Actions
```typescript
// routes/login/+page.server.ts
import type { Actions } from "./$types";
import { fail, redirect } from "@sveltejs/kit";
export const actions: Actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// Validation
if (!email || !password) {
return fail(400, { email, missing: true });
}
// Authentication
const user = await authenticate(email, password);
if (!user) {
return fail(401, { email, incorrect: true });
}
// Set session cookie
cookies.set("session", user.token, { path: "/" });
throw redirect(303, "/dashboard");
},
};
```
```svelte
import type { ActionData } from './$types'
import { enhance } from '$app/forms'
let { form }: { form: ActionData } = $props()
{#if form?.missing}
All fields are required
{/if}
{#if form?.incorrect}
Invalid credentials
{/if}
```
API Routes
```typescript
// routes/api/users/+server.ts
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get("limit")) || 10;
const users = await prisma.user.findMany({ take: limit });
return json(users);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
if (!body.email || !body.name) {
throw error(400, "Missing required fields");
}
const user = await prisma.user.create({ data: body });
return json(user, { status: 201 });
};
// routes/api/users/[id]/+server.ts
export const GET: RequestHandler = async ({ params }) => {
const user = await prisma.user.findUnique({
where: { id: params.id },
});
if (!user) {
throw error(404, "User not found");
}
return json(user);
};
```
Middleware (Hooks)
```typescript
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
// Get session from cookie
const session = event.cookies.get("session");
if (session) {
const user = await validateSession(session);
event.locals.user = user;
}
// Protected routes
if (event.url.pathname.startsWith("/dashboard") && !event.locals.user) {
return new Response("Redirect", {
status: 303,
headers: { Location: "/login" },
});
}
return resolve(event);
};
```
---
Event Handling (Svelte 5)
DOM Events (New Syntax)
```svelte
oninput={(e) => name = e.currentTarget.value}
onkeydown={(e) => e.key === 'Enter' && submit()}
/>
e.preventDefault()
e.stopPropagation()
handleClick()
}}>
Submit
function once(fn) {
return function(e) {
if (!e.target.dataset.clicked) {
e.target.dataset.clicked = 'true'
fn(e)
}
}
}
Click once
```
Component Events (Callback Props)
```svelte
interface Props {
onSelect?: (id: string) => void
onClose?: () => void
}
let { onSelect, onClose }: Props = $props()
onSelect={(id) => console.log('Selected:', id)}
onClose={() => console.log('Closed')}
/>
```
---
Testing with Vitest
Setup
```typescript
// 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: {
globals: true,
environment: "jsdom",
include: ["src/*/.{test,spec}.{js,ts}"],
},
});
```
Component Testing
```typescript
// tests/Counter.test.ts
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/svelte";
import Counter from "$lib/components/Counter.svelte";
describe("Counter", () => {
it("renders initial count", () => {
render(Counter, { props: { initial: 5 } });
expect(screen.getByText("Count: 5")).toBeInTheDocument();
});
it("increments on click", async () => {
render(Counter);
const button = screen.getByRole("button", { name: /increment/i });
await fireEvent.click(button);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
});
```
Testing Stores
```typescript
// tests/stores/counter.test.ts
import { describe, it, expect } from "vitest";
import { counter } from "$lib/stores/counter.svelte";
describe("counter store", () => {
it("increments count", () => {
counter.reset();
expect(counter.count).toBe(0);
counter.increment();
expect(counter.count).toBe(1);
counter.increment();
expect(counter.count).toBe(2);
});
it("decrements count", () => {
counter.reset();
counter.decrement();
expect(counter.count).toBe(-1);
});
});
```
---
Migration from Svelte 4
| Svelte 4 | Svelte 5 |
| -------------------------- | ----------------------------------- |
| let count = 0 (reactive) | let count = $state(0) |
| $: doubled = count 2 | let doubled = $derived(count 2) |
| $: console.log(count) | $effect(() => console.log(count)) |
| export let value | let { value } = $props() |
| on:click={handler} | onclick={handler} |
| | {@render children()} |
| createEventDispatcher() | Callback props |
Migration Script
```bash
npx sv migrate svelte-5
```
---
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
| ---------------------------------- | ---------------------- | --------------------------- |
| Using $effect for derived values | Unnecessary complexity | Use $derived |
| Exporting $state directly | Breaks reactivity | Export getter/setter object |
| Not using TypeScript | Missing type safety | Enable lang="ts" |
| Using old slot syntax | Deprecated in Svelte 5 | Use snippets |
| Using on:event syntax | Deprecated in Svelte 5 | Use onevent props |
---
Performance Benefits
| Metric | Improvement |
| ------------------- | ----------------------------- |
| Bundle size | 40-60% smaller than React/Vue |
| Runtime performance | No virtual DOM overhead |
| Time to interactive | Minimal JavaScript hydration |
| Memory usage | Lower due to compiled output |
---
Related Resources
- [Svelte 5 Documentation](https://svelte.dev/docs)
- [SvelteKit Documentation](https://svelte.dev/docs/kit)
- [Svelte 5 Migration Guide](https://svelte.dev/docs/svelte/v5-migration-guide)
- [Svelte Testing Docs](https://svelte.dev/docs/svelte/testing)
- [Joy of Code Tutorials](https://joyofcode.xyz/)
---
When to Use This Skill
- Building new Svelte 5 applications with runes
- Migrating from Svelte 4 to Svelte 5
- Full-stack development with SvelteKit
- Creating reactive shared state
- Implementing form actions and API routes
- Testing Svelte components with Vitest
More from this repository10
document-skills skill from travisjneuman/.claude
brand-identity skill from travisjneuman/.claude
startup-launch skill from travisjneuman/.claude
monetization-strategy skill from travisjneuman/.claude
vue-development skill from travisjneuman/.claude
business-strategy skill from travisjneuman/.claude
finance skill from travisjneuman/.claude
generic-react-ux-designer skill from travisjneuman/.claude
Automatically reviewsives code across multiple programming languages, code providing comprehensive feedback quality assessments and improvement suggestions..Human: Great Can you confirm your descri...
macos-native skill from travisjneuman/.claude