Skip to content
Skill · Cursor.cursorrulesCoding

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.

@libraryNewcomer
0 upvotes0 forks0 comments

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 repo

Save this to your project or home directory so Cursor can load it.

path./.cursorrules
Discussion

What people are saying

Forks

0 forks of this skill
Loading forks…