Files
crumbs/.agents/skills/svelte5-best-practices/references/runes.md
T
Frederic CabassutandClaude Opus 4.6 02e6b57764 chore: add agent skills for Svelte 5, SvelteKit, TypeScript, and Drizzle
Install project-level skills via skills.sh CLI:
- svelte5-best-practices (runes, snippets, events, migration)
- sveltekit-structure (routing, layouts, error boundaries)
- sveltekit-data-flow (load functions, form actions, serialization)
- typescript-best-practices (type-first development, discriminated unions)
- drizzle-migrations (SQLite schema, query patterns)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:11:36 +01:00

7.0 KiB

Svelte 5 Runes Reference

Table of Contents


$state - Reactive State

In Svelte 5, plain let declarations are no longer automatically reactive. Use $state() for reactive state.

Basic Usage

<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>
  Clicks: {count}
</button>

Object and Array State

Objects and arrays are deeply reactive by default:

<script>
  let user = $state({ name: 'Alice', age: 30 });
  let items = $state(['apple', 'banana']);
</script>

<button onclick={() => user.age++}>Age: {user.age}</button>
<button onclick={() => items.push('cherry')}>Items: {items.length}</button>

Class State

<script>
  class Counter {
    count = $state(0);
    increment() { this.count++; }
  }
  const counter = new Counter();
</script>

<button onclick={() => counter.increment()}>{counter.count}</button>

Raw State (Opt-out of Deep Reactivity)

<script>
  let items = $state.raw([1, 2, 3]);

  // This WON'T trigger an update:
  items.push(4);

  // This WILL trigger an update:
  items = [...items, 4];
</script>

$derived - Computed Values

Replaces $: reactive statements for computed values.

Basic Usage

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  let quadrupled = $derived(doubled * 2);
</script>

Complex Derivations with $derived.by

<script>
  let items = $state([1, 2, 3, 4, 5]);
  let filter = $state('even');

  let filteredItems = $derived.by(() => {
    if (filter === 'even') return items.filter(n => n % 2 === 0);
    return items.filter(n => n % 2 !== 0);
  });
</script>

Deriving from Props

<script>
  let { firstName, lastName } = $props();
  let fullName = $derived(`${firstName} ${lastName}`);
</script>

$derived vs $effect

Use $derived for computing values, $effect for side effects:

<script>
  let count = $state(0);

  // CORRECT: Use $derived for computed values
  let doubled = $derived(count * 2);

  // INCORRECT: Don't use $effect to set derived values
  // let doubled;
  // $effect(() => { doubled = count * 2; }); // Anti-pattern!
</script>

$effect - Side Effects

Runs code when component mounts and when dependencies change. Requires cleanup for subscriptions.

Basic Effect with Cleanup

<script>
  let count = $state(0);

  $effect(() => {
    const interval = setInterval(() => count++, 1000);
    return () => clearInterval(interval); // Cleanup
  });
</script>

DOM Event Listeners

<script>
  let mouseX = $state(0);
  let mouseY = $state(0);

  $effect(() => {
    function handleMouseMove(event) {
      mouseX = event.clientX;
      mouseY = event.clientY;
    }
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  });
</script>

External Subscriptions

<script>
  let { userId } = $props();
  let userData = $state(null);

  $effect(() => {
    const unsubscribe = database.subscribe(`users/${userId}`, (data) => {
      userData = data;
    });
    return () => unsubscribe();
  });
</script>

$effect.pre for Pre-DOM Updates

<script>
  let div;
  let messages = $state([]);

  $effect.pre(() => {
    if (div) {
      const shouldScroll = div.scrollTop + div.clientHeight >= div.scrollHeight - 20;
      if (shouldScroll) {
        tick().then(() => { div.scrollTop = div.scrollHeight; });
      }
    }
  });
</script>

Untracked Dependencies

<script>
  import { untrack } from 'svelte';

  let count = $state(0);
  let logCount = $state(0);

  $effect(() => {
    // Only runs when count changes, not logCount
    console.log(count, untrack(() => logCount));
  });
</script>

$props - Component Props

Replaces export let for declaring component props.

Basic Usage

<script>
  let { name, count = 0, disabled = false } = $props();
</script>

Rest Props Pattern

<script>
  let { class: className, children, ...restProps } = $props();
</script>

<div class={className} {...restProps}>
  {@render children?.()}
</div>

Renaming Reserved Words

<script>
  let {
    class: className,  // 'class' is reserved
    for: htmlFor,      // 'for' is reserved
    ...rest
  } = $props();
</script>

TypeScript Props

<script lang="ts">
  interface Props {
    name: string;
    count?: number;
    onClick?: (event: MouseEvent) => void;
  }

  let { name, count = 0, onClick }: Props = $props();
</script>

Props with Children

<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    title: string;
    children: Snippet;
    footer?: Snippet;
  }

  let { title, children, footer }: Props = $props();
</script>

<article>
  <h1>{title}</h1>
  <main>{@render children()}</main>
  {#if footer}<footer>{@render footer()}</footer>{/if}
</article>

$bindable - Two-Way Binding

Props must be explicitly marked with $bindable() to support bind:.

Basic Usage

<!-- Input.svelte -->
<script>
  let { value = $bindable() } = $props();
</script>

<input bind:value />

<!-- Parent.svelte -->
<script>
  import Input from './Input.svelte';
  let name = $state('');
</script>

<Input bind:value={name} />

Bindable with Default Value

<script>
  let { value = $bindable('default') } = $props();
</script>

Multiple Bindable Props

<script>
  let {
    value = $bindable(''),
    checked = $bindable(false),
    selected = $bindable(null)
  } = $props();
</script>

TypeScript with Bindable

<script lang="ts">
  interface Props {
    value: string;
    disabled?: boolean;
  }

  let { value = $bindable(''), disabled = false }: Props = $props();
</script>

$inspect - Debugging

Development-only debugging that logs values when they change. Stripped in production.

Basic Usage

<script>
  let count = $state(0);
  $inspect(count); // Logs every time count changes
</script>

Multiple Values

<script>
  let name = $state('Alice');
  let age = $state(30);
  $inspect(name, age);
</script>

Custom Logging with $inspect.with

<script>
  let user = $state({ name: 'Alice', age: 30 });

  $inspect.with((type, ...values) => {
    if (type === 'init') console.log('Initial value:', values);
    else console.log('Updated to:', values);
  }, user);
</script>

Debugging with Breakpoints

<script>
  let data = $state({ x: 0, y: 0 });

  $inspect.with((type, value) => {
    if (type === 'update' && value.x > 100) debugger;
  }, data);
</script>