Install
openclaw skills install @ivangdavila/svelteBuilds, debugs, and reviews Svelte and SvelteKit apps: runes, stores, snippets, load functions, form actions, adapters. Use when the UI does not update after a state change, an effect loops forever ("effect_update_depth_exceeded"), props or bindings stop reacting, a Map or class field is not tracked, state leaks between users on the server, "window is not defined" breaks the build, markup mismatches on hydration, data stays stale after a mutation, a form action does nothing or returns 403, scoped CSS is pruned as an unused selector, transitions never fire, or a Node, Vercel, Cloudflare, or static adapter build fails. Also for migrating Svelte 4 to Svelte 5 runes, converting export let to $props, $: to $derived, createEventDispatcher to callback props, slots to snippets and on:click to onclick, typing props and route data in TypeScript, testing components with Vitest or Playwright, and cutting bundle size and rerender cost. Not for Vue or Nuxt, React, or plain browser JavaScript semantics.
openclaw skills install @ivangdavila/svelteUser 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/.
.svelte.js state modules, or SvelteKit routes+server.js endpointswindow is not defined, state shared between requests, adapter build errorsvue, nuxt), React (react), or language-level JavaScript semantics (javascript)| Situation | Play |
|---|---|
| UI does not update after a change | Is the variable $state? Destructured? A Map/Set/class instance? → Reactivity Model table, then debug.md |
effect_update_depth_exceeded | An 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, $props | runes.md |
| Shared state across files, context, legacy stores | stores.md |
| Snippets, bindings, callback props, attachments, wrapping an imperative library | components.md |
| Scoped CSS not applying, unused-selector warning, transitions, motion | styling.md |
Typing props, snippets, route data, app.d.ts | typescript.md |
| Symptom-first debugging and the error-code catalog | debug.md |
Svelte 4 → 5: export let, $:, slots, on:click, new Component() | migration.md |
| Routes, layouts, groups, param matchers, navigation, shallow routing | routing.md |
load, invalidate, streaming, depends, serialization | data-loading.md |
Form actions, use:enhance, validation, uploads, remote functions | forms.md |
window is not defined, hydration mismatch, per-request state, env vars | ssr.md |
| Login, sessions, cookies, route guards, roles, CSRF, XSS, CSP, leaked secrets | security.md |
| Bundle size, slow lists, rerender cost, preloading | performance.md |
Vitest, component tests, mocking $app/*, Playwright, svelte-check | testing.md |
| Adapters, prerendering, CSP, service workers, self-hosting | deployment.md |
| Packaging components for npm, custom elements, library API design | library.md |
| Anything else | Apply Core Rules; reproduce in a single component with no props before blaming the framework |
$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.$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.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.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).{#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.+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).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.<form method="POST" action="?/save"> 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.$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().What is tracked, and what silently is not:
| Declaration | Reactive on | Rule |
|---|---|---|
let x = $state(0) | reassignment | Primitives: assign to update |
let o = $state({a: {b: 1}}) | reassignment + deep mutation | Plain objects/arrays become recursive proxies; o.a.b = 2 and arr.push() both work |
$state.raw(bigList) | reassignment only | No proxy: cheaper for large data you replace wholesale |
new Map(), Set, Date, URL inside $state | nothing | Not proxied — use SvelteMap, SvelteSet, SvelteDate, SvelteURL from svelte/reactivity |
Class field count = $state(0) | reassignment | Methods and get accessors stay reactive; the instance itself is not a proxy |
let { a, b = 1 } = $props() | parent updates | Destructuring props IS reactive in runes mode — the compiler rewrites the reads |
const { a } = someStateObject | nothing | Destructuring state reads the value once; keep the object, or wrap in $derived |
Exported let x = $state(0) from a module | not across the import | Importers get a snapshot binding — export an object, a class instance, or a getter |
let x = 0 in a runes file | nothing | Compiler 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.
svelte >=5 — runes, snippets, onclick event attributes, mount()/unmount(); $:, export let, slots, and new Component() only work in legacy modesvelte >=5.3 — <svelte:boundary> with failed snippet and onerrorsvelte >=5.29 — attachments ({@attach fn}) supersede actions (use:fn); actions still worksvelte >=5.36 — experimental await inside components and $effect.pending(), behind the experimental.async compiler option@sveltejs/kit >=2 — error() 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.16 — PageProps 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| File | Runs | Can hold secrets | Shipped to browser |
|---|---|---|---|
+page.svelte, +layout.svelte | SSR render, then client | No | Yes |
+page.js (universal load) | Server on first load, client on navigation | No | Yes |
+page.server.js (server load, actions) | Server only | Yes | No |
+server.js (endpoint) | Server only | Yes | No |
hooks.server.js | Server, every request | Yes | No |
hooks.client.js | Browser only | No | Yes |
$lib/server/**, *.server.js | Server only | Yes | Import 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.
| Code or message | Meaning | First move |
|---|---|---|
state_unsafe_mutation | State written while a derived or the template was computing it | Move the write into an event handler or $effect |
effect_update_depth_exceeded | An effect writes state that it (or a chained effect) reads | Convert to $derived; if the read is genuinely one-way, wrap it in untrack() |
hydration_mismatch | Server HTML differs from the first client render | Remove Date.now()/Math.random()/window from render; check invalid nesting (<div> inside <p>) |
rune_outside_svelte | A rune used in a .js/.ts file | Rename the file to .svelte.js / .svelte.ts |
lifecycle_outside_component | onMount, setContext, or getContext called after an await or inside a callback | Call synchronously during component initialization |
derived_references_self | A $derived reads its own value | Compute from source state, or keep the accumulator in $state |
each_key_duplicate | Two items produced the same key | Key by a unique id; never by index or by a repeated value |
bind_invalid_export / binding error | bind: to a prop the child did not declare bindable | let { 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 POST | Kit's CSRF origin check rejected a cross-origin submission | Submit same-origin, or set csrf.checkOrigin deliberately |
Cannot import $lib/server/... into client-side code | A server-only module reached a client bundle | Import it in +page.server.js and pass the result through load |
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/svelte/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| syntax_mode | runes | legacy | mixed | runes | Selects the syntax of every generated component ($state/$props vs let/export let) and whether migration advice is offered at all |
| kit_project | bool | true | When false, drops routing, load, actions, and adapter guidance and treats the app as Svelte + Vite with client-side routing |
| deployment_adapter | auto | node | static | vercel | cloudflare | netlify | auto | Drives the deploy checklist, which env-var mechanism is valid, and whether prerender/SPA fallback is required |
| package_manager | npm | pnpm | yarn | bun | npm | Sets the command syntax in install, sv add, build, and test instructions |
| styling | scoped-css | tailwind | unocss | css-modules | scoped-css | Shapes generated markup and which scoped-CSS caveats apply |
| typescript | bool | true | Emits <script lang="ts">, typed $props(), and ./$types imports; false switches to JSDoc annotations |
| experimental_features | bool | false | When 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_threshold | error | warning | error | The 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:
$lib structure, barrel filesexperimental_features, upgrade cadence, and whether to propose a migration on a codebase that currently workscheck_thresholdBefore emitting a component or route, verify:
$state; no bare let expected to rerender?$derived, with $effect reserved for DOM, network, storage, or third-party libraries?{#each} over reorderable data keyed by a stable id?$env/static/private import reachable from +page.svelte or +page.js?$effect/onMount or behind a browser check?event.locals or context — never a module-level variable?export let or $: in a file that uses runes?| Trap | Why it fails | Do instead |
|---|---|---|
$effect used to compute derived state | Extra render pass and a loop the moment the effect reads what it writes | $derived / $derived.by |
Destructuring a $state object, then mutating the copy | The destructured constants captured values, not the proxy | Keep the object and read o.a; $derived for a stable view |
let count = $state(0) exported from a .svelte.js module | Importers bind to the value at import time | Export a class instance or { get count() {...} } |
| Per-user data in a server module variable | One value per process, shared by all concurrent requests | event.locals in hooks, setContext in components |
throw redirect(...) / throw error(...) inside a try | Kit 2 helpers throw internally; your catch swallows the control flow | Call them after the try/catch block |
Fetching your own +server.js from a server load | An extra HTTP hop, lost types, cookie forwarding you now own | Call the function or database directly in server load |
{@html userInput} | XSS, and component-scoped styles do not apply to injected markup | Sanitize server-side; style with :global |
Passing $state straight to a chart or map library | The library receives a Proxy and its identity checks fail | $state.snapshot(value) |
await early in an $effect, then reading state | Reads after the await are untracked; the effect never reruns | Read every dependency synchronously first |
on:click or createEventDispatcher in a runes component | Legacy syntax; the runes compiler rejects or deprecates it | onclick={...} and callback props |
<svelte:component this={C}> in runes mode | Deprecated: components are already dynamic values | Capitalized variable: <C /> |
bind:value to a prop the child never declared bindable | Bindings are opt-in in runes mode | let { value = $bindable() } = $props() |
document or window at module scope | Module bodies execute during SSR | Move into $effect/onMount, or guard with browser |
Unkeyed {#each} around inputs or stateful children | DOM nodes are reused by index | Key by id (rule 5) |
$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.$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.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.More Clawic skills, get them at https://clawic.com/skills/svelte (install if the user confirms):
typescript — type-system depth beyond the Svelte-specific typingsvite — dev server, plugins, and build configuration under SvelteKitplaywright — end-to-end testing of the running apptailwindcss — utility styling inside Svelte componentsnodejs — running and hardening the adapter-node serverPart of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/svelte.