Browser

Svelte

Build, debug, review, and migrate Svelte and SvelteKit applications.

What it does

Build, review, and troubleshoot Svelte 5 components and SvelteKit routes using runes, snippets, load functions, form actions, SSR, and adapters. Diagnose stale UI, effect loops, hydration mismatches, server-state leaks, browser-only API failures, invalidation gaps, TypeScript issues, and deployment errors. Migrate legacy Svelte 4 patterns and produce guidance tailored to syntax mode, adapter, package manager, styling, and TypeScript settings.

When to use it

  • Debugging stale UI or effect loops
  • Migrating Svelte 4 components to runes
  • Fixing SvelteKit SSR and form actions
  • Reviewing adapter builds and deployment

The skill document

User preferences and memory live in ~/Clawic/data/svelte/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/svelte/ or ~/clawic/svelte/), move it to ~/Clawic/data/svelte/.

When To Use

  • Writing or reviewing Svelte components, .svelte.js state modules, or SvelteKit routes
  • Reactivity failures: UI not updating, effect loops, bindings that stop propagating, stale derived values
  • SvelteKit request-layer work: load functions, invalidation, form actions, hooks, +server.js endpoints
  • Server-only failures: hydration mismatch, window is not defined, state shared between requests, adapter build errors
  • Migrating a Svelte 4 codebase to Svelte 5 runes, or maintaining a mixed legacy/runes codebase
  • Not for Vue or Nuxt (vue, nuxt), React (react), or language-level JavaScript semantics (javascript)

Quick Reference

SituationPlay
UI does not update after a changeIs the variable $state? Destructured? A Map/Set/class instance? → Reactivity Model table, then debug.md
effect_update_depth_exceededAn effect writes state it also reads — convert to $derived, or untrack() the read (rule 2)
Value computed from other state$derived / $derived.by; never $effect + assignment (rule 2)
Deep dive on $state, $derived, $effect, $propsrunes.md
Shared state across files, context, legacy storesstores.md
Snippets, bindings, callback props, attachments, wrapping an imperative librarycomponents.md
Scoped CSS not applying, unused-selector warning, transitions, motionstyling.md
Typing props, snippets, route data, app.d.tstypescript.md
Symptom-first debugging and the error-code catalogdebug.md
Svelte 4 → 5: export let, $:, slots, on:click, new Component()migration.md
Routes, layouts, groups, param matchers, navigation, shallow routingrouting.md
load, invalidate, streaming, depends, serializationdata-loading.md
Form actions, use:enhance, validation, uploads, remote functionsforms.md
window is not defined, hydration mismatch, per-request state, env varsssr.md
Login, sessions, cookies, route guards, roles, CSRF, XSS, CSP, leaked secretssecurity.md
Bundle size, slow lists, rerender cost, preloadingperformance.md
Vitest, component tests, mocking $app/*, Playwright, svelte-checktesting.md
Adapters, prerendering, CSP, service workers, self-hostingdeployment.md
Packaging components for npm, custom elements, library API designlibrary.md
Anything elseApply Core Rules; reproduce in a single component with no props before blaming the framework

Core Rules

  1. Reactivity comes from $state, not from assignment. In runes mode a bare let count = 0 never re-renders no matter how you assign it — the compiler emits non_reactive_update. let count = $state(0) then count++ or obj.items.push(x) both work: $state on a plain object or array is a deep proxy.
  2. $derived for values, $effect for the outside world. Decision rule: if the new value is a pure function of other state → $derived; if it touches the DOM, network, storage, or a non-Svelte library → $effect. An effect that assigns state costs an extra render pass and is the direct cause of effect_update_depth_exceeded.
  3. Effects track only synchronous reads. Dependencies are collected while the effect body runs to its first await; anything read after an await, in a setTimeout, or in a .then() is invisible to the tracker and the effect will not rerun. Read your dependencies at the top, then await.
  4. Module-level state on the server belongs to every user at once. A let user = $state(null) exported from a .svelte.js module is one value per Node process, not per request — request A writes it, request B renders it. Per-request data goes in event.locals (server) and setContext/props (components).
  5. Key every {#each} whose items can move. {#each rows as row (row.id)}. Unkeyed, Svelte maps DOM to array index: delete row 0 and the input, focus, and component state of index 0 stay attached to what is now a different row. This is a correctness bug, not an optimization.
  6. Secrets and the database live behind +page.server.js. Universal +page.js runs in the browser too, so anything it imports ships. Server load output crosses the wire through devalue serialization: Date, Map, Set, BigInt, and cycles survive; class instances and functions do not (register them with the transport hook).
  7. A load function reruns only for what it read. Rerun happens when: a params or url property it accessed changes, OR an invalidate(key) matches something it depends() on or a URL it fetched, OR a parent it await parent()-ed reran, OR invalidateAll() fired. After any mutation outside use:enhance, call invalidateAll() yourself or the page keeps serving the old data.
  8. Mutations go through form actions, not fetch handlers. `` works with JavaScript disabled and on the first paint; use:enhance upgrades the same form to a fetch with no reload and reinvalidates data on success. Reach for a +server.js endpoint only for non-form clients: webhooks, third parties, file streaming.
  9. Guard browser APIs by phase, not by try/catch. $effect and onMount never run during SSR — that is the guard. Module scope and load functions run on the server, so window, document, localStorage, and IntersectionObserver there need import { browser } from '$app/environment' or a dynamic import().

Reactivity Model

What is tracked, and what silently is not:

DeclarationReactive onRule
let x = $state(0)reassignmentPrimitives: assign to update
let o = $state({a: {b: 1}})reassignment + deep mutationPlain objects/arrays become recursive proxies; o.a.b = 2 and arr.push() both work
$state.raw(bigList)reassignment onlyNo proxy: cheaper for large data you replace wholesale
new Map(), Set, Date, URL inside $statenothingNot proxied — use SvelteMap, SvelteSet, SvelteDate, SvelteURL from svelte/reactivity
Class field count = $state(0)reassignmentMethods and get accessors stay reactive; the instance itself is not a proxy
let { a, b = 1 } = $props()parent updatesDestructuring props IS reactive in runes mode — the compiler rewrites the reads
const { a } = someStateObjectnothingDestructuring state reads the value once; keep the object, or wrap in $derived
Exported let x = $state(0) from a modulenot across the importImporters get a snapshot binding — export an object, a class instance, or a getter
let x = 0 in a runes filenothingCompiler warning non_reactive_update

Passing state to a non-Svelte library (structuredClone, IndexedDB, postMessage, charting libs) hands it a Proxy; send $state.snapshot(x) instead.

Version Gates

  • svelte >=5 — runes, snippets, onclick event attributes, mount()/unmount(); $:, export let, slots, and new Component() only work in legacy mode
  • svelte >=5.3 — `` with failed snippet and onerror
  • svelte >=5.29 — attachments ({@attach fn}) supersede actions (use:fn); actions still work
  • svelte >=5.36 — experimental await inside components and $effect.pending(), behind the experimental.async compiler option
  • @sveltejs/kit >=2error() and redirect() throw internally: call them, never throw them; cookies.set requires an explicit path
  • @sveltejs/kit >=2.12$app/state (page, navigating, updated) replaces the $app/stores subscriptions; page.url, not $page.url
  • @sveltejs/kit >=2.16PageProps and LayoutProps in ./$types; below it, type $props() with { data: PageData; form: ActionData }
  • @sveltejs/kit >=2.27 — remote functions (query, form, command) in .remote.js, behind experimental.remoteFunctions

Where Code Runs

FileRunsCan hold secretsShipped to browser
+page.svelte, +layout.svelteSSR render, then clientNoYes
+page.js (universal load)Server on first load, client on navigationNoYes
+page.server.js (server load, actions)Server onlyYesNo
+server.js (endpoint)Server onlyYesNo
hooks.server.jsServer, every requestYesNo
hooks.client.jsBrowser onlyNoYes
$lib/server/**, *.server.jsServer onlyYesImport from client code = build error

Order per request: handle hook → server loads (+layout.server.js, then +page.server.js) → universal loads → render. Loads at the same level run in parallel unless one calls await parent(), which serializes it behind the parent.

Error Codes

Code or messageMeaningFirst move
state_unsafe_mutationState written while a derived or the template was computing itMove the write into an event handler or $effect
effect_update_depth_exceededAn effect writes state that it (or a chained effect) readsConvert to $derived; if the read is genuinely one-way, wrap it in untrack()
hydration_mismatchServer HTML differs from the first client renderRemove Date.now()/Math.random()/window from render; check invalid nesting (inside)
rune_outside_svelteA rune used in a .js/.ts fileRename the file to .svelte.js / .svelte.ts
lifecycle_outside_componentonMount, setContext, or getContext called after an await or inside a callbackCall synchronously during component initialization
derived_references_selfA $derived reads its own valueCompute from source state, or keep the accumulator in $state
each_key_duplicateTwo items produced the same keyKey by a unique id; never by index or by a repeated value
bind_invalid_export / binding errorbind: to a prop the child did not declare bindablelet { value = $bindable() } = $props()
ownership_invalid_mutation (dev warning)A child mutated an object owned by its parent$bindable() or a callback prop
css_unused_selector (warning)Selector matches nothing in this component's own markup:global(...), or move the rule into the child component
403 on a form POSTKit's CSRF origin check rejected a cross-origin submissionSubmit same-origin, or set csrf.checkOrigin deliberately
Cannot import $lib/server/... into client-side codeA server-only module reached a client bundleImport it in +page.server.js and pass the result through load

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/svelte/config.yaml.

VariableTypeDefaultEffect
syntax_moderunes | legacy | mixedrunesSelects the syntax of every generated component ($state/$props vs let/export let) and whether migration advice is offered at all
kit_projectbooltrueWhen false, drops routing, load, actions, and adapter guidance and treats the app as Svelte + Vite with client-side routing
deployment_adapterauto | node | static | vercel | cloudflare | netlifyautoDrives the deploy checklist, which env-var mechanism is valid, and whether prerender/SPA fallback is required
package_managernpm | pnpm | yarn | bunnpmSets the command syntax in install, sv add, build, and test instructions
stylingscoped-css | tailwind | unocss | css-modulesscoped-cssShapes generated markup and which scoped-CSS caveats apply
typescriptbooltrueEmits ``, typed $props(), and ./$types imports; false switches to JSDoc annotations
experimental_featuresboolfalseWhen false, remote functions, experimental.async/$effect.pending(), and attachments-over-actions are mentioned as available but never the recommended form; when true they are written by default
check_thresholderror | warningerrorThe svelte-check --threshold value in every CI recipe, and whether a warning-level finding blocks the work

Preference areas to record as the user reveals them:

  • conventions — component and route file naming, folder layout, $lib structure, barrel files
  • stack — form validation library, ORM or data client, auth approach, i18n, component library
  • progressive enhancement — whether the app must work with JavaScript disabled; decides form-action vs client-fetch mutations
  • accessibility posture — treat compiler a11y warnings as errors, or advisory
  • testing strategy — component tests in browser mode vs jsdom, and the unit/e2e split
  • risk posture — appetite for experimental and just-released APIs beyond experimental_features, upgrade cadence, and whether to propose a migration on a codebase that currently works
  • budgets — bundle-size and first-load targets, coverage floor, and any perf number the project gates on beyond check_threshold
  • output format — explanation depth (fix only vs walkthrough), full files vs diffs, comment density in generated code
  • proactivity — how eagerly to flag reactivity, boundary, and bundle issues versus answering only what was asked

Output Gates

Before emitting a component or route, verify:

  • Every mutable value declared with $state; no bare let expected to rerender?
  • Every computed value a $derived, with $effect reserved for DOM, network, storage, or third-party libraries?
  • Every {#each} over reorderable data keyed by a stable id?
  • No secret, database client, or $env/static/private import reachable from +page.svelte or +page.js?
  • Mutations expressed as a form action that still works with JavaScript disabled?
  • Browser APIs only inside $effect/onMount or behind a browser check?
  • Per-request data in event.locals or context — never a module-level variable?
  • One syntax mode per file: no export let or $: in a file that uses runes?

Traps

TrapWhy it failsDo instead
$effect used to compute derived stateExtra render pass and a loop the moment the effect reads what it writes$derived / $derived.by
Destructuring a $state object, then mutating the copyThe destructured constants captured values, not the proxyKeep the object and read o.a; $derived for a stable view
let count = $state(0) exported from a .svelte.js moduleImporters bind to the value at import timeExport a class instance or { get count() {...} }
Per-user data in a server module variableOne value per process, shared by all concurrent requestsevent.locals in hooks, setContext in components
throw redirect(...) / throw error(...) inside a tryKit 2 helpers throw internally; your catch swallows the control flowCall them after the try/catch block
Fetching your own +server.js from a server loadAn extra HTTP hop, lost types, cookie forwarding you now ownCall the function or database directly in server load
{@html userInput}XSS, and component-scoped styles do not apply to injected markupSanitize server-side; style with :global
Passing $state straight to a chart or map libraryThe library receives a Proxy and its identity checks fail$state.snapshot(value)
await early in an $effect, then reading stateReads after the await are untracked; the effect never rerunsRead every dependency synchronously first
on:click or createEventDispatcher in a runes componentLegacy syntax; the runes compiler rejects or deprecates itonclick={...} and callback props
`` in runes modeDeprecated: components are already dynamic valuesCapitalized variable: ``
bind:value to a prop the child never declared bindableBindings are opt-in in runes modelet { value = $bindable() } = $props()
document or window at module scopeModule bodies execute during SSRMove into $effect/onMount, or guard with browser
Unkeyed {#each} around inputs or stateful childrenDOM nodes are reused by indexKey by id (rule 5)

Where Experts Disagree

  • Runes vs stores for shared state. Default: a class or object with $state fields in a .svelte.js module — plain values, no $ prefix, works outside components. Stores remain the right shape for push-based external sources (sockets, geolocation, third-party subscriptions) and for anything consuming the subscribe contract; fromStore/toStore bridge the two rather than forcing a rewrite.
  • How much $effect is acceptable. The framework position is that effects are a last resort for synchronizing with systems outside Svelte; the pragmatic position accepts effects for analytics, logging, and persistence. Both agree on the hard line: an effect that assigns state which feeds back into its own dependencies is a bug, not a style choice.
  • SSR by default vs SPA. Prerendering or SSR wins for public, indexable, first-paint-sensitive pages. adapter-static with ssr = false is a legitimate choice for an internal dashboard behind auth where every view needs the client anyway — the deciding factors are SEO, first paint, and no-JS support, not fashion.
  • Validation libraries in form actions. Hand-rolled checks are fine for one to three fields. Once you need repopulating on failure, nested data, arrays, or multi-step wizards, a schema library plus a form helper stops the per-field boilerplate from drifting out of sync with the server.

More Clawic skills, get them at https://clawic.com/skills/svelte (install if the user confirms):

  • typescript — type-system depth beyond the Svelte-specific typings
  • vite — dev server, plugins, and build configuration under SvelteKit
  • playwright — end-to-end testing of the running app
  • tailwindcss — utility styling inside Svelte components
  • nodejs — running and hardening the adapter-node server

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/svelte.

Questions people ask

Can it help migrate a Svelte 4 codebase to Svelte 5?
Yes. It covers replacing `export let`, `$:`, slots, `on:click`, and `new Component()` with runes-mode props, derived values, snippets, event attributes, and `mount()`/`unmount()`, while also supporting mixed legacy/runes codebases.
How does it diagnose Svelte reactivity failures?
It checks whether state uses `$state`, whether a value belongs in `$derived` rather than `$effect`, and whether destructuring, native Map/Set objects, class fields, async reads, or exported module state have broken tracking.
Does it cover SvelteKit server and deployment problems?
Yes. It addresses load invalidation, actions, endpoints, hooks, SSR boundaries, per-request state, secrets, hydration, and adapter builds for Node, static hosting, Vercel, Cloudflare, and Netlify.

Related skills

Build, debug, and review Vue 3 code across reactivity, components, state, routing, forms, and performance.

121 installs8 stars

Build and review React code while diagnosing state, hook, rendering, hydration, and performance issues.

245 installs3 stars

Write and troubleshoot Swift code, from concurrency and ARC issues to SwiftUI, packages, and interop.

77 installs2 stars

Write, configure, migrate, and debug Tailwind CSS across utility markup, themes, variants, and builds.

129 installs5 stars

Build and troubleshoot App Router projects across rendering, data, caching, auth, routing, and deployment.

143 installs4 stars

Design Prisma schemas and queries, then resolve migration, pooling, transaction, type, and deployment failures.

101 installs2 stars