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.raw() vs $state() β opt into mutation reactivity, raw otherwisePrinciple: be explicit about when you're opting into mutation reactivity.
For primitives the two are equivalent (one extra typeof check on set). For
objects and arrays, $state() proxies the value so in-place mutations trigger
updates; $state.raw() stores the value directly and only tracks reassignment.
Use $state() when you want in-place mutation to trigger reactivity:
- Arrays you push, splice, pop, sort, or index-assign
- Objects with individual property mutations
- bind:value={obj.field} β binding writes to a property on the object, which
needs deep proxy reactivity (binding to a primitive let works either way,
since the binding reassigns the variable)
Use $state.raw() for everything else β primitives, values replaced
wholesale (filter/spread/reassignment), API responses, data passed to APIs
that compare object identity, anything where property-level reactivity isn't
wanted.
This is a fuz-stack stylistic preference, not a technical requirement, and
diverges from Svelte's official guidance β which defaults to $state() and
treats $state.raw as a perf opt-out for large values that are only ever
reassigned (API responses and similar). The benefit here is explicit intent β
reading a state class tells you which fields are designed to mutate in place.
The cost is friction with idiomatic-Svelte reviewers and AI assistants that
default to $state().
structuredClone, JSON.stringify, and postMessage all walk through
$state() proxies cleanly β proxy traps return the target's own keys.
JSON.stringify also calls toJSON() through the proxy.
// $state.raw() β values replaced wholesale or never reassigned
let name = $state.raw(''); // primitive
let api_response = $state.raw<ApiResponse | null>(null); // replaced wholesale
let selections: ReadonlyArray<Item> = $state.raw([]); // array replaced wholesale
// $state() β opt-in for in-place mutation
let items = $state<string[]>([]);
items.push('new'); // triggers reactivity
let form_data = $state({name: '', email: ''});
form_data.name = 'Alice'; // triggers reactivity via proxy
// const objects with property writes need $state()
const config = $state({iterations: 5, warmup: 2});
// bind:value={config.iterations} writes a property; $state.raw() here silently
// breaks (const can't be reassigned, raw doesn't track property writes)Watch for const objects: A const object declared with $state.raw() has
no way to trigger reactivity β it can't be reassigned and property mutations
aren't tracked. If the object's properties are mutated (directly or via
bind:), use $state().
Check consumer files, not just the declaring file. A class field may be
mutated in place by external code that accesses it β e.g., a component importing
a state class and calling thing.items.splice(i, 1). Grep the entire src/
directory for mutation patterns on the field name before deciding.
$state.raw()! Non-null Assertion PatternClass properties initialized by constructor or init() use $state.raw()!:
export class ThemeState {
theme: Theme = $state.raw()!;
color_scheme: ColorScheme = $state.raw()!;
constructor(options?: ThemeStateOptions) {
this.theme = options?.theme ?? default_themes[0]!;
this.color_scheme = options?.color_scheme ?? 'auto';
}
}Used across fuz_ui state classes and zzz Cell subclasses. Use $state()! only
for arrays/objects that are mutated in place (see above).
$state.snapshot()Deep-cloned plain copy of a reactive value. Per Svelte's source: recurses
into plain objects and arrays; for class instances with toJSON(), calls
it and clones the result; otherwise falls through to structuredClone
(which strips class prototypes).
// cell.svelte.ts - encode_property uses snapshot for serialization
encode_property(value: unknown, _key: string): unknown {
return $state.snapshot(value);
}Use it when handing a $state() proxy structure to code that does
reference-identity checks on members and would otherwise see proxy
identities. $state.raw() values holding plain data don't need it at all.
For serialization, JSON.stringify and structuredClone walk through
proxies on their own.
Observed quirk (Svelte 5.55 + vite-plugin-svelte): const r = $state.snapshot(x) is
silently elided to const r = x somewhere in the toolchain (Svelte's
compileModule output is correct, so it's a downstream pass).
return $state.snapshot(x) and inline expression use work correctly.
zzz Cell's encode_property is the direct-return form, so to_json() is
unaffected. If you write const r = $state.snapshot(x) and the snapshot
semantics seem missing, this is the cause.
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 readonly $derived and readonly $derived.by(). Always
mark $derived class properties as readonly unless you explicitly need
reassignment (which Svelte 5 does allow):
// 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
readonly can_collapse = $derived(this.selections.length > 1);
readonly 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
readonly 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.
For entity streams where the same data is consumed by different
lookups, maintain multiple SvelteMap indexes over it β rebuild
on snapshot events, update incrementally on delta events. Deriveds
then use .get() lookups instead of array scans.
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.raw()!;
color_scheme: ColorScheme = $state.raw()!;
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 a Cell base class that automates JSON hydration from
Zod schemas. Same rune conventions ($state.raw()! by default, $state()!
for in-place mutations, readonly $derived for computed values). See
./zod-schemas for the full schema/class pattern.
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.
For an inventory of contexts in fuz_ui and zzz, grep for create_context< in
the source.
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>Children can be parameterized β Dialog passes a close function back to the consumer:
<!-- Dialog.svelte -->
<script lang="ts">
const {children}: {
children: Snippet<[close: (e?: Event) => void]>;
} = $props();
</script>
{@render children(close)}ThemeRoot uses the same pattern with multiple values:
Snippet<[theme_state: ThemeState, style: string | null, theme_style_html: string | null]>.
<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}For optional snippets, fall back with {#if snippet} {@render snippet()} {:else} ... {/if}.
For props that accept either a string or a snippet (e.g. icon?: string | Snippet),
branch on typeof at render. fuz_ui's Card and Alert use this; Alert further
parameterizes with Snippet<[icon: string]> to pass the resolved icon back.
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 or timers:
$effect(() => {
const interval = setInterval(() => {
tick_count++;
}, 1000);
// Cleanup runs before next effect and on destroy
return () => clearInterval(interval);
});For window/document listeners, prefer <svelte:window onkeydown={...}> and
<svelte:document> over $effect + addEventListener. For element-scoped
listeners, prefer {@attach} (with on() from svelte/events inside).
$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,
.svelte.ts files, and plain .ts modules. Preserves correct ordering
relative to declarative handlers that use event delegation. Always prefer
on() over addEventListener β even in non-component code. Returns a
cleanup function:
import {on} from 'svelte/events';
// Inside an attachment or module
const cleanup = on(element, 'scroll', onscroll);
return () => cleanup();
// With options (e.g., passive: false for wheel events)
const cleanup = on(element, 'wheel', onwheel, {passive: false});swallow β Claiming Eventsswallow() from @fuzdev/fuz_util/dom.js combines preventDefault() and
stopImmediatePropagation() (or stopPropagation() with immediate: false).
Design principle: handling an event = claiming it. If you call
preventDefault, you're already saying "I own this event's default behavior."
Use swallow to extend that to "and no one else should react to it either."
If a parent needs to observe events before children claim them, use the
capture phase explicitly β don't rely on implicit bubbling.
import {swallow} from '@fuzdev/fuz_util/dom.js';
// swallow(event, immediate?, preventDefault?)
swallow(e); // preventDefault + stopImmediatePropagation (default)
swallow(e, false); // preventDefault + stopPropagation (non-immediate)
swallow(e, true, false); // stopImmediatePropagation only (no preventDefault)Use swallow whenever you would call preventDefault β the event is yours,
stop it from propagating too. For handlers that only need stopPropagation
without preventDefault (e.g., preventing game input from seeing keystrokes
in a chat input), use e.stopPropagation() directly.
<!-- Claiming an event in a handler -->
<script lang="ts">
import {swallow} from '@fuzdev/fuz_util/dom.js';
const on_keydown = (e: KeyboardEvent): void => {
if (e.key === 'Enter') {
swallow(e);
send();
} else if (e.key === 'Escape') {
swallow(e);
close();
} else {
// only stop propagation, don't prevent default (e.g., typing characters)
e.stopPropagation();
}
};
</script>// Programmatic listener claiming context menu and wheel events
const cleanup_contextmenu = on(canvas, 'contextmenu', (e) => {
swallow(e);
});
const cleanup_wheel = on(canvas, 'wheel', (e) => {
handle_zoom(e);
swallow(e);
}, {passive: false});<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. Prefer classes over module-level state β export a class,
instantiate once at the appropriate root, and share it via context.
Don't declare $state variables at module scope and expose them through
getter/setter objects. A module-level rune is a hidden global: it can't be
reset per test, per realm, or per session; it ties the lifetime of the
state to the module rather than to a component; and a second instance is
impossible when you later decide you need one.
// Anti-pattern: module-level runes exposed through a singleton
let show_map = $state.raw(false);
let show_sidebar = $state.raw(true);
export const world_ui = {
get show_map() { return show_map; },
set show_map(v: boolean) { show_map = v; },
get show_sidebar() { return show_sidebar; },
set show_sidebar(v: boolean) { show_sidebar = v; },
};Use a class + context instead β the class owns its state, and a root component sets it once:
// world_ui_state.svelte.ts
import {create_context} from '@fuzdev/fuz_ui/context_helpers.js';
export const world_ui_context = create_context<WorldUiState>();
export class WorldUiState {
show_map: boolean = $state.raw(false);
show_sidebar: boolean = $state.raw(true);
}<!-- +layout.svelte or similar root component -->
<script>
import {WorldUiState, world_ui_context} from '$lib/world_ui_state.svelte.js';
world_ui_context.set(new WorldUiState());
</script><!-- any descendant component -->
<script>
import {world_ui_context} from '$lib/world_ui_state.svelte.js';
const world_ui = world_ui_context.get();
</script>When module-level runes are fine: inside a factory function body (see below) β the state is scoped to the returned object, not the module.
// 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.raw(0);
height: number = $state.raw(0);
}Canvas2D/WebGPU renderers, requestAnimationFrame loops, and
long-lived pointer listeners are the inverse case: use a plain
class with no runes, mounted by a thin .svelte wrapper. Private
fields (e.g. #hovered_id, #cursor_x) stay non-reactive on purpose
β mutating them from an rAF tick must not schedule reruns. The
wrapper binds dimensions, forwards reactive sources via
getter-backed options, and calls destroy() on unmount. Runes live
in the wrapper, never in the loop.
$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}).
Goal: minimal <style> blocks. Components should delegate styling to
fuz_css utility classes and design tokens. Many well-designed components
have no <style> block at all. See css-patterns.md Β§Component Styling
Philosophy for the full rationale, anti-patterns, and examples.
When a <style> block is needed, keep it focused on component-specific
layout logic (positioning, complex pseudo-states, responsive breakpoints).
All values should reference design tokens, not hardcoded pixels or colors.
Class naming: fuz_css utilities use snake_case (p_md, gap_lg).
Component-local classes use kebab-case (site-header, nav-links) to
distinguish them visually.
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 |
The decision-fraught choices, summarized:
- $state.raw() vs $state() β $state.raw() for primitives and values
replaced wholesale; $state() when you want in-place mutation (push,
property writes, bind: on object properties) to trigger reactivity.
- $derived vs $derived.by() β $derived takes an expression;
$derived.by() takes a function for loops/conditionals/multi-step logic.
Mark class-level deriveds readonly.
- {@attach} vs $effect β attachments for element behavior (replaces
use:action); effects for everything else, but reach for $derived,
<svelte:window>, or event handlers first.
- create_context<T>() vs raw setContext/getContext β fuz_ui's
create_context provides the throw-on-missing get() plus get_maybe(),
with optional fallback factory.