Svelte 5 完全ガイド【2026年最新】|Runes($state・$derived・$effect・$props・$bindable・$inspect・$host)・Snippets・{@render}・Universal Reactivity・Svelte 4 からの移行まで解説

Svelte 5 完全ガイド【2026年最新】|Runes($state・$derived・$effect・$props・$bindable・$inspect・$host)・Snippets・{@render}・Universal Reactivity・Svelte 4 からの移行まで解説 TypeScript

Svelte 5 は「コンパイラの魔法でリアクティブになる」Svelte 4 までの仕組みを捨て、Runes という明示的・ランタイム・ユニバーサルなリアクティビティに生まれ変わりました。2024 年 10 月の 5.0 安定版リリース以降、$state / $derived / $effect / $props / $bindable / $inspect / $host の 7 つの Runes と、Snippets による柔軟なコンテンツ再利用が Svelte の新しい基盤になりました。

let 変数が勝手にリアクティブになる「黒魔術」は、わかりやすい反面「どこまでがリアクティブか」「コンポーネント外でも動くか」という疑問に曖昧さを残していました。Runes は「リアクティブにしたい値は $state で明示する・コンポーネント外でも同じ仕組みで動く」という原則でこれを解消します。この記事では 2026 年 4 月時点の Svelte 5.x を前提に、全 Runes の使い方、Snippets、Universal Reactivity、Svelte 4 からの移行、SvelteKit との連携、TypeScript 型付け、落とし穴までを体系的に解説します。

スポンサーリンク

Svelte 4 と Svelte 5 のパラダイム比較

まずは全体像を把握するために、Svelte 4 の書き方が Svelte 5 でどう変わるかを一覧で見ておきます。

目的 Svelte 4 Svelte 5
リアクティブ変数 let count = 0; let count = $state(0);
派生値 $: double = count * 2; let double = $derived(count * 2);
副作用 $: { console.log(count); } $effect(() => console.log(count));
プロパティ export let name; let { name } = $props();
双方向バインド export let value; let { value = $bindable() } = $props();
イベント on:click={fn} onclick={fn}
子要素 <slot /> {@render children()}
名前付きスロット <slot name="header"> Snippets を props で受け取る
イベント発行 createEventDispatcher() コールバック props
コンポーネント外の状態 Stores(writable .svelte.js で $state 直接
本質的な変化: Svelte 4 は「コンパイル時にコンポーネント内の let をリアクティブに書き換える」仕組みで、.svelte ファイル内でしか魔法が効きませんでした。Svelte 5 の Runes はランタイムのシグナルベースで動くため、.svelte / .svelte.js / .svelte.ts を問わず同じ書き方でリアクティブになります(= Universal Reactivity)。これにより Store という別概念を覚える必要が消えました。

$state ── リアクティブ状態の宣言

$state() で包んだ値は、参照するあらゆる場所(テンプレート・派生値・副作用)に変更が自動伝搬します。プリミティブも配列もオブジェクトもそのまま書けます。

Counter.svelte ── 基本形
<script>
  let count = $state(0);

  function increment() {
    count += 1;               // 代入でそのまま更新できる(Store の .set() 不要)
  }
</script>

<button onclick={increment}>
  clicked {count} {count === 1 ? "time" : "times"}
</button>

オブジェクト・配列は「深くリアクティブ」

Todo.svelte ── 配列操作もそのまま反映
<script>
  let todos = $state([
    { id: 1, text: "Svelte 5 を学ぶ", done: false },
  ]);

  function add() {
    todos.push({ id: Date.now(), text: "", done: false });   // OK
  }

  function toggle(i) {
    todos[i].done = !todos[i].done;                          // OK
  }
</script>

{#each todos as todo, i (todo.id)}
  <label>
    <input type="checkbox" checked={todo.done} onchange={() => toggle(i)} />
    {todo.text}
  </label>
{/each}
<button onclick={add}>追加</button>
Proxy ベースの深いリアクティビティ: $state(obj) はオブジェクトを Proxy でラップし、ネストした代入まで検出します。ただし Proxy なので obj === $state(obj) は falseです。DOM API や外部ライブラリに渡す時は $state.snapshot(obj) で素の値に戻せます。

.svelte.js / .svelte.ts でも同じ書き方

counter.svelte.ts ── コンポーネント外でもリアクティブ
// .svelte.ts 拡張子なら runes が使える
export function createCounter(initial = 0) {
  let count = $state(initial);

  return {
    get count() { return count; },
    increment() { count += 1; },
    reset() { count = 0; },
  };
}
利用側 ── Store と同じように使える
<script>
  import { createCounter } from "./counter.svelte.ts";
  const counter = createCounter(10);
</script>

<button onclick={counter.increment}>
  {counter.count}
</button>
getter / setter パターンがイディオム: クロージャ内の $state 変数は再代入で更新されるため、外部に公開するときは get count() { return count; } の形にするのが定石です。直接 return { count } と書くと、オブジェクト生成時点の値が固定されてしまいます。

$derived ── 派生値の計算

$derived() は依存先の値が変わると自動再計算される派生値です。Svelte 4 の $: ラベルの置換ですが、関数として切り出しても依存追跡が効くのが大きな違いです。

基本と $derived.by で式をブロック化
<script>
  let width  = $state(10);
  let height = $state(5);

  // ① インライン
  let area = $derived(width * height);

  // ② 複雑なロジックは $derived.by で関数形式
  let category = $derived.by(() => {
    if (area < 20)   return "small";
    if (area < 100)  return "medium";
    return "large";
  });
</script>

<input type="number" bind:value={width}  />
<input type="number" bind:value={height} />
<p>面積: {area}({category})</p>
依存追跡がランタイム: Svelte 4 の $: はコンパイル時に依存を解析していたため、関数に切り出すと追跡が切れる制限がありました。$derived は実行時に参照された $state を追跡するため、任意の関数呼び出しを経由しても正しく依存が捕捉されます。このおかげで「ロジックを関数に切り出す」リファクタリングが安全に行えます。

$effect ── 副作用の実行

$effect() はリアクティブ値の変更に応じて副作用を走らせます。DOM 直接操作・外部 API 通知・ログ出力・サードパーティライブラリとの同期などに使います。コンポーネントの mount 時と依存変更時に実行され、戻り値の関数が unmount 時に呼ばれるのが React の useEffect と同じ設計です。

Canvas.svelte ── ライブラリとの同期
<script>
  import Chart from "chart.js/auto";
  let canvas;
  let data = $state([1, 2, 3]);

  $effect(() => {
    const chart = new Chart(canvas, {
      type: "bar",
      data: { labels: data.map((_, i) => i), datasets: [{ data }] },
    });
    return () => chart.destroy();      // cleanup(unmount / 再実行前)
  });
</script>

<canvas bind:this={canvas}></canvas>
<button onclick={() => data.push(Math.random() * 10)}>追加</button>

$effect.pre ── DOM 更新前に走らせる

Svelte 4 の beforeUpdate 置換
<script>
  let messages = $state([]);
  let box;
  let autoscroll = $state(true);

  $effect.pre(() => {
    if (!box) return;
    // 更新後にスクロール末尾だったか事前判定
    autoscroll = box.scrollTop + box.clientHeight >= box.scrollHeight - 4;
  });

  $effect(() => {
    if (autoscroll) box.scrollTop = box.scrollHeight;
  });
</script>
$effect でループを回すな: $effect の中で $state を書き換えると、別の $effect が発火して……という連鎖で無限ループの温床になります。「$derived で書けるものは $derived に寄せる」を鉄則にして、$effect は外部世界との同期(DOM / ストレージ / ネットワーク)に限定するのが健全です。

$props ── プロパティの受け取り

基本 ── 分割代入とデフォルト値
<script lang="ts">
  interface Props {
    title: string;
    subtitle?: string;
    size?: "sm" | "md" | "lg";
  }

  let { title, subtitle = "", size = "md" }: Props = $props();
</script>

<h2 class="size-{size}">{title}</h2>
{#if subtitle}<p>{subtitle}</p>{/if}

Rest Props と $props.id()

Input.svelte ── 残りを DOM に渡す + ID 発行
<script lang="ts">
  let { label, ...rest }: { label: string; [k: string]: any } = $props();
  const uid = $props.id();    // コンポーネントインスタンス固有の安定 ID
</script>

<label for={uid}>{label}</label>
<input id={uid} {...rest} />
$props.id()(Svelte 5.20+): SSR / CSR の両方で同じ ID を発行するため、aria-labelledby / for のようなアクセシビリティ連携で Hydration 不一致を避けられます。2026 年はこのパターンが標準です。

$bindable ── 親へ値を書き戻す

TextField.svelte ── 双方向バインドを許可
<script lang="ts">
  interface Props { value: string; placeholder?: string; }
  // $bindable() を付けると親側で bind: が使える
  let { value = $bindable(""), placeholder = "" }: Props = $props();
</script>

<input {placeholder} bind:value />
親コンポーネント
<script>
  import TextField from "./TextField.svelte";
  let query = $state("");
</script>

<TextField bind:value={query} placeholder="検索..." />
<p>現在の値: {query}</p>
$bindable の守備範囲: Svelte 4 では「子で export let value すれば自動で双方向」でしたが、意図しない書き戻しでバグを生みやすい仕様でした。Svelte 5 は $bindable() を書いたプロパティだけが双方向許可されるため、コンポーネントの API が明示的になります。

$inspect ── リアクティブなデバッグ出力

Svelte 5 の専用デバッガ
<script>
  let user = $state({ name: "Alice", age: 25 });

  // 値が変わるたびに console に出力(初回は init、以降は update でラベル付け)
  $inspect(user);

  // 出力方法をカスタム
  $inspect(user).with((type, value) => {
    if (type === "update") {
      console.warn(`user updated:`, value);
    }
  });
</script>
console.log との違い: $inspect は Svelte コンパイラに認識されるため、依存に変化があった時だけ走る最適化が効きます。本番ビルドでは自動的に削除されるので、コードに残っても本番バンドルに影響しません。

$host ── カスタム要素内でのホスト要素参照

Web Component として公開する時だけ使う
<svelte:options customElement="my-counter" />
<script>
  let count = $state(0);

  function dispatch() {
    // Custom Element のホスト要素を取得
    $host().dispatchEvent(new CustomEvent("tick", { detail: count }));
  }

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

<span>{count}</span>
$host は特殊用途: <svelte:options customElement="..."> を付けて Web Component として配布するコンポーネントの中でしか使いません。ホスト要素にイベントを発火させる・属性を読み取る等、Custom Elements API との橋渡しに使います。

Snippets と {@render} ── スロットの完全置換

Svelte 5 では <slot> が廃止され、Snippets に置き換わりました。Snippets は「コンポーネント内外で呼び出せる、引数を取れるテンプレート断片」で、スロットより表現力が上がっています。

子要素を受け取る ── children

Card.svelte
<script lang="ts">
  import type { Snippet } from "svelte";
  let { children }: { children: Snippet } = $props();
</script>

<div class="card">
  {@render children()}
</div>
親から使う
<Card>
  <p>任意のコンテンツ</p>
</Card>

名前付きコンテンツ(旧・名前付きスロットの置換)

Layout.svelte ── header / footer を受け取る
<script lang="ts">
  import type { Snippet } from "svelte";
  let {
    header,
    children,
    footer,
  }: { header?: Snippet; children: Snippet; footer?: Snippet } = $props();
</script>

<header>{#if header}{@render header()}{/if}</header>
<main>{@render children()}</main>
<footer>{#if footer}{@render footer()}{/if}</footer>
親から渡す
<Layout>
  {#snippet header()}
    <h1>タイトル</h1>
  {/snippet}

  本文コンテンツ

  {#snippet footer()}
    <small>© 2026 codingls</small>
  {/snippet}
</Layout>

引数つき Snippet ── リスト描画の再利用

List.svelte
<script lang="ts">
  import type { Snippet } from "svelte";
  let {
    items,
    row,
  }: { items: Item[]; row: Snippet<[Item, number]> } = $props();
</script>

<ul>
  {#each items as item, i}
    <li>{@render row(item, i)}</li>
  {/each}
</ul>
親から引数ありで呼ぶ
<List {items}>
  {#snippet row(item, i)}
    <strong>{i + 1}.</strong> {item.title}
  {/snippet}
</List>
Snippet は関数相当: コンパイル後は引数を取る関数になるため、{@render snippet(a, b)} で呼び出すだけで再利用できます。同じ {#snippet} を複数箇所で {@render} してもよく、「リスト行・ツールチップ・モーダルの本文」のような部品化に向いています。

イベントハンドラとコールバック Props

DOM イベント ── on:click → onclick
<!-- Svelte 4 -->
<button on:click={handler} on:click|preventDefault={submit}>

<!-- Svelte 5 -->
<button onclick={handler} onclick={(e) => { e.preventDefault(); submit(); }}>

コンポーネントイベントの新作法

LikeButton.svelte ── createEventDispatcher 廃止
<script lang="ts">
  interface Props {
    count: number;
    onlike?: (count: number) => void;   // ← コールバック props
  }
  let { count, onlike }: Props = $props();
</script>

<button onclick={() => onlike?.(count + 1)}>
  ♥ {count}
</button>
親 ── onlike として受け取るだけ
<LikeButton count={likes} onlike={(n) => likes = n} />
イベント修飾子(|preventDefault, |stopPropagation, |once 等)は廃止されました。 自分でラッパを書くか、ユーティリティ関数を用意します: onclick={(e) => (e.preventDefault(), handler())}。CapLock トリガーのような特殊イベントもハンドラ内で判定してください。

Universal Reactivity ── .svelte.js / .svelte.ts の威力

Svelte 4 の writable / readable / derived Store は、コンポーネント外でリアクティブを実現するための別 APIでした。Svelte 5 は .svelte.js / .svelte.ts 拡張子のファイル内で $state / $derived / $effect がそのまま動くため、Store という別概念を学ぶ必要がなくなりました。

cart.svelte.ts ── ショッピングカートの共有状態
export type CartItem = { sku: string; qty: number };

function createCart() {
  let items = $state<CartItem[]>([]);
  let total = $derived(items.reduce((sum, it) => sum + it.qty, 0));

  return {
    get items() { return items; },
    get total() { return total; },
    add(sku: string) {
      const found = items.find((it) => it.sku === sku);
      if (found) found.qty += 1;
      else items.push({ sku, qty: 1 });
    },
    remove(sku: string) {
      items = items.filter((it) => it.sku !== sku);
    },
    clear() { items = []; },
  };
}

export const cart = createCart();
任意のコンポーネントから使う
<script>
  import { cart } from "./cart.svelte.ts";
</script>

<p>カート: {cart.total} 点</p>
<button onclick={() => cart.add("SKU-001")}>追加</button>
{#each cart.items as item (item.sku)}
  <li>{item.sku} x {item.qty}</li>
{/each}
「Svelte Store を使うべきか Runes を使うべきか?」の答え: 2026 年 4 月時点ではほぼすべてのユースケースで Runes 推奨です。既存プロジェクトの writable は暫く残りますが、新規はすべて .svelte.ts + $state で書き、既存 Store は段階的に置換するのが王道。型推論も Runes の方が自然に効きます。

TypeScript での型付けパターン

.svelte.ts で型安全な State ファクトリ
type User = { id: string; name: string; email: string };

export function createUserState() {
  let user = $state<User | null>(null);
  let loading = $state(false);
  let error = $state<Error | null>(null);

  async function load(id: string) {
    loading = true; error = null;
    try {
      const res = await fetch(`/api/users/${id}`);
      user = (await res.json()) as User;
    } catch (e) {
      error = e as Error;
    } finally {
      loading = false;
    }
  }

  return {
    get user() { return user; },
    get loading() { return loading; },
    get error() { return error; },
    load,
  };
}
Props に型を付ける(ジェネリック含む)
<script lang="ts" generics="T">
  import type { Snippet } from "svelte";
  interface Props<T> {
    items: T[];
    row: Snippet<[T, number]>;
  }
  let { items, row }: Props<T> = $props();
</script>

<ul>
  {#each items as item, i}<li>{@render row(item, i)}</li>{/each}
</ul>

Svelte 4 → 5 移行手順

依存アップデートと自動移行ツール
# 1) Svelte 5 にアップグレード
npm install svelte@5 @sveltejs/kit@latest
# TypeScript を使っているなら
npm install -D typescript@latest svelte-check@latest

# 2) 公式マイグレーションスクリプト(破壊的に書き換える)
npx sv migrate svelte-5

# 3) 差分確認(必ず git diff でレビュー)
git diff

# 4) 開発サーバーで警告を拾う
npm run dev

# 5) 型チェック
npm run check

自動で置換される範囲

  • let count = 0;let count = $state(0);(テンプレートで参照されている場合のみ)
  • $: double = count * 2;let double = $derived(count * 2);
  • $: { /* 副作用 */ }$effect(() => { /* 副作用 */ });
  • export let title;let { title } = $props();
  • on:clickonclick(基本ケース)
  • <slot />{@render children()}

手作業が残る範囲

  • createEventDispatcher → コールバック props(API 設計変更なので手作業)
  • イベント修飾子(|preventDefault 等)→ ハンドラ内で実装
  • beforeUpdate / afterUpdate$effect.pre / $effect(意図判定が難しい)
  • Store を使っていたコード → .svelte.ts + $state 化(任意、徐々に)
  • 複雑な $: ロジック(代入と副作用の混在)→ $derived$effect への分離
Svelte 4 と 5 はコンポーネントレベルで混在可能: 移行は大規模 PR で一気にやるのではなく、「葉コンポーネントから順に Runes 化 → props で親に伝搬 → 最後にエントリポイント」が安全。npx sv migrate svelte-5 も 1 ディレクトリごとに走らせて diff をレビューしていきます。

SvelteKit と Svelte 5

SvelteKit 2(2024〜)以降は Svelte 5 を正式サポートし、+page.svelte / +layout.svelte / +page.server.ts のすべてで Runes が使えます。ルーティング・フォームアクション・ロード関数は変わらず、コンポーネント内の書き方だけが Runes に変わったと捉えると分かりやすいです。

+page.svelte ── SvelteKit での典型形
<script lang="ts">
  import type { PageData, ActionData } from "./$types";

  let { data, form }: { data: PageData; form: ActionData } = $props();

  // data.items は PageData から型推論される
  let filter = $state("");
  let filtered = $derived(
    data.items.filter((it) => it.title.includes(filter))
  );
</script>

<input bind:value={filter} placeholder="検索..." />

<form method="POST" action="?/create" use:enhance>
  <input name="title" required />
  <button>作成</button>
  {#if form?.error}<p class="err">{form.error}</p>{/if}
</form>

{#each filtered as it (it.id)}
  <article>{it.title}</article>
{/each}

落とし穴と注意点

$state は Proxy なので参照等価性が変わる

const a = { x: 1 }; const b = $state(a); の時、a !== b です。DOM API や外部ライブラリに「同じオブジェクトを渡した」はずなのに別物扱いされる事故が起きます。必要なら $state.snapshot(b) で素の値を取り出してください。

$effect 内で state を書き換える無限ループ

$effect の中で count += 1 を書くと、その $effect が再度発火して……と無限ループになります。Svelte はある程度検出してくれますが、原則「$effect では state を変えない」を守るのが安全です。値の派生は $derived に寄せます。

コンポーネント外で $state を使うには .svelte.js / .svelte.ts 拡張子が必要

普通の .ts.js では Runes は使えません(コンパイラが処理しない)。ファイル名を cart.svelte.ts にするだけで有効化されます。VSCode / svelte-check の型エラーが出ないのに実行時に動かない場合は、まずファイル名を確認してください。

getter / setter を忘れると state が固定化する

return { count, increment }; と書くと、return した時点の count 値がスナップショットされて固定されます。必ず return { get count() { return count; }, increment }; のように getter で包みます。

Svelte 4 のイベント修飾子が無くなっている

on:click|preventDefault のような修飾子記法が 5 では動きません。自動マイグレーションでも警告のまま残るケースがあるので、検索して置換してください。

旧 Stores(writable)と Runes の混在

Svelte 4 Stores と Runes は共存できますが、データの同一ソースを両方で扱うと混乱します。移行時は「1 つの状態は Store か Runes のどちらかに寄せる」を徹底し、徐々に Runes へ統一していきます。

よくある質問

QSvelte 5 の Runes は既存プロジェクトで必須ですか?
A必須ではありません。Svelte 5 は Svelte 4 の書き方もサポートし続けるため、Runes なしでも動きます。ただし「新規コンポーネントは Runes、既存は段階的に移行」が公式推奨で、長期的には Runes 前提に移行するのが自然です。<svelte:options runes={true} /> を書かないコンポーネントは Svelte 4 互換モードで動作します。
QSvelte 5 と React 19 はどちらを選ぶべきですか?
A新規チーム採用でタイピング重視・DX 重視なら Svelte 5、既存のエコシステムの豊富さ・求人市場を考えるなら React 19 が無難です。Svelte 5 は Runes により React の useState / useMemo / useEffect / useContext に相当する機能を $state / $derived / $effect で表現できるため、概念の対応は非常にスムーズです。React 19 の詳細は React 19 完全ガイド を参照してください。
QSvelteKit 2 で Svelte 5 を使うには何か特別な設定が必要ですか?
ASvelteKit 2 系を最新にすれば Svelte 5 を使えます。新規プロジェクトは npx sv create で作成すると Svelte 5 + SvelteKit 2 最新構成で生成されます。既存 SvelteKit プロジェクトは svelte@sveltejs/kit の両方を最新にし、npx sv migrate svelte-5 を走らせるのが早道です。
QRunes は .ts ファイルでも使えますか?
A通常の .ts では使えず、.svelte.ts 拡張子のファイル内でのみ有効です。これはコンパイラが .svelte.* の付いたファイルだけを処理対象にしているためです。共通ロジックを Runes 化する際は、ファイルを xxx.svelte.ts にリネームしてから $state を使う運用になります。
QSvelte 5 で Store(writable / readable)は完全に廃止ですか?
A廃止ではなく、後方互換で動作し続けます。ただし Svelte チームは「Runes を新標準」と位置付けており、ドキュメントも Runes 中心に書き換えられています。新規コードは Runes、既存 Store は安定して動くので急いで置換する必要はない、というのが実務的な温度感です。
QSnippets は React の children / render prop と何が違いますか?
Aコンセプトは似ています。Snippet は「Svelte コンポーネント内で定義できる再利用可能な断片」で、React の render prop + children を同じ記法で書けるのが強みです。{#snippet name(a, b)}...{/snippet} で定義し、{@render name(1, 2)} で呼び出す一貫した API になっています。
QTypeScript でコンポーネントの型を他のファイルから参照できますか?
Aはい。ComponentProps<typeof MyComponent> で props の型を取り出せます。Svelte 5 は内部的にコンポーネントを関数として扱うため、型レベルの連携が React に近い形になりました。Snippet の型は Snippet<[引数タプル]> で定義できます。
QSvelte 5 を Astro の Islands として使えますか?
A使えます。npx astro add svelte でインテグレーションを追加すると、Svelte 5 コンポーネントを client:load などで島として埋め込めます。Astro 全体の設計は Astro 完全ガイド を参照してください。コンテンツ主体のページは Astro、インタラクティブな島を Svelte で書く組合せは、バンドルサイズを最小化できる 2026 年の強力な構成です。

まとめ

  • Svelte 5 の本質は Runes による「明示的・ランタイム・ユニバーサルなリアクティビティ」。let 変数の自動反応という魔法を捨て、$state で明示する設計に
  • 7 つの Runes: $state / $derived / $effect / $props / $bindable / $inspect / $host。$derived は関数切り出しても依存追跡が効くのが Svelte 4 との大差
  • Snippets と {@render} がスロットを置換。引数を渡せて名前付きにできる、React の render prop 相当が公式機能に
  • Universal Reactivity: .svelte.js / .svelte.ts で $state をそのまま書ける。Store は事実上不要
  • イベントは onclick などの属性形式on:click と修飾子(|preventDefault 等)は廃止。createEventDispatcher もコールバック props に
  • 移行は npx sv migrate svelte-5 で自動化できる部分を先に処理し、createEventDispatcher / beforeUpdate / イベント修飾子は手作業
  • 落とし穴は Proxy の参照非等価・$effect の無限ループ・.svelte.ts 拡張子の忘れ・getter 不在の state 固定化

関連記事として React 19 完全ガイドAstro 完全ガイドBun 完全ガイドTypeScript × Hono 完全ガイドTypeScript × Drizzle ORM 完全ガイドClaude Code × Supabase 完全ガイド もあわせて、Svelte 5 を核に据えた 2026 年型軽量フロントエンド構成を組み上げてください。