Svelte 5 runes and patterns used across the Fuz ecosystem.
- State Runes - Derived Values - Reactive Collections - Schema-Driven Reactive Classes - Context Patterns - Snippet Patterns - Effect Patterns - Attachment Patterns - Props Patterns - Event Handling - Component Composition - Runes in .svelte.ts Files - Debugging - Each Blocks - CSS in Components - Legacy Features to Avoid - Quick Reference
Only use $state for variables that should be reactive — variables that
cause an $effect, $derived, or template expression to update. Everything
else can be a normal variable.
$state() vs $state.raw()$state() for deep reactivity (objects are proxied — mutation triggers updates,
but with performance overhead). $state.raw() for data replaced wholesale
(no proxy, better for large objects).
// $state() - deep reactivity, use for UI state
let form_data = $state({name: '', email: ''});
form_data.name = 'Alice'; // triggers reactivity
// $state.raw() - shallow, use for API responses and immutable data
let ollama_show_response = $state.raw<OllamaShowResponse | null>(null);
let completion_request = $state.raw<CompletionRequest | null>(null);
let completion_response = $state.raw<CompletionResponse | null>(null);When to use $state.raw():
- API responses (replaced entirely on each fetch)
- ReadonlyArray collections replaced via spread ($state.raw([]))
- Large objects where deep tracking is wasteful
- Immutable data structures
- Objects from external libraries
When to use $state():
- Form state with individual field updates - UI state (toggles, selections, counters) - Objects you'll mutate property-by-property
$state()! Non-null Assertion PatternClass properties initialized by constructor or init() use $state()!:
export class ThemeState {
theme: Theme = $state()!;
color_scheme: ColorScheme = $state()!;
constructor(options?: ThemeStateOptions) {
this.theme = options?.theme ?? default_themes[0]!;
this.color_scheme = options?.color_scheme ?? 'auto';
}
}Used in fuz_ui state classes (ThemeState, SelectedStyleVariable) and zzz
Cell subclasses. Library and Module use the variant $state.raw()! for
data replaced wholesale.
// Reactive array - mutations tracked
let items = $state<string[]>([]);
items.push('new'); // triggers reactivity
items[0] = 'updated'; // triggers reactivity
// Raw array - only replacement tracked (common for immutable lists)
let selections: ReadonlyArray<ItemState> = $state.raw([]);
selections = [...selections, new_item]; // triggers
selections.push(new_item); // does NOT trigger (and type error with ReadonlyArray)$state.snapshot()Returns a plain (non-reactive) snapshot of a $state proxy. Used in zzz's
Cell base class for JSON serialization:
// cell.svelte.ts - encode_property uses snapshot for serialization
encode_property(value: unknown, _key: string): unknown {
return $state.snapshot(value);
}Use when you need to pass reactive state to non-reactive APIs (e.g.,
structuredClone, JSON.stringify, or external libraries that don't
understand Svelte proxies).
Use $derived to compute from state — never $effect with assignment.
Deriveds are writable (assign to override, but the expression re-evaluates on
dependency change). Derived objects/arrays are not made deeply reactive.
$derived vs $derived.by()$derived takes an expression (not a function). $derived.by() for loops,
conditionals, or multi-step logic.
// Simple expression - use $derived
let count = $state(0);
let doubled = $derived(count * 2);
let is_empty = $derived(items.length === 0);
// Complex logic - use $derived.by()
let filtered_items = $derived.by(() => {
if (!filter) return items;
return items.filter((item) => item.name.includes(filter));
});
// Loops require $derived.by()
let total = $derived.by(() => {
let sum = 0;
for (const item of items) {
sum += item.value;
}
return sum;
});$derived in ClassesClass properties use $derived and $derived.by() directly:
// From Library class (fuz_ui/library.svelte.ts)
export class Library {
readonly library_json: LibraryJson = $state.raw()!;
readonly package_json = $derived(this.library_json.package_json);
readonly source_json = $derived(this.library_json.source_json);
readonly name = $derived(this.library_json.name);
readonly repo_url = $derived(this.library_json.repo_url);
readonly modules = $derived(
this.source_json.modules
? this.source_json.modules.map((module_json) => new Module(this, module_json))
: [],
);
readonly module_by_path = $derived(
new Map(this.modules.map((m) => [m.path, m])),
);
}// From Thread class (zzz/thread.svelte.ts) - $derived.by for complex logic
readonly model: Model = $derived.by(() => {
const model = this.app.models.find_by_name(this.model_name);
if (!model) throw new Error(`Model "${this.model_name}" not found`);
return model;
});
// From ContextmenuState - $derived for simple, $derived.by for multi-step
can_collapse = $derived(this.selections.length > 1);
can_expand = $derived.by(() => {
const selected = this.selections.at(-1);
return !!selected?.is_menu && selected.items.length > 0;
});Treat props as though they will change — use $derived for values that depend
on props:
let {type} = $props();
// Do this — updates when type changes
let color = $derived(type === 'danger' ? 'red' : 'green');
// Don't do this — color won't update if type changes
// let color = type === 'danger' ? 'red' : 'green';SvelteMap and SvelteSetFrom svelte/reactivity — reactive Map/Set that trigger updates on mutations:
import {SvelteMap, SvelteSet} from 'svelte/reactivity';
// From DocsLinks class (fuz_ui/docs_helpers.svelte.ts)
export class DocsLinks {
readonly links: SvelteMap<string, DocsLinkInfo> = new SvelteMap();
readonly fragments_onscreen: SvelteSet<string> = new SvelteSet();
// $derived.by works with SvelteMap - recomputes when links change
docs_links = $derived.by(() => {
const children_map: Map<string | undefined, Array<DocsLinkInfo>> = new Map();
for (const link of this.links.values()) {
// ... build tree from SvelteMap entries
}
return result;
});
}Standard Map/Set are not tracked by Svelte's reactivity.
Zod schemas paired with Svelte 5 runes classes — the schema defines the JSON shape, the class adds reactivity and behavior. See ./zod-schemas.
// theme_state.svelte.ts
export class ThemeState {
theme: Theme = $state()!;
color_scheme: ColorScheme = $state()!;
constructor(options?: ThemeStateOptions) {
this.theme = options?.theme ?? default_themes[0]!;
this.color_scheme = options?.color_scheme ?? 'auto';
}
toJSON(): ThemeStateJson {
return {
theme: this.theme,
color_scheme: this.color_scheme,
};
}
}Advanced version with Cell base class that automates JSON hydration from
Zod schemas:
// Schema with CellJson base, .meta for class registration
export const ChatJson = CellJson.extend({
name: z.string().default(''),
thread_ids: z.array(Uuid).default(() => []),
}).meta({cell_class_name: 'Chat'});
export class Chat extends Cell<typeof ChatJson> {
// Schema fields use $state()! - set by Cell.init()
name: string = $state()!;
thread_ids: Array<Uuid> = $state()!;
// Computed values use $derived or $derived.by()
readonly threads: Array<Thread> = $derived.by(() => {
const result: Array<Thread> = [];
for (const id of this.thread_ids) {
const thread = this.app.threads.items.by_id.get(id);
if (thread) result.push(thread);
}
return result;
});
constructor(options: ChatOptions) {
super(ChatJson, options);
this.init(); // Must call at end of constructor
}
}Key patterns:
- Zod schema defines the JSON shape (see ./zod-schemas)
- Class properties use $state()! for reactivity (non-null assertion)
- $derived / $derived.by() for computed values in classes
- $state.raw() for properties replaced wholesale
- toJSON() or to_json() for serialization (zzz Cell uses a $derived json
property)
create_context<T>() from @fuzdev/fuz_ui/context_helpers.js. Two overloads:
without fallback, get() throws if unset and get_maybe() returns undefined;
with fallback, get() uses it and set() value is optional:
// Without fallback -- get() throws if unset, get_maybe() returns undefined
export function create_context<T>(): {
get: (error_message?: string) => T;
get_maybe: () => T | undefined;
set: (value: T) => T;
};
// With fallback -- get() uses fallback if unset, set() value is optional
export function create_context<T>(fallback: () => T): {
get: () => T;
set: (value?: T) => T;
};// Define the context (typically in a shared module)
export const frontend_context = create_context<Frontend>();
export const section_depth_context = create_context(() => 0);<!-- Provider component sets the context -->
<script>
import type {Snippet} from 'svelte';
import {frontend_context} from './frontend.svelte.js';
const {app, children}: {app: Frontend; children: Snippet} = $props();
frontend_context.set(app);
</script>
{@render children()}<!-- Consumer components get the context -->
<script>
import {frontend_context} from './frontend.svelte.js';
const app = frontend_context.get();
</script>Some contexts wrap values in () => T so the context reference stays stable
while the value can change:
// Type is () => ThemeState, not ThemeState
export const theme_state_context = create_context<() => ThemeState>();
// Setting with a getter
theme_state_context.set(() => theme_state);
// Consuming - call the getter
const get_theme_state = theme_state_context.get();
const theme_state = get_theme_state();Used when the context value might be reassigned (e.g., theme_state is a prop).
Direct value contexts like frontend_context and library_context are for
values stable for the context's lifetime.
fuz_ui contexts:
| Context | Type | Source file | Purpose |
| ---------------------------------- | ---------------------------- | ---------------------------------- | -------------------------------- |
| theme_state_context | () => ThemeState | theme_state.svelte.ts | Theme state (getter pattern) |
| library_context | Library | library.svelte.ts | Package API metadata for docs |
| tomes_context | () => Map<string, Tome> | tome.ts | Available documentation tomes |
| tome_context | () => Tome | tome.ts | Current documentation page |
| docs_links_context | DocsLinks | docs_helpers.svelte.ts | Documentation navigation |
| section_depth_context | number | TomeSection.svelte | Heading depth (fallback: 0) |
| register_section_header_context | RegisterSectionHeader | TomeSection.svelte | Register section header callback |
| section_id_context | string \| undefined | TomeSection.svelte | Current section ID |
| contextmenu_context | () => ContextmenuState | contextmenu_state.svelte.ts | Context menu state (getter) |
| contextmenu_submenu_context | SubmenuState | contextmenu_state.svelte.ts | Current submenu state |
| contextmenu_dimensions_context | Dimensions | contextmenu_state.svelte.ts | Context menu positioning |
| selected_variable_context | SelectedStyleVariable | style_variable_helpers.svelte.ts | Style variable selection |
| mdz_components_context | MdzComponents | mdz_components.ts | Custom mdz components |
| mdz_elements_context | MdzElements | mdz_components.ts | Allowed HTML elements in mdz |
| mdz_base_context | () => string \| undefined | mdz_components.ts | Base path for mdz links |
zzz contexts:
| Context | Type | Source file | Purpose |
| -------------------- | ---------- | -------------------- | ----------------- |
| frontend_context | Frontend | frontend.svelte.ts | Application state |
Svelte 5 replaces slots with snippets ({#snippet} and {@render}).
children SnippetImplicit children replaces the default slot. Typed as Snippet (or
Snippet<[params]> with parameters):
<script lang="ts">
import type {Snippet} from 'svelte';
const {children}: {children: Snippet} = $props();
</script>
<div class="wrapper">
{@render children()}
</div>Content between component tags becomes children:
<Wrapper>
<p>This becomes the children snippet.</p>
</Wrapper>ThemeRoot and Dialog pass data back via parameterized children:
<!-- ThemeRoot.svelte passes theme_state, style, and html to children -->
<script lang="ts">
const {children}: {
children: Snippet<[theme_state: ThemeState, style: string | null, theme_style_html: string | null]>;
} = $props();
</script>
{@render children(theme_state, style, theme_style_html)}<!-- Dialog.svelte passes a close function -->
<script lang="ts">
const {children}: {
children: Snippet<[close: (e?: Event) => void]>;
} = $props();
</script>
{@render children(close)}<script lang="ts">
import type {Snippet} from 'svelte';
const {
summary,
children,
}: {
summary: string | Snippet;
children: Snippet;
} = $props();
</script>
<details>
<summary>
{#if typeof summary === 'string'}
{summary}
{:else}
{@render summary()}
{/if}
</summary>
{@render children()}
</details><!-- List.svelte -->
<script lang="ts" generics="T">
import type {Snippet} from 'svelte';
const {
items,
item,
empty,
}: {
items: T[];
item: Snippet<[T]>;
empty?: Snippet;
} = $props();
</script>
{#if items.length === 0}
{#if empty}
{@render empty()}
{/if}
{:else}
{#each items as entry}
{@render item(entry)}
{/each}
{/if}<script lang="ts">
import type {Snippet} from 'svelte';
const {menu}: {menu?: Snippet} = $props();
</script>
{#if menu}
{@render menu()}
{:else}
<!-- Default content when no snippet provided -->
<button>Default Menu</button>
{/if}Card accepts icons as string (emoji) or Snippet:
const {icon}: {icon?: string | Snippet} = $props();Alert uses a richer variant — the Snippet receives the resolved icon string,
and null hides the icon:
const {icon}: {icon?: string | Snippet<[icon: string]> | null | undefined} = $props();{#if typeof final_icon === 'string'}
{final_icon}
{:else}
{@render final_icon()}
{/if}Effects are an escape hatch — avoid when possible. Prefer:
- $derived / $derived.by() for computing from state
- {@attach} for syncing with external libraries or DOM
- Event handlers / function bindings for responding to user interaction
- $inspect / $inspect.trace() for debugging (not $effect + console.log)
- createSubscriber from svelte/reactivity for observing external sources
Don't wrap effect contents in if (browser) {...} — effects don't run on the
server. Avoid updating $state inside effects.
$effect(() => {
// Runs when any tracked dependency changes
console.log('Count is now:', count);
});Return a cleanup function for subscriptions, timers, or listeners:
$effect(() => {
const interval = setInterval(() => {
tick_count++;
}, 1000);
// Cleanup runs before next effect and on destroy
return () => clearInterval(interval);
});
$effect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') close();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
});$effect.pre()Runs before DOM updates. Used for dev-mode validation and scroll management:
// Dev-mode validation (GithubLink.svelte)
if (DEV) {
$effect.pre(() => {
if (!path && !href_prop) {
throw new Error('GithubLink requires either `path` or `href` prop');
}
});
}effect_with_count()From @fuzdev/fuz_ui/rune_helpers.svelte.js — passes call count to the
effect, useful for skipping the initial run:
import {effect_with_count} from '@fuzdev/fuz_ui/rune_helpers.svelte.js';
// Skip the first run (count === 1), save on subsequent changes
effect_with_count((count) => {
const v = theme_state.color_scheme;
if (count === 1) return; // skip initial
save_color_scheme(v);
});untrack()Read values without creating dependencies:
import {untrack} from 'svelte';
$effect(() => {
// count is tracked
console.log('Count changed to:', count);
// other_value is NOT tracked - reading it won't re-run the effect
const snapshot = untrack(() => other_value);
save_snapshot(count, snapshot);
});Use cases: reading config that shouldn't trigger re-runs, accessing stable references, breaking infinite loops in bidirectional syncing.
Svelte 5 attachments ({@attach}) replace actions (use:). Attachments live
in *.svelte.ts files and use Attachment from svelte/attachments.
An attachment is (element) => cleanup | void. fuz_ui uses a factory
pattern — export a function that accepts configuration and returns the
Attachment:
import type {Attachment} from 'svelte/attachments';
export const my_attachment =
(options?: MyOptions): Attachment<HTMLElement | SVGElement> =>
(el) => {
// setup
return () => {
// cleanup (optional)
};
};Usage: {@attach my_attachment()} or {@attach my_attachment({...options})}
autofocus -- Focus on MountSolves the HTML autofocus attribute not working when elements mount from
reactive conditionals ({#if}) in SPAs.
// autofocus.svelte.ts
import type {Attachment} from 'svelte/attachments';
export const autofocus =
(options?: FocusOptions): Attachment<HTMLElement | SVGElement> =>
(el) => {
el.focus({focusVisible: true, ...options} as FocusOptions);
};<script>
import {autofocus} from '@fuzdev/fuz_ui/autofocus.svelte.js';
</script>
<!-- Basic usage -->
<input {@attach autofocus()} />
<!-- With options -->
<input {@attach autofocus({preventScroll: true})} />intersect -- IntersectionObserverWraps IntersectionObserver with a lazy function pattern — reactive callbacks update without recreating the observer.
// intersect.svelte.ts — signature only, see source for implementation
export const intersect =
(
get_params: () => IntersectParamsOrCallback | null | undefined,
): Attachment<HTMLElement | SVGElement> =>
(el) => {
// Uses $effect internally: callbacks update reactively,
// observer only recreates when options change (deep equality check)
};<script>
import {intersect} from '@fuzdev/fuz_ui/intersect.svelte.js';
</script>
<!-- Simple callback (receives IntersectState: intersecting, intersections, el, observer, disconnect) -->
<div {@attach intersect(() => ({intersecting}) => { ... })}>
<!-- Full params with options -->
<div {@attach intersect(() => ({
onintersect: ({intersecting, el}) => {
el.classList.toggle('visible', intersecting);
},
ondisconnect: ({intersecting, intersections}) => { ... },
count: 1,
options: {threshold: 0.5},
}))}>contextmenu_attachment -- Context Menu DataCaches context menu params on an element via dataset. Direct params (no lazy function). Returns cleanup that removes the cache entry.
// contextmenu_state.svelte.ts (exported alongside Contextmenu state class)
export const contextmenu_attachment =
<T extends ContextmenuParams, U extends T | Array<T>>(
params: U | null | undefined,
): Attachment<HTMLElement | SVGElement> =>
(el): undefined | (() => void) => {
if (params == null) return;
// cache params in dataset, return cleanup
};Attachments as class properties, sharing reactive state with the instance:
// scrollable.svelte.ts (simplified — see source for flex-direction handling)
export class Scrollable {
scroll_y: number = $state(0);
readonly scrolled: boolean = $derived(this.scroll_y > this.threshold);
// Listens to scroll events, updates class state
container: Attachment = (element) => {
const cleanup = on(element, 'scroll', () => {
this.scroll_y = element.scrollTop;
});
return () => cleanup();
};
// Attachments run in an effect context — reruns when `this.scrolled` changes
target: Attachment = (element) => {
if (this.scrolled) {
element.classList.add(this.target_class);
} else {
element.classList.remove(this.target_class);
}
return () => element.classList.remove(this.target_class);
};
}<div {@attach scrollable.container} {@attach scrollable.target}>| Pattern | When to use | Example |
| ----------------------------- | ----------------------------------------- | --------------- |
| Simple factory | Fire-once, no ongoing observation | autofocus |
| Lazy function (() => p) | Reactive callbacks without observer churn | intersect |
| Direct params | Static config cached for later retrieval | contextmenu |
| Class method | Attachment shares state with a class | Scrollable |
1. Create src/lib/my_attachment.svelte.ts
2. Export a factory function returning Attachment<HTMLElement | SVGElement>
3. Return cleanup if holding resources (observers, listeners)
4. Use $effect inside for reactive behavior, on() for event listeners
5. Add JSDoc with @module and @param tags
<script lang="ts">
const {
name,
count = 0,
items = [],
}: {
name: string;
count?: number;
items?: string[];
} = $props();
</script>Use let (not const) for $bindable() props:
<script lang="ts">
let {
value = $bindable(180),
children,
}: {
value?: number;
children?: Snippet;
} = $props();
</script>
<!-- Usage -->
<HueInput bind:value={hue} />Real examples from fuz_ui:
// HueInput.svelte
let {value = $bindable(180), children, ...rest} = $props();
// Details.svelte
let {open = $bindable(), ...rest} = $props();
// DocsSearch.svelte
let {search_query = $bindable(), ...rest} = $props();Use SvelteHTMLElements from svelte/elements intersected with custom props:
<script lang="ts">
import type {Snippet} from 'svelte';
import type {SvelteHTMLElements} from 'svelte/elements';
const {
align = 'left',
icon,
children,
...rest
}: SvelteHTMLElements['div'] & SvelteHTMLElements['a'] & {
align?: 'left' | 'right' | 'above' | 'below';
icon?: string | Snippet;
children: Snippet;
} = $props();
</script>
<div {...rest} class="card {rest.class}">
{@render children()}
</div>Use SvelteHTMLElements['div'] (not HTMLAttributes<HTMLDivElement>).
Svelte 5 uses standard DOM event syntax:
<button onclick={handle_click}>Click</button>
<input oninput={(e) => value = e.currentTarget.value} />
<!-- Conditional event handlers (pass undefined to remove) -->
<svelte:window onkeydown={active ? on_window_keydown : undefined} />on() from svelte/events for programmatic listeners in attachments and
.svelte.ts files. Returns cleanup:
import {on} from 'svelte/events';
// Inside an attachment
const cleanup = on(element, 'scroll', onscroll);
return () => cleanup();<script lang="ts" module> for component-level exports (contexts, types):
<!-- TomeSection.svelte -->
<script lang="ts" module>
import {create_context} from './context_helpers.js';
export type RegisterSectionHeader = (get_fragment: () => string) => string | undefined;
export const register_section_header_context = create_context<RegisterSectionHeader>();
export const section_depth_context = create_context(() => 0);
export const section_id_context = create_context<string | undefined>();
</script>
<script lang="ts">
// instance script
</script><!-- Wrapper.svelte -->
<script lang="ts">
import type {Snippet} from 'svelte';
import Inner from './Inner.svelte';
const {
header,
children,
}: {
header?: Snippet;
children?: Snippet;
} = $props();
</script>
<div class="wrapper">
<Inner {header}>
{#if children}
{@render children()}
{/if}
</Inner>
</div><script lang="ts" generics="T">
import type {Snippet} from 'svelte';
const {
items,
render,
}: {
items: T[];
render: Snippet<[T, number]>;
} = $props();
</script>
{#each items as item, index}
{@render render(item, index)}
{/each}svelte:element for components rendering different HTML tags:
<script lang="ts">
const {tag, href, children, ...rest} = $props();
const link = $derived(!!href);
const final_tag = $derived(tag ?? (link ? 'a' : 'div'));
</script>
<svelte:element this={final_tag} {...rest} {href}>
{@render children()}
</svelte:element><script>
import {slide} from 'svelte/transition';
</script>
{#if open}
<div transition:slide>{@render children()}</div>
{/if}.svelte.ts files use runes ($state, $derived, $effect) outside
components.
// api_search.svelte.ts
export const create_api_search = (library: Library): ApiSearchState => {
let query = $state('');
const all_modules = $derived(library.modules_sorted);
const filtered_modules = $derived.by(() => {
if (!query.trim()) return all_modules;
const terms = query.trim().toLowerCase().split(/\s+/);
return all_modules.filter((m) => {
const path_lower = m.path.toLowerCase();
const comment_lower = m.module_comment?.toLowerCase() ?? '';
return terms.every((term) => path_lower.includes(term) || comment_lower.includes(term));
});
});
const all_declarations = $derived(library.declarations);
const filtered_declarations = $derived.by(() => {
const items = query.trim() ? library.search_declarations(query) : all_declarations;
return items.sort((a, b) => a.name.localeCompare(b.name));
});
return {
get query() { return query; },
set query(v: string) { query = v; },
modules: {
get all() { return all_modules; },
get filtered() { return filtered_modules; },
},
declarations: {
get all() { return all_declarations; },
get filtered() { return filtered_declarations; },
},
};
};The most common pattern for shared state:
// dimensions.svelte.ts
export class Dimensions {
width: number = $state(0);
height: number = $state(0);
}// rune_helpers.svelte.ts
export const effect_with_count = (fn: (count: number) => void, initial = 0): void => {
let count = initial;
$effect(() => {
fn(++count);
});
};$inspect.trace()Add as the first line of an $effect or $derived.by to trace dependencies
and discover which one triggered an update:
$effect(() => {
$inspect.trace('my-effect');
// ... effect body
});Prefer keyed each blocks — Svelte can surgically insert or remove items rather than updating existing DOM:
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}The key must uniquely identify the object — do not use the array index.
Avoid destructuring if you need to mutate the item (e.g.,
bind:value={item.count}).
Use style: directive to pass JS values as CSS custom properties:
<div style:--columns={columns}>...</div>
<style>
div { grid-template-columns: repeat(var(--columns), 1fr); }
</style>Prefer CSS custom properties. Use :global only when necessary (e.g.,
third-party components):
<!-- Parent passes custom property -->
<Child --color="red" />
<!-- Child uses it -->
<style>
h1 { color: var(--color); }
</style><!-- :global override (last resort) -->
<div>
<Child />
</div>
<style>
div :global {
h1 { color: red; }
}
</style>Use clsx-style arrays and objects in class attributes instead of class:
directive:
<!-- Do this -->
<div class={['card', active && 'active', size]}></div>
<!-- Not this -->
<div class="card" class:active class:size></div>Always use runes mode. Deprecated patterns and their replacements:
| Instead of | Use |
| ---------------------------------- | --------------------------------------------- |
| let count = 0 (implicit) | let count = $state(0) |
| $: assignments/statements | $derived / $effect |
| export let | $props() |
| on:click={...} | onclick={...} |
| <slot> | {#snippet} / {@render} |
| <svelte:component this={C}> | <C /> (dynamic component directly) |
| <svelte:self> | import Self from './Self.svelte' + <Self> |
| use:action | {@attach} |
| class:active | class={['base', active && 'active']} |
| Stores (writable, readable) | Classes with $state fields |
| Pattern | Use Case |
| -------------------- | --------------------------------------------- |
| $state() | Mutable UI state, form data, class properties |
| $state()! | Class properties initialized by constructor |
| $state.raw() | API responses, ReadonlyArrays, immutable data |
| $state.snapshot() | Plain copy of reactive state for serialization |
| $derived | Simple computed values, class properties |
| $derived.by() | Complex logic, loops, conditionals |
| $effect | Side effects, subscriptions |
| $effect.pre() | Before DOM update, dev-mode validation |
| effect_with_count | Skip initial effect run |
| untrack() | Read without tracking |
| $inspect.trace() | Debug dependency tracking in effects/derived |
| $props() | Component inputs (const or let) |
| $bindable() | Two-way binding props (requires let) |
| {#snippet} | Named content areas |
| {@render} | Render snippets |
| {@attach} | DOM element behaviors (replaces use:) |
| create_context | Typed Svelte context |
| SvelteMap/Set | Reactive Map/Set collections |
| on() (events) | Programmatic event listeners |