Svelte 5 Runes Conventions
Svelte 5 rules for $state/$derived/$effect discipline, migrating legacy stores, snippets over slots, and typed component props.
Use when: Use in Svelte 5 projects to enforce runes-first reactivity and stop legacy $:, export let, and store patterns from creeping into new code.
The skill file
Cursor · .cursorrules.cursorrules3895 chars
You are working in a Svelte 5 + TypeScript project. Runes mode only — legacy syntax (`export let`, `$:` reactive statements, `$$props`, `on:click`) must not appear in new code; when touching a legacy component, migrate it rather than mixing modes.
## $state / $derived discipline
- `$state` is for values that change over time and are owned by this component. Everything computable from other state is `$derived` — never a second `$state` kept in sync manually.
- `$derived(expression)` for one-liners; `$derived.by(() => { ... })` once logic needs statements. If a `$derived.by` exceeds ~15 lines, extract a named pure function and derive from its call.
- `$effect` is a last resort, only for synchronizing with the outside world: DOM measurement, third-party libraries, `localStorage`, analytics. An `$effect` that assigns one piece of state from another is always wrong — use `$derived`.
- Effects that set up subscriptions/timers return a teardown function. Untracked reads inside effects use `untrack()` explicitly, with a comment — never rely on accidental non-tracking.
- Deep reactivity: `$state` on objects/arrays is proxied — mutate directly (`items.push(item)`). Do not clone-and-replace (`items = [...items, item]`) out of React habit; both work, mutation is the house style.
- Cross-component shared state lives in a `.svelte.ts` module exporting `$state`-based objects with mutation functions (`export const session = $state({...})`). Export functions to mutate; never export bare setters of internals.
## Stores migration
- New shared state never uses `writable`/`readable`. Replace store modules with `.svelte.ts` rune modules as you touch them.
- Exception: keep a store when a contract requires the subscribe interface — SvelteKit's `page` on older versions or third-party libraries. Bridge at the edge: read the store with `$` once, put the result into rune state, and keep stores out of component logic.
- `derived()` store chains are tech debt: collapse into `$derived` in the consuming module during migration.
## Component API
- Props via `let { items, onselect, dense = false } = $props()` with an explicit interface: `interface Props { items: Item[]; onselect?: (id: string) => void; dense?: boolean }` and `$props<Props>()` typing (or satisfies pattern the codebase uses). Never `export let`.
- Two-way binding is opt-in and explicit: `let { value = $bindable("") } = $props()`. Limit `$bindable` to form-like leaf components; data flows down, events flow up everywhere else.
- Events are callback props (`onselect`, `onclose`) — `createEventDispatcher` is banned in runes mode. Native event attributes use the new syntax: `onclick={handleClick}`, not `on:click`.
- Children render via snippets: `let { children, header } = $props()` and `{@render children?.()}`. No new `<slot>` usage; named slots become named snippet props.
- Side-effect-free formatting belongs in plain `.ts` utilities, not components. A component over ~150 lines or with 8+ props gets split.
## Props passthrough and attributes
- Forward unknown attributes with `let { class: className, ...rest } = $props()` and spread `{...rest}` onto the root element of wrapper components; merge classes explicitly rather than overwriting.
- Conditional classes use the object/array `class` syntax (`class={["card", { active }]}`), not string concatenation in expressions.
## Misc
- `#each` blocks always key: `{#each items as item (item.id)}`. Never key by index.
- `$inspect(value)` for debugging during development, removed before commit — no `console.log` of `$state` proxies (they log as Proxy noise).
- SSR safety: no `window`/`document` at module top level; gate browser-only code in `$effect` (runs client-side only) or `browser` checks in SvelteKit.
- Derived values that feed templates more than once get a name (`const subtotal = $derived(...)`) instead of repeating the expression inline.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursorrules
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…