SvelteKit 2 & Svelte 5 (Runes) logo

SvelteKit 2 & Svelte 5 (Runes)

0

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.

10 rules

# 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` |
Add to Cursor