SvelteKit 2 & Svelte 5 (Runes) SvelteKit 2 + Svelte 5 (Runes) rules for Cursor. Pinned to svelte ^5.55.7, @sveltejs/kit ^2.60.1, vite ^8.0.13, typescript ^6.0.3, vitest ^4.1.6, @testing-library/svelte ^5.3.1, @playwright/test ^1.60.0, tailwindcss ^4.3.0, bits-ui ^2.18.1, @lucide/svelte ^1.16.0, sveltekit-superforms ^2.30.1, formsnap ^2.0.1, zod ^4.4.3, @sveltejs/enhanced-img ^0.10.4, mode-watcher ^1.1.0, runed ^0.37.1. Leads with the Svelte 5 Runes that LLMs trained on Svelte 4 do not know ($state, $derived, $effect, $props, $bindable, $host, $inspect, $props.id()), the snippets-replace-slots model, callback-prop event handling that replaces createEventDispatcher, .svelte.ts state modules that replace writable() stores, the SvelteKit 2 no-throw error/redirect API, mandatory cookies path, and the $app/state replacement for $app/stores in 2.12+. Catches 40 LLM regressions including let count = 0 without $state, $: reactive statements in runes mode, export let, on:click event directives, event modifiers, createEventDispatcher, <slot>, new App({ target }), bind:this to non-state, destructured $state losing reactivity, $effect setting state to derive, $effect infinite loops, $state for huge arrays without $state.raw, throw error/redirect in SvelteKit 2, cookies.set without path, top-level unawaited promises in load, goto external, $page from $app/stores, resolvePath, use:enhance form/data renamed to formElement/formData, missing enctype on file forms, paths.relative default flip, $env/dynamic during prerender, writable() for component state, lucide-svelte instead of @lucide/svelte, melt-ui instead of bits-ui, Felte forms, raw <img> over <enhanced:img>, CustomEvent typed payloads, withDefaults-style prop defaults, manual ARIA IDs over $props.id(), {@render children} with <slot>, mutating non-$bindable props, +page.ts without satisfies PageLoad, mount(App) for tests.
Rules (10) Agents (1) Skills (5)
10 rules Svelte 5 events and snippets model: onclick / oninput / onsubmit are plain DOM attributes (no on: namespace, no event modifiers), createEventDispatcher is replaced by callback props (parent passes onsave={fn}, child calls onsave(payload)), <slot /> and <slot name='x' /> are replaced by snippets ({@render children?.()} for default, named snippet props for named), {#snippet name(arg)}...{/snippet} declares reusable template fragments, the Snippet<[T]> type from 'svelte' types snippet props. Companion to sveltekit-anti-patterns rule entries #4, #5, #6, #7, #34, #37. Copy# Svelte 5 Events and Snippets
Cursor: Svelte 5 dropped four Svelte 4 mechanisms in one go: the `on:event` directive namespace, event modifiers, `createEventDispatcher`, and the `<slot>` element. The replacements are plain DOM attributes, callback props, and snippets.
The companion `sveltekit-anti-patterns` rule rejects the Svelte 4 shapes (entries #4, #5, #6, #7, #34, #37). This rule explains the positive form.
## DOM event handlers: `onclick`, `oninput`, `onsubmit`
Svelte 5 uses lowercase DOM property names directly. They behave the same as a React `onClick` (camelCase aside) or a vanilla HTML attribute.
```svelte
<script lang="ts">
let count = $state(0)
const inc = () => count++
</script>
<button onclick={inc}>{count}</button>
<input oninput={(e) => console.log(e.currentTarget.value)} />
<form onsubmit={(e) => { e.preventDefault(); save() }}>...</form>
```
There is NO event modifier syntax. Handle it in the function:
```svelte
<a onclick={(e) => { e.stopPropagation(); openDialog() }}>Open</a>
```
For one-shot listeners, track it yourself:
```svelte
<script lang="ts">
let fired = $state(false)
function once(e: Event) {
if (fired) return
fired = true
save()
}
</script>
<button onclick={once}>Save once</button>
```
Capture phase: prefix with `on` and add `capture`:
```svelte
<button onclickcapture={handleCapture}>Capture</button>
```
## Component callback props (replaces `createEventDispatcher`)
The child accepts a function prop. The parent passes it. There is no `dispatch`, no `CustomEvent`, no `e.detail`.
```svelte
<!-- child: Picker.svelte -->
<script lang="ts">
type Props = { onselect: (id: string) => void }
let { onselect }: Props = $props()
function pick(id: string) {
onselect(id)
}
</script>
<button onclick={() => pick('a')}>A</button>
<button onclick={() => pick('b')}>B</button>
```
```svelte
<!-- parent -->
<Picker onselect={(id) => console.log('picked', id)} />
```
Optional callback, with optional chaining:
```svelte
<script lang="ts">
type Props = { onsave?: (value: string) => void }
let { onsave }: Props = $props()
</script>
<button onclick={() => onsave?.('hello')}>Save</button>
```
Multiple events:
```svelte
<!-- child -->
<script lang="ts">
type Props = {
onsave: (v: string) => void
oncancel: () => void
onchange?: (v: string) => void
}
let { onsave, oncancel, onchange }: Props = $props()
</script>
```
## Snippets replace `<slot>`
The default slot is a prop named `children`. Render with `{@render children?.()}`. Named slots are individual snippet props.
```svelte
<!-- child: Card.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
type Props = {
header?: Snippet
children?: Snippet
footer?: Snippet<[year: number]>
}
let { header, children, footer }: Props = $props()
</script>
<article class="card">
{#if header}
<header>{@render header()}</header>
{/if}
<div class="body">{@render children?.()}</div>
{#if footer}
<footer>{@render footer(2026)}</footer>
{/if}
</article>
```
```svelte
<!-- parent -->
<Card>
{#snippet header()}<h2>Title</h2>{/snippet}
<p>Body content here.</p>
{#snippet footer(year)}
<small>Copyright {year}</small>
{/snippet}
</Card>
```
## Snippet typing with `Snippet<[T1, T2, ...]>`
The `Snippet` generic type from `svelte` parameterises the tuple of snippet arguments.
```ts
import type { Snippet } from 'svelte'
type Props = {
rows: Row[]
// Snippet that takes one Row argument:
row: Snippet<[row: Row, index: number]>
// Snippet that takes no arguments:
empty?: Snippet
}
```
## Reusable snippets via `{#snippet}` blocks at template scope
Snippets can also be declared at the top level of a `<template>` and reused anywhere a snippet is expected.
```svelte
<script lang="ts">
let items = $state([
{ id: '1', label: 'A' },
{ id: '2', label: 'B' },
])
</script>
{#snippet row(label: string)}
<li class="row">{label}</li>
{/snippet}
<ul>
{#each items as item}
{@render row(item.label)}
{/each}
</ul>
```
## Why these matter for LLM output
Pre-October-2024 trained models will produce:
- `<button on:click={handle}>` (anti-pattern #4)
- `<form on:submit|preventDefault={save}>` (anti-pattern #5)
- `import { createEventDispatcher } from 'svelte'` (anti-pattern #6)
- `<slot />` and `<slot name="x" />` (anti-pattern #7)
- `function onSelect(e: CustomEvent<{ id: string }>)` (anti-pattern #34)
Each is the Svelte 4 documented form. Each compiles to a warning or error in Svelte 5 runes mode.
## Migration cheat sheet
| Svelte 4 | Svelte 5 |
| --------------------------------------------------- | ------------------------------------------------- |
| `<button on:click={fn}>` | `<button onclick={fn}>` |
| `<form on:submit\|preventDefault={fn}>` | `<form onsubmit={(e) => { e.preventDefault(); fn(e) }}>` |
| `dispatch('save', payload)` | `onsave?.(payload)` |
| `e.detail.foo` in parent handler | `(foo) => ...` plain function arg |
| `<slot />` | `{@render children?.()}` |
| `<slot name="header" />` | `{@render header?.()}` |
| `<svelte:fragment slot="header">...</...>` | `{#snippet header()}...{/snippet}` |
| `let:item={item}` slot prop | `{#snippet row(item)}...{/snippet}` + `{@render row(item)}` |
| `CustomEvent<{ id: string }>` | `(id: string) => void` |
Svelte 5 reactivity traps that LLMs hit: $effect runs only in the browser (never SSR; do not initialise SSR-visible state in it), reads inside $effect after the first await are NOT tracked (capture them synchronously up front), $effect that reads and writes the same $state creates an infinite loop (wrap reads in untrack from 'svelte'), $effect used to derive a value should be $derived instead, $state for arrays >1000 elements should be $state.raw with replacement instead of mutation, destructuring a $state proxy captures a snapshot (lose reactivity in the local), $bindable() required to mutate a prop in a child, bind:this targets must be $state to be readable as reactive. Companion to sveltekit-anti-patterns rule entries #9, #10, #11, #12, #13, #14, #15, #38. CopySvelte 5 Runes that LLMs trained pre-October 2024 do not know: $state for reactive locals (proxy-wrapped, mutate in place), $state.raw for large arrays / immutable replacement, $state.snapshot for serialisation, $derived for pure derivations, $derived.by for multi-line derivations, $effect for browser-only side effects, $effect.pre for pre-DOM-update timing, $effect.tracking for tracking introspection, $effect.root for manual effect roots, $props for typed component inputs, $props.id for SSR-safe instance IDs (5.20+), $bindable for opt-in two-way binding, $host for custom-element host element, $inspect for debug logging. Companion to sveltekit-anti-patterns rule entries #1, #2, #3, #11, #12, #13, #14, #15. CopySvelte 5 TypeScript canonical patterns: $props<T>() with inline type annotation OR exported type from sibling .ts (preferred for reuse), Snippet<[arg1, arg2]> tuple-arg generic from 'svelte' for typed snippets, Component<Props, Exports, Bindings> generic from 'svelte' for typing component references, satisfies PageLoad / PageServerLoad / LayoutLoad / Action for load and action exports, generics on components via <script lang='ts' generic='T extends ...'>, defineProps-style is NOT a Svelte concept (Vue), discriminated unions for variant props, type-narrowing on $bindable defaults. Companion to sveltekit-anti-patterns rule entries #34, #35, #39. CopySvelteKit 2 core changes from SvelteKit 1: error() and redirect() are no longer thrown by you (just call them, the helpers throw internally), cookies.set / cookies.delete require explicit path: '/' (no default), top-level promises in load are no longer auto-awaited (use await or Promise.all), goto() rejects external URLs (use window.location.href), $page from $app/stores deprecated in 2.12+ (use page from $app/state), resolvePath renamed resolveRoute in $app/paths, paths.relative default flipped from false to true, reroute hook for URL aliasing, shallow routing via pushState / replaceState from $app/navigation. Companion to sveltekit-anti-patterns rule entries #17, #18, #19, #20, #21, #22, #23, #26. Copy40 SvelteKit 2 + Svelte 5 (Runes) LLM regressions with BAD / GOOD pairs. Catches let count = 0 used as component state without $state (silently non-reactive in runes mode), $: reactive statements in runes-mode files, export let prop in any runes file, on:click and any on:event directive (use onclick), event modifiers |preventDefault / |stopPropagation / |self / |once (handle in the function), createEventDispatcher + dispatch (use callback props), <slot /> and <slot name> (use {@render children?.()} and snippets), component instantiation via new App({ target }) (use mount() / hydrate()), bind:this to a non-$state variable (loses reactivity), destructured $state proxy used as the destructured local (loses reactivity), setting $state inside $effect to derive a value (use $derived), $effect that reads and writes the same $state (infinite loop, wrap in untrack), $state for arrays larger than 1000 elements without considering $state.raw, $effect used to initialise state on the server (effects only run in the browser), reading state inside $effect after await (no longer tracked), importing from svelte/legacy (run, createEventDispatcher) in new code, throw error(...) / throw redirect(...) in SvelteKit 2 (just call them), wrapping redirect() in try/catch (swallows the redirect), cookies.set / cookies.delete without explicit path: '/', top-level unawaited promises in load (SvelteKit 2 requires explicit await / Promise.all), goto('https://external') (rejected in SvelteKit 2, use window.location.href), $page from $app/stores in new code (use page from $app/state, 2.12+), resolvePath (renamed resolveRoute in $app/paths), use:enhance callback args form / data renamed formElement / formData, form with file inputs missing enctype='multipart/form-data', paths.relative default flipped to true (declare explicitly if hardcoding), $env/dynamic/* read during prerender (use $env/static/*), writable() / readable() stores for component-local state (use $state), stores in .ts modules for cross-component shared state (use .svelte.ts with $state), lucide-svelte import (use @lucide/svelte the scoped Svelte 5 fork), melt-ui import (use bits-ui for Svelte 5), Felte for forms (effectively abandoned, use sveltekit-superforms + formsnap), <img> for above-the-fold images (use <enhanced:img>), component event payload typed via CustomEvent<T> (use callback-prop function signature), withDefaults-style prop defaults (Svelte 5 supports defaults inline in destructure), manually generating SSR-safe IDs for ARIA (use $props.id()), let { children } = $props() then <slot> (use {@render children?.()}), mutating non-$bindable props in a child, +page.ts exports without satisfies PageLoad, mount(App) without unmount on teardown / new App() for tests. Copy$app/state replaces $app/stores in @sveltejs/kit 2.12+. Plain reactive objects (page, navigating, updated) instead of stores; no $ prefix, no subscription, fine-grained reactivity through Svelte 5 runes. page.url, page.params, page.route, page.status, page.error, page.data, page.form, page.state. navigating.from, navigating.to, navigating.type, navigating.willUnload, navigating.delta, navigating.complete. updated.current. Works inside or outside <script> blocks. Companion to sveltekit-anti-patterns rule entry #22. Add to Cursor SvelteKit 2 forms and actions: form actions in +page.server.ts (default action or named export inside actions object), use:enhance for progressive enhancement (callback args renamed formElement / formData in v2, NOT form / data), enctype='multipart/form-data' required on forms with file inputs (throws under enhance otherwise), fail() for validation errors that re-render the form, redirect() / error() called without throw, sveltekit-superforms for type-safe Zod-validated forms with one-call client + server wiring, formsnap for form field components, zod schemas as the single source of truth for shape and messages. Companion to sveltekit-anti-patterns rule entries #19, #24, #25, #32. CopySvelteKit 2 routing and load functions: file-based routing under src/routes/ with +page.svelte / +layout.svelte / +error.svelte / +server.ts files, +page.ts (universal load runs on server AND client) vs +page.server.ts (server-only load with access to cookies / locals / private env), satisfies PageLoad / PageServerLoad / LayoutLoad for type inference into +page.svelte, +server.ts for non-page HTTP endpoints (GET, POST, PUT, PATCH, DELETE, OPTIONS exports), depends() for manual invalidation keys, parent() to read parent layout data, fetch() that proxies cookies + reuses SSR responses, route groups via (group) parens, route params via [param] / [...rest] / [[optional]] / [param=matcher]. Companion to sveltekit-anti-patterns rule entries #17, #18, #20, #21, #39. CopySvelteKit 2 + Svelte 5 testing stack: vitest 4 (default pool flipped to 'forks' from v3 'threads'), @testing-library/svelte 5 (render() / screen / fireEvent / cleanup; uses Svelte 5 mount() under the hood, NOT new App({ target })), happy-dom over jsdom for speed, @sveltejs/vite-plugin-svelte for the .svelte file resolver in vitest, mocks for $app/state and $app/navigation via vi.mock, @playwright/test 1.60 for E2E (testDir 'e2e/', baseURL via webServer, Playwright runs the built preview server), no Cypress. Companion to sveltekit-anti-patterns rule entry #40. Copy