Svelte 5 and SvelteKit rules for AI coding agents

Most Svelte code on the web predates runes, so agents keep writing export let, $: and on:click into Svelte 5 components. That syntax still compiles in legacy mode, but these rules forbid mixing it into runes components. They cover Svelte 5 with SvelteKit 2 and start by checking svelte in package.json.

The runes section shows the replacements: $state for state, $derived for computed values, $props() for props, callback props instead of createEventDispatcher. The SvelteKit sections separate server-only load functions, where database access and secrets belong, from universal loads that also run in the browser, and cover form actions that validate with a schema and return fail(400, { errors, values }) on bad input, plus authentication once in hooks.server.ts.

Folder: rules/sveltekit/ · sveltekit.mdc, CLAUDE.md, AGENTS.md

What the SvelteKit rules tell your agent

First rule of each section in sveltekit.mdc, quoted as shipped.

Mistakes these rules are written to prevent

Source: “Pitfalls to avoid”.

Checks the agent runs before it says done

Example commands from the SvelteKit file (swap in your repo's own):

Run npx svelte-check, the linter, vitest run, and vite build. Report the results, including svelte-check warnings.

Before and after: Svelte: migrate a component from Svelte 4 syntax to Svelte 5 runes

Prompt card 40 · Stack: Svelte 5, SvelteKit 2 · Type: Refactor · Best model: a frontier reasoning model

What the card is for

Convert a legacy-syntax component (export let, $:, on:click, createEventDispatcher, slots) to runes, choosing correctly between $derived and $effect, and keep its public API working for parents during the transition.

Worked example from the card

Before: TagPicker.svelte

<script>
  import { createEventDispatcher } from "svelte";
  export let tags = [];
  export let max = 3;
  let selected = [];
  const dispatch = createEventDispatcher();
  $: remaining = max - selected.length;
  $: if (selected.length === max) console.log("limit reached");
  function toggle(tag) {
    selected = selected.includes(tag) ? selected.filter(t => t !== tag) : [...selected, tag];
    dispatch("change", selected);
  }
</script>

{#each tags as tag}
  <button on:click={() => toggle(tag)} disabled={!selected.includes(tag) && remaining === 0}>{tag}</button>
{/each}
<slot name="footer" {remaining} />

Parent: <TagPicker {tags} on:change={e => save(e.detail)}><p slot="footer" let:remaining>{remaining} left</p></TagPicker>

Expected AI output (abridged)

| export let tags/max      | $props() with defaults                 |
| let selected             | $state([])                             |
| $: remaining             | $derived (pure computation)            |
| $: if (...) console.log  | $effect (side effect)                  |
| dispatch("change")       | onchange callback prop                 |
| on:click                 | onclick                                |
| named slot footer + let: | footer snippet prop taking remaining   |
| {#each} without key      | keyed by tag (tags are unique strings) |
Parents migrated in the same change: yes, so on:change becomes onchange.

After

<script lang="ts">
  import type { Snippet } from "svelte";

  let { tags = [], max = 3, onchange, footer }: {
    tags?: string[];
    max?: number;
    onchange?: (selected: string[]) => void;
    footer?: Snippet<[number]>;
  } = $props();

  let selected = $state<string[]>([]);
  const remaining = $derived(max - selected.length);

  $effect(() => {
    if (selected.length === max) console.log("limit reached");
  });

  function toggle(tag: string) {
    selected = selected.includes(tag) ? selected.filter(t => t !== tag) : [...selected, tag];
    onchange?.(selected);
  }
</script>

{#each tags as tag (tag)}
  <button onclick={() => toggle(tag)} disabled={!selected.includes(tag) && remaining === 0}>{tag}</button>
{/each}
{@render footer?.(remaining)}

Parent:

<TagPicker {tags} onchange={save}>
  {#snippet footer(remaining)}<p>{remaining} left</p>{/snippet}
</TagPicker>

Excerpt from card 40; the prompt itself is in the pack.

Using these rules in Cursor, Claude Code and AGENTS.md tools

Cursor

Put _base.mdc and sveltekit.mdc in .cursor/rules/. Globs: svelte.config.js, **/*.svelte, **/*.svelte.ts, **/*.svelte.js, src/routes/**/*.ts, src/lib/**/*.ts, src/hooks*.ts.

Claude Code

Root CLAUDE.md, base first. Path-scoped variant .claude/rules/sveltekit.md for **/*.svelte, **/*.svelte.ts, src/routes/**/*.ts, src/lib/**/*.ts; PostToolUse hook: npx prettier --write <file>.

AGENTS.md tools (Codex, Gemini CLI, Aider)

Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in the SvelteKit app root. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.

Related stacks

← All stacks