Terminal.skills
Skills/svelte-5
>

svelte-5

Build reactive web apps with Svelte 5's new runes system. Use when: building new Svelte 5 apps, migrating from Svelte 4, using runes for reactivity ($state, $derived, $effect, $props), replacing stores with fine-grained state, using snippets instead of slots, or working with SvelteKit 2.

#svelte#svelte-5#runes#reactive#frontend
terminal-skillsv1.0.0
Works with:claude-codeopenai-codexgemini-clicursor
Source

Usage

$
✓ Installed svelte-5 v1.0.0

Getting Started

  1. Install the skill using the command above
  2. Open your AI coding agent (Claude Code, Codex, Gemini CLI, or Cursor)
  3. Reference the skill in your prompt
  4. The AI will use the skill's capabilities automatically

Example Prompts

  • "Review the open pull requests and summarize what needs attention"
  • "Generate a changelog from the last 20 commits on the main branch"

Information

Version
1.0.0
Author
terminal-skills
Category
Development
License
Apache-2.0

Documentation

Overview

Svelte 5 introduces runes — a new reactivity model that replaces $: reactive declarations, let bindings, and Svelte stores with explicit, composable primitives. Runes are functions that start with $ and are processed by the Svelte compiler.

Key changes from Svelte 4:

  • let count = 0let count = $state(0)
  • $: doubled = count * 2let doubled = $derived(count * 2)
  • $: { ... } side effect → $effect(() => { ... })
  • export let proplet { prop } = $props()
  • Svelte stores → $state in .svelte.ts files
  • Slots → Snippets

Runes Reference

$state — reactive state

svelte
<script>
  let count = $state(0);
  let user = $state({ name: "Alice", age: 30 });

  // Deep reactivity: nested properties trigger updates
  function birthday() {
    user.age++; // reactive!
  }
</script>

<button onclick={() => count++}>{count}</button>
<p>{user.name} is {user.age}</p>

$state.raw — shallow (non-deep) reactive value:

svelte
<script>
  // Only the top-level reference is reactive; mutations don't trigger updates
  let items = $state.raw([1, 2, 3]);

  function addItem() {
    items = [...items, items.length + 1]; // must reassign
  }
</script>

$state.snapshot — get a plain (non-reactive) copy:

svelte
<script>
  let form = $state({ name: "", email: "" });

  async function submit() {
    const data = $state.snapshot(form); // plain object for API call
    await fetch("/api", { method: "POST", body: JSON.stringify(data) });
  }
</script>

$derived — computed values

svelte
<script>
  let price = $state(100);
  let quantity = $state(3);

  // Recomputes when price or quantity changes
  let total = $derived(price * quantity);
  let discounted = $derived(total > 200 ? total * 0.9 : total);
</script>

<p>Total: ${total}</p>
<p>With discount: ${discounted.toFixed(2)}</p>

$derived.by — for multi-line derived logic:

svelte
<script>
  let items = $state([{ name: "A", price: 10 }, { name: "B", price: 20 }]);

  let summary = $derived.by(() => {
    const total = items.reduce((sum, item) => sum + item.price, 0);
    const count = items.length;
    return { total, count, avg: count > 0 ? total / count : 0 };
  });
</script>

<p>{summary.count} items, avg ${summary.avg.toFixed(2)}</p>

$effect — side effects

svelte
<script>
  let query = $state("");
  let results = $state([]);

  // Runs when query changes; cleanup runs before next execution
  $effect(() => {
    if (!query) return;
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => { results = data; });

    // Cleanup function (equivalent to useEffect return)
    return () => controller.abort();
  });
</script>

<input bind:value={query} placeholder="Search..." />

$effect.pre — runs before DOM updates:

svelte
<script>
  let messages = $state([]);
  let scrollContainer: HTMLElement;

  $effect.pre(() => {
    // Access messages.length to track changes
    messages.length;
    // Runs before DOM update — useful for scroll position
  });
</script>

$props — component props

svelte
<!-- Button.svelte -->
<script>
  let {
    label,
    variant = "primary",      // default value
    onclick,                   // event handler
    class: className = "",     // renamed (class is reserved)
    ...rest                    // rest props spread to element
  } = $props();
</script>

<button
  class="btn btn-{variant} {className}"
  {onclick}
  {...rest}
>
  {label}
</button>

Usage:

svelte
<Button label="Submit" variant="danger" onclick={() => console.log("clicked")} />

$bindable — two-way binding

svelte
<!-- Input.svelte — exposes value for bind: -->
<script>
  let { value = $bindable(""), placeholder = "" } = $props();
</script>

<input bind:value {placeholder} />
svelte
<!-- Parent.svelte -->
<script>
  import Input from "./Input.svelte";
  let name = $state("");
</script>

<Input bind:value={name} placeholder="Your name" />
<p>Hello, {name}!</p>

$inspect — debug reactive values

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

  // Logs to console whenever count or doubled changes (dev only)
  $inspect(count, doubled);

  // Custom handler
  $inspect(count).with((type, value) => {
    console.log(`[${type}] count =`, value); // type: "init" | "update"
  });
</script>

Snippets (replacing Slots)

Svelte 5 replaces slots with snippets — typed, reusable markup fragments.

Basic snippet

svelte
<!-- Card.svelte -->
<script>
  let { header, children } = $props();
</script>

<div class="card">
  <div class="card-header">
    {@render header()}
  </div>
  <div class="card-body">
    {@render children()}
  </div>
</div>
svelte
<!-- Parent.svelte -->
<Card>
  {#snippet header()}
    <h2>My Card Title</h2>
  {/snippet}
  <p>Card body content here.</p>
</Card>

Snippet with parameters

svelte
<!-- List.svelte -->
<script>
  let { items, row } = $props();
</script>

<ul>
  {#each items as item}
    <li>{@render row(item)}</li>
  {/each}
</ul>
svelte
<!-- Parent.svelte -->
<List {items}>
  {#snippet row(item)}
    <strong>{item.name}</strong> — {item.description}
  {/snippet}
</List>

Migration from Svelte 4

Reactive declarations → $derived

svelte
<!-- Svelte 4 -->
<script>
  let count = 0;
  $: doubled = count * 2;
  $: console.log("count changed:", count);
</script>

<!-- Svelte 5 -->
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  $effect(() => { console.log("count changed:", count); });
</script>

Stores → $state in module

ts
// Svelte 4: stores/counter.ts
import { writable, derived } from "svelte/store";
export const count = writable(0);
export const doubled = derived(count, $c => $c * 2);
ts
// Svelte 5: state/counter.svelte.ts
// Use .svelte.ts extension for runes outside components
let count = $state(0);
let doubled = $derived(count * 2);

export function getCounter() {
  return {
    get count() { return count; },
    get doubled() { return doubled; },
    increment() { count++; },
    reset() { count = 0; },
  };
}
svelte
<!-- Component.svelte -->
<script>
  import { getCounter } from "$lib/state/counter.svelte.ts";
  const counter = getCounter();
</script>

<p>{counter.count} (doubled: {counter.doubled})</p>
<button onclick={counter.increment}>+1</button>

Props: export let$props()

svelte
<!-- Svelte 4 -->
<script>
  export let name;
  export let age = 0;
</script>

<!-- Svelte 5 -->
<script>
  let { name, age = 0 } = $props();
</script>

Slots → Snippets

svelte
<!-- Svelte 4 -->
<div><slot name="header" /><slot /></div>

<!-- Svelte 5 -->
<script>
  let { header, children } = $props();
</script>
<div>{@render header?.()}{@render children?.()}</div>

SvelteKit 2 Compatibility

SvelteKit 2 works with Svelte 5 out of the box. Key patterns:

svelte
<!-- +page.svelte — load data -->
<script>
  let { data } = $props(); // from +page.ts load function
</script>

<h1>{data.title}</h1>
ts
// +page.ts
export async function load({ fetch }) {
  const res = await fetch("/api/posts");
  return { posts: await res.json() };
}

Form actions still work

svelte
<form method="POST" action="?/create">
  <input name="title" />
  <button>Create</button>
</form>

Common Patterns

Toggle with $state

svelte
<script>
  let open = $state(false);
</script>

<button onclick={() => (open = !open)}>
  {open ? "Close" : "Open"}
</button>
{#if open}<div class="modal">Content</div>{/if}

Async derived with $effect

svelte
<script>
  let userId = $state(1);
  let user = $state(null);
  let loading = $state(false);

  $effect(() => {
    loading = true;
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => { user = data; loading = false; });
  });
</script>

{#if loading}<p>Loading...</p>
{:else if user}<p>{user.name}</p>{/if}

Class component pattern (shared state)

ts
// lib/theme.svelte.ts
class ThemeStore {
  current = $state<"light" | "dark">("light");

  toggle() {
    this.current = this.current === "light" ? "dark" : "light";
  }
}

export const theme = new ThemeStore();
svelte
<script>
  import { theme } from "$lib/theme.svelte.ts";
</script>

<button onclick={theme.toggle}>
  Switch to {theme.current === "light" ? "dark" : "light"}
</button>