Svelte 5 replaces the implicit reactivity of Svelte 4 with explicit runes — compiler primitives that make reactivity transparent and composable. $state declares reactive variables. $derived computes values from state. $effect runs side effects when dependencies change. $props defines component inputs with full TypeScript inference. Snippets replace slots with a composable template primitive. Event attributes (onclick) replace the on:click directive. These changes enable fine-grained reactivity — only the DOM nodes depending on changed state update. Claude Code generates Svelte 5 components with runes, SvelteKit route handlers, form actions, and the TypeScript patterns for production Svelte applications.
CLAUDE.md for Svelte 5 Projects
## Svelte 5 Stack
- Version: svelte >= 5.0, @sveltejs/kit >= 2.10
- Runes: $state (reactive), $derived (computed), $effect (side effects), $props (component API)
- Events: onclick/oninput attributes — NOT on:click directives (deprecated)
- Snippets: {#snippet name()}{/snippet} + {@render name()} — replaces slots
- Stores: still work but prefer runes for local state; writable/readable for shared
- Forms: SvelteKit actions with use:enhance for progressive enhancement
- TypeScript: enable with lang="ts" on <script>, type $props with interface
Core Runes
<!-- src/lib/components/OrderList.svelte — Svelte 5 runes -->
<script lang="ts">
import { orderApi } from '$lib/api'
interface Order {
id: string
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
totalCents: number
createdAt: string
}
interface Props {
customerId: string
onOrderCancelled?: (orderId: string) => void
}
// $props: typed component inputs
let { customerId, onOrderCancelled }: Props = $props()
// $state: reactive mutable state
let orders = $state<Order[]>([])
let loading = $state(true)
let error = $state<string | null>(null)
let searchQuery = $state('')
let selectedStatus = $state<string>('all')
// $derived: computed from state, auto-updates
let filteredOrders = $derived(
orders.filter(order => {
const matchesSearch = order.id.includes(searchQuery.toLowerCase())
const matchesStatus = selectedStatus === 'all' || order.status === selectedStatus
return matchesSearch && matchesStatus
})
)
let totalSpent = $derived(
orders.reduce((sum, order) => sum + order.totalCents, 0) / 100
)
let pendingCount = $derived(
orders.filter(o => o.status === 'pending').length
)
// $effect: runs after render when dependencies change
$effect(() => {
// Re-fetches when customerId changes
const abortController = new AbortController()
loading = true
error = null
orderApi.list(customerId, { signal: abortController.signal })
.then(data => {
orders = data
loading = false
})
.catch(err => {
if (err.name !== 'AbortError') {
error = err.message
loading = false
}
})
// Cleanup: abort on re-run or unmount
return () => abortController.abort()
})
async function cancelOrder(orderId: string) {
await orderApi.cancel(orderId)
orders = orders.map(o =>
o.id === orderId ? { ...o, status: 'cancelled' } : o
)
onOrderCancelled?.(orderId)
}
</script>
<div class="order-list">
<div class="stats">
<span>Total: ${totalSpent.toFixed(2)}</span>
<span>{pendingCount} pending</span>
</div>
<div class="filters">
<!-- onclick attribute replaces on:click -->
<input
type="search"
placeholder="Search orders..."
bind:value={searchQuery}
/>
<select bind:value={selectedStatus}>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="shipped">Shipped</option>
</select>
</div>
{#if loading}
<p>Loading orders...</p>
{:else if error}
<p class="error">{error}</p>
{:else}
{#each filteredOrders as order (order.id)}
<div class="order-card">
<span class="id">{order.id.slice(-8)}</span>
<span class="status">{order.status}</span>
<span class="total">${(order.totalCents / 100).toFixed(2)}</span>
{#if order.status === 'pending'}
<button onclick={() => cancelOrder(order.id)}>Cancel</button>
{/if}
</div>
{/each}
{/if}
</div>
Snippets
<!-- src/lib/components/DataTable.svelte — snippets replace slots -->
<script lang="ts" generics="T">
interface Props<T> {
items: T[]
getKey: (item: T) => string
// Snippet type: takes T, returns nothing (renderable)
row: Snippet<[T]>
emptyState?: Snippet
header?: Snippet
}
import type { Snippet } from 'svelte'
let {
items,
getKey,
row,
emptyState,
header,
}: Props<T> = $props()
</script>
<table>
{#if header}
<thead>{@render header()}</thead>
{/if}
<tbody>
{#if items.length === 0}
{#if emptyState}
{@render emptyState()}
{:else}
<tr><td>No items</td></tr>
{/if}
{:else}
{#each items as item (getKey(item))}
{@render row(item)}
{/each}
{/if}
</tbody>
</table>
<!-- Usage: pass snippets as children -->
<DataTable {items} getKey={o => o.id}>
{#snippet header()}
<tr><th>ID</th><th>Status</th><th>Total</th></tr>
{/snippet}
{#snippet row(order)}
<tr>
<td>{order.id.slice(-8)}</td>
<td>{order.status}</td>
<td>${(order.totalCents / 100).toFixed(2)}</td>
</tr>
{/snippet}
{#snippet emptyState()}
<tr><td colspan="3">No orders found</td></tr>
{/snippet}
</DataTable>
Universal State with Classes
// src/lib/stores/cart.svelte.ts — class-based reactive state
class CartStore {
items = $state<CartItem[]>([])
couponCode = $state<string | null>(null)
couponDiscount = $state(0)
// $derived inside class
subtotalCents = $derived(
this.items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0)
)
discountCents = $derived(
Math.round(this.subtotalCents * this.couponDiscount)
)
totalCents = $derived(
this.subtotalCents - this.discountCents
)
itemCount = $derived(
this.items.reduce((sum, item) => sum + item.quantity, 0)
)
addItem(product: Product, quantity = 1) {
const existing = this.items.find(i => i.productId === product.id)
if (existing) {
this.items = this.items.map(i =>
i.productId === product.id
? { ...i, quantity: i.quantity + quantity }
: i
)
} else {
this.items = [...this.items, {
productId: product.id,
name: product.name,
priceCents: product.priceCents,
quantity,
}]
}
}
removeItem(productId: string) {
this.items = this.items.filter(i => i.productId !== productId)
}
async applyCoupon(code: string) {
const result = await fetch(`/api/coupons/${code}`)
if (!result.ok) throw new Error('Invalid coupon')
const { discount } = await result.json()
this.couponCode = code
this.couponDiscount = discount
}
clear() {
this.items = []
this.couponCode = null
this.couponDiscount = 0
}
}
// Singleton exported as module — shared across components
export const cart = new CartStore()
SvelteKit Form Actions
// src/routes/checkout/+page.server.ts
import type { Actions, PageServerLoad } from './$types'
import { fail, redirect } from '@sveltejs/kit'
import { z } from 'zod'
export const load: PageServerLoad = async ({ locals }) => {
const session = await locals.auth.getSession()
if (!session) throw redirect(302, '/login')
return { user: session.user }
}
const CheckoutSchema = z.object({
items: z.string().transform(s => JSON.parse(s)),
line1: z.string().min(1),
city: z.string().min(1),
country: z.string().length(2),
postalCode: z.string().min(1),
})
export const actions: Actions = {
placeOrder: async ({ request, locals }) => {
const session = await locals.auth.getSession()
if (!session) return fail(401, { error: 'Unauthorized' })
const formData = Object.fromEntries(await request.formData())
const parsed = CheckoutSchema.safeParse(formData)
if (!parsed.success) {
return fail(422, {
errors: parsed.error.flatten().fieldErrors,
values: formData,
})
}
const { items, ...address } = parsed.data
const order = await orderService.create({
customerId: session.user.id,
items,
shippingAddress: address,
})
throw redirect(303, `/orders/${order.id}/confirmation`)
},
}
<!-- src/routes/checkout/+page.svelte — progressive enhancement -->
<script lang="ts">
import { enhance } from '$app/forms'
import type { ActionData, PageData } from './$types'
let { data, form }: { data: PageData; form: ActionData } = $props()
let submitting = $state(false)
</script>
<form
method="POST"
action="?/placeOrder"
use:enhance={() => {
submitting = true
return async ({ update }) => {
await update()
submitting = false
}
}}
>
<input type="hidden" name="items" value={JSON.stringify(cartItems)} />
<div>
<label>
Address
<input
name="line1"
value={form?.values?.line1 ?? ''}
class:error={form?.errors?.line1}
/>
{#if form?.errors?.line1}
<span class="error">{form.errors.line1[0]}</span>
{/if}
</label>
</div>
<button type="submit" disabled={submitting}>
{submitting ? 'Placing order...' : 'Place order'}
</button>
</form>
For the React 19 equivalent with server components and actions providing similar progressive enhancement, see the Next.js App Router guide for server actions. For the SolidJS fine-grained reactivity model that inspired Svelte 5 runes, the SolidJS guide covers signals and effects. The Claude Skills 360 bundle includes Svelte 5 skill sets covering runes, snippets, and SvelteKit form actions. Start with the free tier to try Svelte 5 component generation.