
Svelte
- 3 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
svelte is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- svelte
- AI & Agent Building
- AI-coding skill
Svelte by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,657 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill svelteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
Svelte
Reactivity is explicit, compiler-driven, and minimal-runtime. Every reactive declaration uses a $ rune. The compiler transforms declarative code into surgical DOM updates -- no virtual DOM, no diffing, no hidden magic. References contain extended examples, rationale, and edge cases for each topic.
References
| Topic | Reference | Contents |
|---|---|---|
| Runes | [${CLAUDE_SKILL_DIR}/references/runes.md] | $state, $derived, $effect, $props, $bindable details |
| Components | [${CLAUDE_SKILL_DIR}/references/components.md] | Snippets, events, context, special elements |
| SvelteKit | [${CLAUDE_SKILL_DIR}/references/sveltekit.md] | Routing, load functions, form actions, hooks, imports |
Runes
$state
- Every mutable reactive value must use
$stateor$state.raw. Plainlet
declarations are not reactive.
- Arrays and plain objects become deeply reactive proxies. Mutations trigger
granular updates.
- Destructuring
$stateobjects breaks reactivity -- destructured values are
snapshots, not live references.
- Use
$stateon class fields or as first assignment in constructor. The compiler
transforms these into getter/setter pairs. Use arrow functions to preserve this in event handlers on classes.
$state.rawopts out of deep reactivity -- state can only be reassigned, not
mutated. Use for large arrays/objects you replace wholesale to avoid proxy overhead.
$state.snapshot(value)takes a static copy of a reactive proxy for external APIs
that don't expect proxies (e.g., structuredClone, logging).
- Import reactive
Set,Map,Date,URLfromsvelte/reactivitywhen you need
reactive built-in types.
Sharing State Across Modules
Cannot directly export reassignable $state. Two patterns:
- Object property (preferred): export
$state({ count: 0 })as a const, mutate
properties, export modifier functions.
- Getter function: keep
$stateprivate, exportgetCount()andincrement().
Runes only work in .svelte and .svelte.js/.svelte.ts files.
$derived
- Use
$derivedfor all computed values -- never synchronize state with$effect. $derived.by(() => { ... })for complex derivations needing a function body.- Only synchronously read values are tracked. Use
untrackto exempt specific reads. - Derived values can be temporarily overridden (useful for optimistic UI) -- reverts
to derived computation on next dependency update.
- Destructured
$derivedvalues are individually reactive. - Push-pull reactivity: dependents are notified immediately (push) but only
recalculated on read (pull). If new value is referentially identical, downstream updates are skipped.
$effect
$effectis an escape hatch. Use only for side effects: DOM manipulation,
analytics, third-party library calls, timers.
- Return a cleanup function when acquiring resources (intervals, listeners).
- Only synchronously read values are tracked -- values read after
awaitor inside
setTimeout are NOT tracked.
- Conditional reads: only values read in the last execution are dependencies.
- Runs only in the browser, after DOM updates.
$effect.preruns before DOM updates -- use for pre-DOM manipulation like
autoscrolling.
$effect.tracking()returnstrueif code is running inside a tracking context.$effect.root(() => { ... })creates a non-tracked scope for manual effect
lifecycle control. Returns a destroy function.
Never use `$effect` to synchronize state -- use $derived with callback event handlers or function bindings instead.
$props
- Always destructure props:
let { name, count = 0 } = $props(). - Type with an interface in TypeScript:
let { name }: Props = $props(). - Renaming:
let { class: klass } = $props(). - Rest props:
let { a, b, ...rest } = $props(). - All props:
let props = $props(). - Unique ID:
$props.id()-- consistent across SSR/hydration. - Props can be temporarily overridden by child. Do NOT mutate prop objects unless
$bindable. Use callback props to communicate changes upward.
$bindable
- Marks a prop as two-way bindable:
let { value = $bindable() } = $props(). - Parent optionally uses
bind:value={variable}. - Use sparingly -- overuse makes data flow unpredictable. Prefer callback props for
most parent-child communication.
$inspect
- Development-only debugging rune. Re-runs when arguments change. Noop in production.
$inspect(count, message)logs when tracked values change.$inspect(value).with((type, ...args) => { ... })replaces defaultconsole.log
with custom callback. Type is "init" or "update".
$inspect.trace()traces which reactive state caused a re-execution. Must be first
statement in a function body.
$host
Only available inside custom elements. Provides access to the host element for dispatching custom events.
Components
Structure Order
1. Imports 2. Props ($props()) 3. State ($state) 4. Derived values ($derived) 5. Effects ($effect, sparingly) 6. Functions 7. Markup (template) 8. Styles (<style>)
Naming
- Capitalize component names:
<MyComponent />. Required for dynamic rendering. - Component names must be capitalized or use dot notation (
item.component). - Components are dynamic by default --
<svelte:component>is unnecessary. Just
use <Thing /> where Thing is a reactive variable.
Events
- Use standard event attributes:
onclick={handler}, neveron:click={handler}. - Event attributes are case sensitive --
onclicklistens toclick,onClick
listens to Click.
- No event modifiers -- call
event.preventDefault()/event.stopPropagation()
in the handler. For capture, append to event name: onclickcapture={...}.
- Callback props for component events -- pass functions as props:
let { onEvent } = $props(). Never use createEventDispatcher.
- Event forwarding: accept callback props and spread them onto elements.
- Multiple handlers: combine in a single function (no duplicate attributes).
- Svelte uses event delegation for common events (
click,input,keydown) --
single listener at app root. When manually dispatching events, set { bubbles: true }. Prefer on from svelte/events over raw addEventListener.
Snippets
- Use
{@render children?.()}for default content. Never use<slot />. - Named snippets: declare with
{#snippet header()}...{/snippet}in parent, accept
as props, render with {@render header()}.
- Snippets with parameters pass data from child to parent:
{@render item(entry)} in child, {#snippet item(text)} in parent.
- Optional snippets: use
{@render children?.()}or{#if children}with fallback. - Snippets follow lexical scoping -- visible within their declaring block and children.
- Top-level snippets can be exported from
<script module>for cross-component use. - Type snippets with
SnippetandSnippet<[ParamType]>fromsvelte.
Template Syntax
Control flow:
{#if}/{:else if}/{:else}/{/if}for conditional blocks.{#each items as item, index (item.id)}with key expression for lists. Always
provide a key for lists that can change. :else renders when array is empty.
{#key value}destroys and recreates contents when value changes -- triggers entry
transitions or resets component state.
{#await promise}/{:then value}/{:catch error}for async. Short forms:
{#await promise then value} skips loading state.
Special tags:
{@html rawHtml}-- render raw HTML (escape user input to prevent XSS).{@const x = expr}-- declare local constant inside a block scope.{@debug var1, var2}-- trigger debugger when values change.{@render snippet()}-- render a snippet.{@attach action}-- attach an action to an element.
Text expressions: {expression} outputs stringified, escaped value. null and undefined are omitted.
Conditional classes: object syntax like clsx: class={{ cool, lame: !cool }}.
Context
setContext(key, value)/getContext(key)passes data through the component tree
without prop drilling.
- Type-safe context: use
createContext<T>()fromsveltewhich returns
[getContext, setContext] pair.
- Do NOT reassign the context object -- mutate its properties instead.
- For SSR safety, prefer context over global module state.
- Pass functions into
setContextto maintain reactivity across boundaries.
Special Elements
<svelte:boundary>-- error boundary. Use{#snippet failed(error, reset)}for
error UI and {#snippet pending()} for loading state with await expressions.
<svelte:window>-- bind to window events and properties (bind:scrollY).<svelte:head>-- insert elements intodocument.head(SEO meta tags, title).<svelte:element this={tag}>-- render a dynamic HTML element.<svelte:options>-- set compiler options (customElement,namespace).
Component Instantiation
Components are functions, not classes:
mount(Component, { target })for client-side mounting.unmount(app)to destroy.hydrateinstead ofmountfor server-rendered HTML.
State Management
- No shared module state on the server -- module-level
$stateis shared across
requests during SSR. Use context or event.locals instead.
- Return data from
load, don't write to globals. No side effects in load functions. - Context for SSR-safe shared state --
setContext/getContextfor data that must
not leak between requests.
- Use
$derivedfor reactive computed values in components -- plain assignments in
<script> run once, not reactively.
- Store filter/sort state in URL for survival across reload.
- Use snapshots for ephemeral UI state that should survive back/forward navigation.
File Conventions
.svelte.js/.svelte.tsfor reactive modules -- runes only work in.svelte
and .svelte.js/.svelte.ts files.
$libfor shared code -- import from$lib/instead of relative paths climbing
multiple levels.
SvelteKit
Route Files
| File | Purpose |
|---|---|
+page.svelte | Page component (receives data from load) |
+page.js | Universal load (server + browser) |
+page.server.js | Server-only load + form actions |
+layout.svelte | Layout wrapper (must render {@render children()}) |
+layout.js | Layout universal load |
+layout.server.js | Layout server load |
+error.svelte | Error boundary |
+server.js | API endpoint (GET, POST, etc.) |
Key rules: all files can run on the server. All run on the client except +server files. +layout and +error apply to subdirectories too.
Load Functions
Decision tree:
| Need | Use |
|---|---|
| Database, private keys | +page.server.js (PageServerLoad) |
| Non-serializable return values | +page.js (PageLoad) |
| External API, no secrets | +page.js (PageLoad) |
| Both | Both (server data flows to universal) |
Universal vs server:
| Aspect | Universal (+page.js) | Server (+page.server.js) |
|---|---|---|
| Runs on | Server (SSR) + Browser | Server only |
| Access | params, url, fetch | + cookies, locals, request |
| Returns | Any value (classes, components) | Serializable data only |
- Use the provided
fetch, not globalfetch-- inherits cookies, makes relative
requests work on server, bypasses HTTP overhead for internal requests.
- Export page options from
+page.js:prerender,ssr,csr. - Layout load data is available to all child pages.
- Stream non-essential data by returning un-awaited promises.
- SvelteKit tracks load dependencies and only reruns when:
paramschange,url
properties change, parent() was called and parent reran, or invalidate()/invalidateAll() called.
- Use
error()andredirect()from@sveltejs/kitfor error and redirect responses.
Form Actions
Server-only POST handlers in +page.server.js. Work without JavaScript.
- Default action:
export const actions = { default: async ({ request }) => { ... } }. - Named actions:
action="?/login"on form, multiple actions in theactionsobject. - Validation: return
fail(400, { field, missing: true })from action. Access via
form prop in the page component.
- Progressive enhancement: add
use:enhancefrom$app/formsfor JS-enhanced
submission without full page reload.
API Routes
Export HTTP verb handlers from +server.js: GET, POST, PUT, PATCH, DELETE. Return json() or new Response().
Hooks
Server hooks (src/hooks.server.js):
handle({ event, resolve })-- intercept every request. Setevent.locals, modify
response headers.
handleFetch-- modify server-side fetch calls.handleError-- log and sanitize unexpected errors.init-- run once at server startup.
Client hooks (src/hooks.client.js):
handleError-- client-side error handling.
Universal hooks (src/hooks.js):
reroute-- rewrite URLs before routing.transport-- serialize/deserialize custom types across server/client boundary.
Key Imports
Most-used modules: $app/navigation (goto, invalidate), $app/state (page, navigating), $app/forms (enhance), $env/static/private and $env/static/public for environment variables, $lib for shared code. Full imports table in ${CLAUDE_SKILL_DIR}/references/sveltekit.md.
Performance
- Use server
loadfunctions to avoid browser-to-API waterfalls. - Stream non-essential data with un-awaited promises.
- Use
$derivedinstead of$effectfor computed values. - Use link preloading (default on
<body>). - Minimize third-party scripts.
- Use
@sveltejs/enhanced-imgfor image optimization. - Use dynamic
import()for conditional code. - Deploy frontend near backend to minimize latency.
Application
When writing Svelte code:
- Apply all conventions silently -- don't narrate each rule.
- Always use runes, event attributes, and snippets.
- If an existing codebase uses outdated patterns, follow the codebase
and flag the divergence once.
- Type props with interfaces in TypeScript projects.
When reviewing Svelte code:
- Cite the specific violation and show the fix inline.
- Don't lecture -- state what's wrong and how to fix it.
Integration
This skill provides Svelte-specific conventions. The coding skill governs workflow; for TypeScript projects, the typescript skill handles language-level choices; for CSS concerns, the css skill handles styling conventions.
{
"sources": {
"Svelte Official LLM Docs (Small)": "https://svelte.dev/docs/svelte/llms-small.txt",
"SvelteKit Official LLM Docs (Small)": "https://svelte.dev/docs/kit/llms-small.txt",
"Svelte Runes - What Are Runes": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/01-what-are-runes.md",
"Svelte Runes - $state": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/02-$state.md",
"Svelte Runes - $derived": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/03-$derived.md",
"Svelte Runes - $effect": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/04-$effect.md",
"Svelte Runes - $props": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/05-$props.md",
"Svelte Runes - $bindable": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/06-$bindable.md",
"Svelte Runes - $inspect": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/07-$inspect.md",
"Svelte Runes - $host": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/02-runes/08-$host.md",
"Svelte Runtime - Context": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/06-runtime/02-context.md",
"Svelte Template Syntax - Basic Markup": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/01-basic-markup.md",
"Svelte Template Syntax - If Blocks": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/02-if.md",
"Svelte Template Syntax - Each Blocks": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/03-each.md",
"Svelte Template Syntax - Key Blocks": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/04-key.md",
"Svelte Template Syntax - Await Blocks": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/05-await.md",
"Svelte Template Syntax - Snippet": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/06-snippet.md",
"Svelte Template Syntax - Render": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/03-template-syntax/07-@render.md",
"Svelte Special Elements - Boundary": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/05-special-elements/01-svelte-boundary.md",
"Svelte Special Elements - Window": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/05-special-elements/02-svelte-window.md",
"Svelte Special Elements - Head": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/05-special-elements/05-svelte-head.md",
"Svelte Special Elements - Element": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/05-special-elements/06-svelte-element.md",
"Svelte Special Elements - Options": "https://raw.githubusercontent.com/sveltejs/svelte/main/documentation/docs/05-special-elements/07-svelte-options.md",
"SvelteKit - Routing": "https://raw.githubusercontent.com/sveltejs/kit/main/documentation/docs/20-core-concepts/10-routing.md",
"SvelteKit - Load Functions": "https://raw.githubusercontent.com/sveltejs/kit/main/documentation/docs/20-core-concepts/20-load.md",
"SvelteKit - Form Actions": "https://raw.githubusercontent.com/sveltejs/kit/main/documentation/docs/20-core-concepts/30-form-actions.md",
"SvelteKit - State Management": "https://raw.githubusercontent.com/sveltejs/kit/main/documentation/docs/20-core-concepts/50-state-management.md",
"SvelteKit - Hooks": "https://raw.githubusercontent.com/sveltejs/kit/main/documentation/docs/30-advanced/20-hooks.md",
"SvelteKit - Performance": "https://raw.githubusercontent.com/sveltejs/kit/main/documentation/docs/40-best-practices/05-performance.md"
},
"lastFetched": "2026-02-16T16:31:18.989Z"
}
Component Patterns
Svelte component conventions, events, snippets, template syntax, context, and special elements.
Component Structure
<script lang="ts">
// 1. Imports
import { setContext } from 'svelte';
import Button from './Button.svelte';
// 2. Props
interface Props {
title: string;
children: import('svelte').Snippet;
}
let { title, children }: Props = $props();
// 3. State
let count = $state(0);
// 4. Derived values
let doubled = $derived(count * 2);
// 5. Effects (sparingly)
$effect(() => {
document.title = title;
});
// 6. Functions
function increment() {
count++;
}
</script>
<!-- 7. Markup -->
<h1>{title}</h1>
<button onclick={increment}>{count} (doubled: {doubled})</button>
{@render children()}
<!-- 8. Styles -->
<style>
h1 { color: var(--heading-color); }
</style>Events
Use standard event attributes on elements:
<button onclick={() => count++}>click</button>Event attributes are case sensitive. onclick listens to the click event, onClick listens to the Click event.
Component Events as Callback Props
Pass functions as props for component-level events:
<!-- Parent -->
<Pump
inflate={(power) => { size += power; }}
deflate={(power) => { size -= power; }}
/>
<!-- Pump.svelte -->
<script>
let { inflate, deflate } = $props();
</script>
<button onclick={() => inflate(5)}>inflate</button>
<button onclick={() => deflate(5)}>deflate</button>Event Forwarding
Accept callback props and spread them:
<script>
let { onclick, ...rest } = $props();
</script>
<button {onclick} {...rest}>click me</button>No Event Modifiers
Handle in the function body:
<script>
function handleClick(event) {
event.preventDefault();
event.stopPropagation();
// actual logic
}
</script>
<button onclick={handleClick}>click</button>For capture, append to event name: onclickcapture={...}.
Multiple Handlers
Combine in a single function (no duplicate attributes):
<button onclick={(e) => { one(e); two(e); }}>click</button>Event Delegation
Svelte uses event delegation for common events (click, input, keydown, etc.) -- a single listener at the application root handles events that bubble up. When manually dispatching events with delegated listeners, set { bubbles: true }. Prefer the on function from svelte/events over raw addEventListener to ensure correct ordering and stopPropagation behavior.
Template Syntax
Control Flow
If blocks:
{#if condition}
<p>condition is true</p>
{:else if otherCondition}
<p>other condition is true</p>
{:else}
<p>neither is true</p>
{/if}Each blocks:
{#each items as item, index (item.id)}
<li>{index}: {item.name}</li>
{:else}
<li>No items</li>
{/each}Always provide a key expression (item.id) for lists that can change -- it enables efficient DOM reconciliation. The :else clause renders when the array is empty.
Key blocks:
{#key value}
<Component />
{/key}Destroys and recreates contents when value changes. Useful for triggering entry transitions or resetting component state.
Await blocks:
{#await promise}
<p>loading...</p>
{:then value}
<p>The value is {value}</p>
{:catch error}
<p>Error: {error.message}</p>
{/await}Short forms: {#await promise then value} skips the loading state. {#await promise catch error} skips both loading and success.
Special Tags
{@html rawHtml}-- Render raw HTML (escape user input to prevent XSS){@const x = expr}-- Declare a local constant inside a block scope{@debug var1, var2}-- Trigger debugger when values change{@render snippet()}-- Render a snippet (see below){@attach action}-- Attach an action to an element
Text Expressions
{expression} outputs a stringified, escaped value. null and undefined are omitted.
Snippets
Snippets are reusable chunks of markup defined inside components.
Default Content (children)
<!-- Card.svelte -->
<script>
let { children } = $props();
</script>
<div class="card">
{@render children?.()}
</div>
<!-- Usage -->
<Card>
<p>Card content here</p>
</Card>Named Snippets
<!-- Layout.svelte -->
<script>
let { header, main, footer } = $props();
</script>
<header>{@render header()}</header>
<main>{@render main()}</main>
<footer>{@render footer()}</footer>
<!-- Usage -->
<Layout>
{#snippet header()}
<h1>Title</h1>
{/snippet}
{#snippet main()}
<p>Content</p>
{/snippet}
{#snippet footer()}
<p>Footer</p>
{/snippet}
</Layout>Snippets with Parameters
Pass data from child back to parent:
<!-- List.svelte -->
<script>
let { items, item, empty } = $props();
</script>
{#if items.length}
<ul>
{#each items as entry}
<li>{@render item(entry)}</li>
{/each}
</ul>
{:else}
{@render empty?.()}
{/if}
<!-- Usage -->
<List items={['one', 'two']}>
{#snippet item(text)}
<span>{text}</span>
{/snippet}
{#snippet empty()}
<span>No items</span>
{/snippet}
</List>Optional Snippets
Use optional chaining or an {#if} block for fallback content:
{@render children?.()}
{#if children}
{@render children()}
{:else}
<p>Fallback content</p>
{/if}Snippet Scope
Snippets follow lexical scoping -- they can reference variables from their surrounding scope but are only visible within their declaring block and its children.
Exporting Snippets
Top-level snippets can be exported from a <script module> block for use in other components, provided they don't reference non-module declarations.
Typing Snippets
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
header: Snippet;
row: Snippet<[item: Item]>;
}Dynamic Components
Components are dynamic by default -- <svelte:component> is unnecessary:
<script>
let Thing = $state(ComponentA);
</script>
<!-- just use it directly -->
<Thing />Component names must be capitalized or use dot notation (item.component).
Component Instantiation
Components are functions:
import { mount, unmount } from 'svelte';
import App from './App.svelte';
const app = mount(App, { target: document.getElementById('app') });
// later:
unmount(app);Use hydrate instead of mount for server-rendered HTML.
Context
Pass data through the component tree without prop drilling:
<!-- Parent -->
<script>
import { setContext } from 'svelte';
let counter = $state({ count: 0 });
setContext('counter', counter);
</script>
<!-- Deep child -->
<script>
import { getContext } from 'svelte';
const counter = getContext('counter');
</script>
<p>{counter.count}</p>Type-safe Context
// context.ts
import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();Rules
- Do NOT reassign the context object -- mutate its properties instead
- For SSR safety, prefer context over global module state
- Pass functions into
setContextto maintain reactivity across boundaries
Special Elements
<svelte:boundary>
Error boundary and async loading wrapper. Prevents rendering errors from crashing the entire app.
<svelte:boundary>
<FlakyComponent />
{#snippet failed(error, reset)}
<button onclick={reset}>oops! try again</button>
{/snippet}
</svelte:boundary>The pending snippet provides a loading state for await expressions:
<svelte:boundary>
<p>{await delayed('hello!')}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary><svelte:window>
Bind to window events and properties:
<svelte:window onkeydown={handleKeydown} />
<svelte:window bind:scrollY={y} /><svelte:head>
Insert elements into document.head (useful for SEO meta tags):
<svelte:head>
<title>{pageTitle}</title>
<meta name="description" content={description} />
</svelte:head><svelte:element>
Render a dynamic HTML element:
<svelte:element this={tag}>content</svelte:element><svelte:options>
Set compiler options for the component:
<svelte:options customElement="my-component" />
<svelte:options namespace="svg" />Conditional Class Syntax
Objects for conditional class assignment (follows clsx syntax):
<script>
let { cool } = $props();
</script>
<div class={{ cool, lame: !cool }}>Content</div>Runes
Runes are compiler instructions prefixed with $ that control Svelte's reactivity. They are language keywords, not importable functions.
$state
Declares reactive state. The variable is a plain value, not a wrapper.
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>clicks: {count}</button>Deep Reactivity
Arrays and plain objects become deeply reactive proxies. Mutations trigger granular updates:
let todos = $state([{ done: false, text: 'add more todos' }]);
// triggers update on anything depending on todos[0].done
todos[0].done = !todos[0].done;
// pushed objects are also proxified
todos.push({ done: false, text: 'eat lunch' });Destructuring breaks reactivity. Destructured values are snapshots:
let { done, text } = todos[0];
// `done` will NOT update when todos[0].done changes$state in Classes
Use $state on class fields or as first assignment in constructor. The compiler transforms these into getter/setter pairs on the prototype.
class Todo {
done = $state(false);
text = $state('');
constructor(text) {
this.text = $state(text);
}
// use arrow functions to preserve `this` in event handlers
reset = () => {
this.text = '';
this.done = false;
};
}$state.raw
Opt out of deep reactivity. State can only be reassigned, not mutated:
let person = $state.raw({ name: 'Heraclitus', age: 49 });
// NO effect -- mutations are ignored
person.age += 1;
// works -- full reassignment
person = { name: 'Heraclitus', age: 50 };Use $state.raw for large arrays/objects you replace wholesale -- avoids proxy overhead.
$state.snapshot
Take a static copy of a reactive proxy for external APIs:
console.log($state.snapshot(counter)); // plain object, not ProxyUse when passing state to libraries that don't expect proxies (e.g., structuredClone, logging).
Reactive Built-ins
Import reactive Set, Map, Date, URL from svelte/reactivity.
Sharing State Across Modules
Cannot directly export reassignable $state. Two patterns:
Object property pattern (preferred):
// state.svelte.js
export const counter = $state({ count: 0 });
export function increment() { counter.count += 1; }Getter function pattern:
// state.svelte.js
let count = $state(0);
export function getCount() { return count; }
export function increment() { count += 1; }$derived
Declares computed state that recalculates when dependencies change:
let count = $state(0);
let doubled = $derived(count * 2);$derived.by
For complex derivations that need a function body:
let total = $derived.by(() => {
let sum = 0;
for (const n of numbers) sum += n;
return sum;
});Dependencies
Anything read synchronously inside $derived is tracked. Use untrack to exempt specific reads.
Overriding Derived Values
Derived values can be temporarily reassigned (useful for optimistic UI):
let likes = $derived(post.likes);
async function onclick() {
likes += 1; // optimistic update
try {
await like();
} catch {
likes -= 1; // rollback
}
}The value reverts to the derived computation on the next dependency update.
Destructuring Derived
Destructured $derived values are individually reactive:
let { a, b, c } = $derived(stuff());
// equivalent to:
// let a = $derived(_stuff.a); etc.Update Propagation
Push-pull reactivity: dependents are notified immediately (push) but only recalculated on read (pull). If the new value is referentially identical to the previous value, downstream updates are skipped.
$effect
Side-effect that runs when dependencies change. Runs only in the browser, after DOM updates.
$effect(() => {
const ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
});Teardown
Return a cleanup function from the effect:
$effect(() => {
const interval = setInterval(() => count += 1, milliseconds);
return () => clearInterval(interval);
});Dependency Tracking
- Only synchronously read values are tracked
- Values read after
awaitor insidesetTimeoutare NOT tracked - Effect reruns only when the tracked values change
- Conditional reads: only values read in the last execution are dependencies
$effect.pre
Runs before DOM updates. Same API as $effect, different timing. Use for pre-DOM manipulation like autoscrolling.
$effect.tracking
Returns true if code is running inside a tracking context (effect or template).
$effect.root
Creates a non-tracked scope for manual effect lifecycle control:
const destroy = $effect.root(() => {
$effect(() => { /* setup */ });
return () => { /* cleanup */ };
});
// later: destroy();When NOT to Use $effect
$effect is an escape hatch. Prefer $derived for computed values:
<!-- WRONG: effect to sync state -->
<script>
let count = $state(0);
let doubled = $state();
$effect(() => { doubled = count * 2; }); // don't do this
</script>
<!-- RIGHT: derived -->
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>Do not use $effect to synchronize two pieces of state -- use $derived with callback event handlers or function bindings instead.
$props
Declares component input properties via destructuring:
<script>
let { adjective, count = 0 } = $props();
</script>Patterns
- Renaming:
let { class: klass } = $props(); - Rest props:
let { a, b, ...rest } = $props(); - All props:
let props = $props(); - Unique ID:
const uid = $props.id();-- consistent across SSR/hydration
Type Safety
<script lang="ts">
interface Props {
adjective: string;
count?: number;
}
let { adjective, count = 0 }: Props = $props();
</script>Updating Props
Props can be temporarily overridden by the child. Do NOT mutate prop objects unless they are $bindable. Use callback props to communicate changes upward.
$bindable
Marks a prop as two-way bindable:
<!-- FancyInput.svelte -->
<script>
let { value = $bindable(), ...props } = $props();
</script>
<input bind:value={value} {...props} />Parent can optionally use bind::
<FancyInput bind:value={message} />Use sparingly -- overuse makes data flow unpredictable. Prefer callback props for most parent-child communication.
$inspect
Development-only debugging rune. Equivalent to console.log but re-runs whenever its arguments change. Tracks reactive state deeply. Becomes a noop in production builds.
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // logs when count or message change
</script>$inspect(...).with
Replace the default console.log with a custom callback:
<script>
let count = $state(0);
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // or console.trace, or whatever you want
}
});
</script>The first argument is either "init" or "update".
$inspect.trace
Traces the surrounding function in development. When an effect or derived re-runs, the console shows which reactive state caused the re-execution. Must be the first statement in a function body.
$effect(() => {
$inspect.trace();
doSomeWork();
});Accepts an optional label argument for identification.
$host
Only available inside custom elements. Provides access to the host element for dispatching custom events:
<svelte:options customElement="my-stepper" />
<script>
function dispatch(type) {
$host().dispatchEvent(new CustomEvent(type));
}
</script>
<button onclick={() => dispatch('decrement')}>decrement</button>
<button onclick={() => dispatch('increment')}>increment</button>SvelteKit
SvelteKit conventions: routing, load functions, form actions, hooks, and state management.
Filesystem Routing
Routes are directories under src/routes/. Files with + prefix are route files.
src/routes/
├── +page.svelte -> /
├── +layout.svelte -> wraps all pages
├── about/
│ └── +page.svelte -> /about
├── blog/
│ ├── +page.svelte -> /blog
│ └── [slug]/
│ ├── +page.svelte -> /blog/:slug
│ ├── +page.server.js -> server load + actions
│ └── +error.svelte -> error boundary
└── api/
└── items/
└── +server.js -> API endpointKey Rules
- All files can run on the server
- All files run on the client except
+serverfiles +layoutand+errorapply to subdirectories too
Page Files
+page.svelte
Renders the page. Receives data from load functions:
<script>
/** @type {import('./$types').PageProps} */
let { data } = $props();
</script>
<h1>{data.title}</h1>+page.js (Universal Load)
Runs on server during SSR and in browser during navigation:
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
return {
post: { title: `Post ${params.slug}` }
};
}Export page options: prerender, ssr, csr.
+page.server.js (Server Load)
Runs only on the server. Use for database access, private env vars:
import * as db from '$lib/server/database';
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return { post: await db.getPost(params.slug) };
}Also exports actions for form handling.
Layout Files
+layout.svelte
Wraps pages. Must render children:
<script>
let { children } = $props();
</script>
<nav><!-- navigation --></nav>
{@render children()}+layout.js / +layout.server.js
Layout load data is available to all child pages.
Load Functions
Universal vs Server
| Aspect | +page.js / +layout.js | +page.server.js / +layout.server.js |
|---|---|---|
| Runs on | Server (SSR) + Browser | Server only |
| Access | params, url, fetch | + cookies, locals, request |
| Returns | Any value (classes, components) | Serializable data (devalue) |
| Use for | External APIs, non-secret data | Database, private keys |
Load Function Input
Both types receive: params, route, url, fetch, setHeaders, parent, depends, untrack.
Server loads additionally receive: cookies, locals, platform, request.
Using fetch in Load
Use the provided fetch, not global fetch:
- Inherits cookies for same-origin requests
- Makes relative requests work on server
- Internal requests bypass HTTP overhead
- Responses are inlined during SSR
Streaming with Promises
Return un-awaited promises for non-essential data:
export async function load({ params }) {
return {
post: await loadPost(params.slug), // awaited -- blocks render
comments: loadComments(params.slug) // NOT awaited -- streams
};
}{#await data.comments}
Loading comments...
{:then comments}
{#each comments as c}<p>{c.content}</p>{/each}
{/await}Streaming requires platform support (works with Node.js servers and edge runtimes; does not work with AWS Lambda).
Rerunning Load Functions
SvelteKit tracks dependencies and only reruns when:
paramsvalues changeurlproperties changeparent()was called and parent reraninvalidate(url)orinvalidateAll()called
Errors and Redirects
import { error, redirect } from '@sveltejs/kit';
export function load({ locals }) {
if (!locals.user) redirect(307, '/login');
if (!locals.user.isAdmin) error(403, 'not an admin');
}Form Actions
Server-only POST handlers in +page.server.js. Work without JavaScript.
Default Action
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
// process...
}
};<form method="POST">
<input name="email" type="email">
<button>Submit</button>
</form>Named Actions
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
// ...
return { success: true };
},
register: async (event) => { /* ... */ }
};<form method="POST" action="?/login">
<!-- fields -->
<button>Log in</button>
<button formaction="?/register">Register</button>
</form>Validation Errors
import { fail } from '@sveltejs/kit';
export const actions = {
login: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
if (!email) return fail(400, { email, missing: true });
// ...
}
};Access via form prop:
<script>
let { form } = $props();
</script>
{#if form?.missing}<p class="error">Email required</p>{/if}Progressive Enhancement
Add use:enhance for JavaScript-enhanced form submission:
<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance>API Routes (+server.js)
Export HTTP verb handlers:
import { json, error } from '@sveltejs/kit';
export async function GET({ url }) {
return json({ data: 'value' });
}
export async function POST({ request }) {
const body = await request.json();
return json(body);
}Hooks
Server Hooks (src/hooks.server.js)
handle -- intercept every request:
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.cookies.get('sessionid'));
const response = await resolve(event);
response.headers.set('x-custom', 'value');
return response;
}handleFetch -- modify server-side fetch calls.
handleError -- log and sanitize unexpected errors.
init -- run once at server startup.
Client Hooks (src/hooks.client.js)
handleError -- client-side error handling.
Universal Hooks (src/hooks.js)
reroute -- rewrite URLs before routing.
transport -- serialize/deserialize custom types across server/client boundary.
State Management
Rules
- No shared state on server. Module-level variables are shared across
requests. Use event.locals or context instead.
- No side effects in load. Return data, don't write to global state.
- Use context for SSR-safe shared state. Context is per-component-tree,
not global.
- Components are reused on navigation. Use
$derivedfor values that
depend on data props -- plain assignments run only once.
- Store state in URL for filter/sort that should survive reload.
- Use snapshots for ephemeral UI state that should survive back/forward.
Context Pattern for Shared State
<!-- +layout.svelte -->
<script>
import { setContext } from 'svelte';
let { data } = $props();
setContext('user', () => data.user);
</script>
<!-- any child -->
<script>
import { getContext } from 'svelte';
const user = getContext('user');
</script>
<p>{user().name}</p>Key Imports
| Module | Exports |
|---|---|
$app/navigation | goto, invalidate, invalidateAll, beforeNavigate, afterNavigate |
$app/state | page (reactive page info), navigating, updated |
$app/forms | enhance, applyAction, deserialize |
$app/paths | base, assets, resolveRoute |
$app/server | getRequestEvent, read |
$env/static/private | Compile-time private env vars |
$env/static/public | Compile-time public env vars (PUBLIC_*) |
$env/dynamic/private | Runtime private env vars |
$env/dynamic/public | Runtime public env vars (PUBLIC_*) |
$lib | Alias for src/lib |
Performance
- Use server
loadfunctions to avoid browser-to-API waterfalls - Stream non-essential data with promises
- Use
$derivedinstead of$effectfor computed values - Use link preloading (default on
<body>) - Minimize third-party scripts
- Use
@sveltejs/enhanced-imgfor image optimization - Use dynamic
import()for conditional code - Deploy frontend near backend to minimize latency