コンポーネントの基本

Svelteコンポーネントは、以下の3つの部分から構成されます。

  • script(ロジック)
  • markup(HTML)
  • style(CSS)

これらは単一の.svelteファイル内に記述され、カプセル化されたコンポーネントを形成します。

まずは最小のコンポーネントから

最も単純な .svelte ファイルは、変数を 1 つだけ持ち、それを HTML として描画し、少しスタイルを当てるだけの構造です。3 つのブロックがどう並ぶかをまず体感してみてください。

<!-- Hello.svelte -->

<!-- 1. Script部分:ロジックとデータ -->
<script lang="ts">
  let name = 'World';
</script>

<!-- 2. Markup部分:HTML構造 -->
<h1>Hello, {name}!</h1>

<!-- 3. Style部分:スタイリング -->
<style>
  h1 {
    color: purple;
  }
</style>

script で宣言した name を、markup の {name} で展開しているだけのシンプルな構造です。style ブロック内の CSS はこのコンポーネントだけに スコープ され、他の <h1> に影響しません(詳しくは後述)。

他のコンポーネントを使う

作ったコンポーネントを別のコンポーネントから利用するには、script でインポートして markup 内でタグとして配置するだけです。

<!-- App.svelte -->
<script lang="ts">
  // 同じディレクトリにある Hello.svelte をインポート
  import Hello from './Hello.svelte';
</script>

<Hello />

ファイル名(拡張子を除く部分)がそのままインポート名・タグ名になります。PascalCase で命名するのが Svelte の慣例です。

この記事の構成

ここから先では、3 つのブロックそれぞれの中で使える機能(リアクティブな状態、テンプレート構文、スコープ付きスタイル等)を順に見ていきます。Svelte 5 では let だけでリアクティブにはならず、$state などの Runes を使う点に注意してください。

コンポーネントの基本構造

実用的なコンポーネントでは、リアクティブな状態($state)やイベントハンドラ、複数のスタイルが組み合わさります。カウンターを例に、3 ブロックが揃った典型的な形を見ておきましょう。下のサンプルは実行可能で、ボタンを押すと UI が更新され、ブラウザの DevTools コンソール(あるいは Playground の Console パネル)にもログが出ます。

<!-- Counter.svelte -->

<!-- 1. Script部分:ロジックとデータ -->
<script lang="ts">
  // $state で宣言した値は変更すると UI が自動更新される(Runes)
  let count = $state(0);

  function increment(): void {
    count++;
    console.log(`カウンターが増加しました: ${count}`);
  }
</script>

<!-- 2. Markup部分:HTML構造 -->
<div class="counter">
  <h2>カウンター: {count}</h2>
  <button onclick={increment}>
    クリック
  </button>
</div>

<!-- 3. Style部分:スタイリング -->
<style>
  .counter {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
  }

  h2 {
    color: #999;
  }

  button {
    background: #ff3e00;
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    cursor: pointer;
  }
</style>

Script部分の詳細

変数宣言とリアクティビティ

注意

Svelte 5より前のバージョンでは、letで宣言した変数は自動的にリアクティブになりました。

Svelte 5では、Runesシステム($stateなど)を使用してリアクティビティを明示的に制御します。ここに記載している以前のバージョンの宣言方法は使用しないでください。

Click to expand/fold panel

変数宣言とリアクティビティ

Svelte 5より前のバージョンでは、letで宣言した変数は自動的にリアクティブになります。

<script lang="ts">
  // これらの変数は自動的にリアクティブ(Svelte 4以前)
  let name: string = 'Alice';
  let age: number = 25;
  let isActive: boolean = true;

  // オブジェクトと配列もリアクティブ
  let user = {
    name: 'Bob',
    email: 'bob@example.com'
  };

  let items: string[] = ['item1', 'item2'];

  function updateUser(): void {
    // UIが自動的に更新される
    user.name = 'Charlie';
    items.push('item3');
  }
</script>

Svelte 3, 4の問題点

  • すべてのlet変数が自動的にリアクティブになるため、どれがリアクティブか分かりにくい
  • パフォーマンスの観点で無駄がある場合がある
  • TypeScriptとの統合が複雑

Svelte 5の改善

  • $stateで明示的にリアクティブを宣言
  • より予測可能で理解しやすい
  • TypeScriptの型推論が向上
  • パフォーマンスの最適化

インポート

他の Svelte コンポーネントや、ユーティリティ・型などは 通常の ES モジュールと同じ構文 でインポートできます。

<script lang="ts">
  // 他の Svelte コンポーネントをインポート
  import Button from './Button.svelte';

  // ユーティリティ関数をインポート
  import { formatDate } from '$lib/utils';

  // TypeScript の型をインポート
  import type { User } from '$lib/types';
</script>

<!-- インポートしたコンポーネントは PascalCase のタグとして使う -->
<Button />

ファイル名(拡張子を除く部分)がそのまま import 時の名前になります。慣例として PascalCase で命名するのが推奨です。$lib は SvelteKit が用意するエイリアスで、src/lib/ を指します。

Props でデータを受け渡す

コンポーネント間でデータを渡すには Svelte 5 の $props ルーンを使います。本ページでは後の「親から子へデータを渡す(最小の Props)」節で受け渡しの基本を扱います。$bindable、Snippets 経由の受け渡しなどの応用は $props - プロパティ で扱います。

インポートの実例

実際に 複数ファイルでの import を動かしてみます。下の例では Hello.svelte を別ファイルとして定義し、App.svelte からインポートして利用しています。「インタラクティブに試す」を押すと、Playground 上でも 2 つのファイルがタブで表示され、import が機能している様子を確認できます。

<!-- @file: Hello.svelte -->
<script lang="ts">
  let name = 'World';
</script>

<h1>Hello, {name}!</h1>

<style>
  h1 {
    color: purple;
  }
</style>

<!-- @file: App.svelte -->
<script lang="ts">
  // 同じディレクトリにある Hello.svelte をインポート
  import Hello from './Hello.svelte';
</script>

<Hello />
`<!-- @file: ... -->` マーカーについて

このサイト独自の記法で、live ブロック内を複数のファイルに分けて Playground 埋め込みに渡すためのものです。実際のプロジェクトでは普通に Hello.svelteApp.svelte の 2 つの別ファイル として保存します。マーカー行はファイル間の区切り(およびタブ名の指定)として機能し、ファイル本体には含まれません。

親から子へデータを渡す(最小の Props)

import できたら、次は 「親から子へデータを渡す」 ところまで一気に押さえてしまいましょう。Svelte 5 では $props() ルーンを使い、TypeScript の分割代入で受け取ります。下の例は Card.svelte が親から titlebody を受け取って表示する最小構成です。

<!-- @file: Card.svelte -->
<script lang="ts">
  type Props = { title: string; body: string };
  let { title, body }: Props = $props();
</script>

<article>
  <h3>{title}</h3>
  <p>{body}</p>
</article>

<style>
  article {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
    background: #f9f9f9;
  }
  h3 { margin: 0 0 0.5rem 0; color: #ff3e00; }
  p { margin: 0; color: #444; }
</style>

<!-- @file: App.svelte -->
<script lang="ts">
  import Card from './Card.svelte';
</script>

<Card title="Welcome" body="親から渡された props を子で受け取っています。" />
<Card title="Svelte 5" body="$props ルーンを分割代入で受け取り、Props 型で型付けします。" />

ポイントは次のとおりです。

  • let { ... }: Props = $props() — 分割代入で受け取り、Props 型で型付けする
  • 属性として渡す — 親側は <Card title="..." body="..." /> のように HTML 属性と同じ書き方
  • PascalCase — タグ名は Card(コンポーネント名と同じ)
`$props` の詳細はリファレンスへ

オプショナル props、$bindable、Snippets 経由のテンプレート受け渡し、$props.id() などの応用は $props - プロパティ で扱います。ここでは「親 → 子へデータを渡せる」という基本だけ押さえれば十分です。

Markup部分の詳細

Markup 部分には、コンポーネントが描画する HTML 構造 を記述します。ただし Svelte の Markup は 素の HTML ではなく、テンプレート構文で拡張された HTML です。{ } で JavaScript 式を埋め込んだり、{#if} のようなブロック構文や {@html} のようなアノテーションで動的な振る舞いを表現できます。

<script lang="ts">
  let name = $state('World');
  let isLoggedIn = $state(false);
</script>

<!-- {} で式を埋め込み -->
<h1>Hello, {name}!</h1>

<!-- 三項演算子も普通の JS 式として書ける -->
<p>{isLoggedIn ? 'ログイン中' : '未ログイン'}</p>

このように、Markup ブロックの中身は 「HTML + テンプレート式・ブロック構文・アノテーション」 の組み合わせです。

テンプレート構文の体系は専用ページで網羅

{#if} / {#each} / {#await} などの ブロック構文{@html} / {@render} などの アノテーションbind:innerHTML などの 特殊バインディング といったテンプレート構文の体系は、テンプレート構文 - 制御フローとアノテーション ページで網羅的に扱います。

本ページでは、Markup ブロックで最も基本となる 式展開 { expr } だけを、すぐ下でインタラクティブに体験できる形で扱います。

式展開でできること

{ expr } の中身は 普通の JavaScript 式 として評価されます。三項演算子・論理演算子・関数呼び出し・メソッドチェーンなど、特別な構文を覚えなくても任意の JS 式がそのまま書けます。下のデモはスライダーとチェックボックスを動かすと、各種パターンが連動して表示更新される様子を観察できます。

<script lang="ts">
  let count = $state(5);
  let isLoggedIn = $state(false);
  let user = $state({ name: 'taro', email: 'taro@example.com' });
</script>

<div class="controls">
  <label>
    カウント: {count}
    <input type="range" min="0" max="20" bind:value={count} />
  </label>
  <label>
    <input type="checkbox" bind:checked={isLoggedIn} />
    ログイン状態
  </label>
</div>

<!-- 単純な値の埋め込み -->
<p>カウント: {count}</p>

<!-- 計算式 -->
<p>2 倍: {count * 2}</p>

<!-- 三項演算子で出し分け -->
<p>状態: {isLoggedIn ? 'ログイン中' : '未ログイン'}</p>

<!-- 論理 AND(短絡評価)で表示制御 -->
<p>{count > 10 && '✨ 10 を超えました!'}</p>

<!-- メソッド呼び出し -->
<p>名前(大文字): {user.name.toUpperCase()}</p>

<!-- プロパティアクセス -->
<p>連絡先: {user.email}</p>

<style>
  .controls {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    padding: 0.75rem 1rem;
    background: #f5f5f5;
    color: #222;
    border-radius: 6px;
    margin-bottom: 1rem;
  }
  .controls label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: #222;
  }
  p {
    margin: 0.25rem 0;
  }
</style>

ポイントは次のとおりです。

  • { } の中は普通の JS 式 — 三項演算子・論理演算子・関数呼び出しが書ける。新しい構文を覚える必要はない
  • リアクティブ更新の条件 — 式の中で参照する値が $state などのリアクティブな源であれば、値が変わるたびに UI が自動で再評価される。普通の let 変数だけを参照する式は更新されない
  • 三項演算子はルーンの相棒ではないlet label = 'Hello' のような非リアクティブな値でも {label ? 'A' : 'B'} は書ける。「テンプレート式の評価」と「リアクティビティ」は独立した話
ブロック構文 `&#123;#if&#125;` と三項演算子の使い分け
ケース推奨
複数行・要素ブロック単位で出し分け{#if} / {:else} ブロック構文(テンプレート構文
テキスト・属性値・クラス名など 1 つの値の出し分け三項演算子 {a ? b : c}
表示するかどうかだけ(else 不要){#if} または短絡評価 {flag && ...}

ブロックを丸ごと」なら {#if}、「式 1 つの結果」なら三項演算子、と覚えると迷いません。

Style部分の詳細

スコープ付きスタイル

Svelteのスタイルは、デフォルトでコンポーネントにスコープされます。

<style>
  /* このスタイルは現在のコンポーネントにのみ適用される */
  p {
    color: blue;
  }

  /* 生成されるCSSは以下のようになる
     p.svelte-xyz123 { color: blue; } */
</style>

グローバルスタイル

:global()を使用してグローバルスタイルを定義

<style>
  /* このコンポーネント内のp要素のみ */
  p {
    color: blue;
  }

  /* 全てのp要素に適用 */
  :global(p) {
    margin: 0;
  }

  /* 子要素のグローバルスタイル */
  .container :global(a) {
    color: red;
  }
</style>

動的スタイル

スタイル属性やクラスを動的に適用できます。条件に応じてスタイルを変更する場合に便利です。下の例では カラーピッカー・サイズスライダー・チェックボックス を操作すると、3 種類の動的スタイル指定方法(インラインスタイル / style: ディレクティブ / class: ディレクティブ)がすべてリアルタイムで連動します。

<script lang="ts">
  let color = $state('#ff3e00');
  let size = $state(16);
  let isActive = $state(true);
</script>

<div class="controls">
  <label>
    色: <input type="color" bind:value={color} />
  </label>
  <label>
    サイズ: {size}px
    <input type="range" min="10" max="40" bind:value={size} />
  </label>
  <label>
    <input type="checkbox" bind:checked={isActive} />
    アクティブ
  </label>
</div>

<!-- 1. インラインスタイル(属性に式を埋め込む) -->
<h4>インラインスタイル</h4>
<p style="color: {color}; font-size: {size}px;">
  動的スタイル
</p>

<!-- 2. style: ディレクティブ(より簡潔な記法) -->
<h4>style: ディレクティブ</h4>
<p
  style:color
  style:font-size="{size}px"
  style:font-weight={isActive ? 'bold' : 'normal'}
>
  style: 記法
</p>

<!-- 3. class: ディレクティブ(クラスの動的適用) -->
<h4>class: ディレクティブ</h4>
<div
  class="base"
  class:active={isActive}
  class:large={size > 20}
>
  条件付きクラス(サイズが 20 を超えると "large" クラス追加)
</div>

<style>
  .controls {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    padding: 0.75rem 1rem;
    background: #f5f5f5;
    color: #222;
    border-radius: 6px;
    margin-bottom: 1rem;
  }
  .controls label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: #222;
  }
  h4 {
    margin: 0.75rem 0 0.25rem;
    font-size: 0.8rem;
  }
  .base {
    padding: 1rem;
    border: 1px solid #888;
    border-radius: 4px;
    background: #fafafa;
    color: #222;
  }
  .active {
    background: #ff3e00;
    color: white;
  }
  .large {
    font-size: 1.5rem;
  }
</style>

イベントハンドリング

Svelte 5 では、イベントハンドラの記法が on:click から標準的な HTML 属性の onclick に変更されました。関数参照でもインライン関数でも登録できます。

<script lang="ts">
  function handleClick(event: MouseEvent): void {
    console.log('クリックされました', {
      type: event.type,
      x: event.clientX,
      y: event.clientY
    });
  }
</script>

<!-- 関数参照 -->
<button onclick={handleClick}>
  クリック
</button>

<!-- インライン関数 -->
<button onclick={() => console.log('インライン')}>
  インライン関数
</button>
イベントの詳細な扱い

preventDefault / stopPropagation などの修飾子相当パターンや、window / document へのグローバルイベント登録、on() ヘルパーの使い方は イベントハンドリング で詳しく解説しています。

双方向バインディング

bind: ディレクティブを使用して、フォーム要素の値とコンポーネントの $state 変数 を双方向にバインドできます。入力値が自動的に変数へ反映され、変数の変更も入力フィールドに反映されます。下のデモは 入力するたびに右側のバインド結果が即時更新 されるので、各 bind: の挙動を視覚的に確認できます。

<script lang="ts">
  // 各入力要素にひとつずつ独立した状態を用意
  let textName = $state('');
  let textEmail = $state('');
  let textAge = $state(0);
  let checkboxAgreed = $state(false);
  let radioOption = $state('option1');
  let selectFruit = $state('');
  let multiColors = $state<string[]>([]);
  let textareaContent = $state('');
</script>

<h4>① テキスト入力</h4>
<label>
  名前:
  <input type="text" bind:value={textName} placeholder="入力してください" />
</label>
<p class="binding-out">name = "{textName}"</p>

<label>
  メール:
  <input type="email" bind:value={textEmail} placeholder="user@example.com" />
</label>
<p class="binding-out">email = "{textEmail}"</p>

<label>
  年齢:
  <input type="number" bind:value={textAge} />
</label>
<p class="binding-out">age = {textAge} (型: {typeof textAge})</p>

<h4>② チェックボックス</h4>
<label>
  <input type="checkbox" bind:checked={checkboxAgreed} />
  利用規約に同意する
</label>
<p class="binding-out">agreed = {checkboxAgreed}</p>

<h4>③ ラジオボタン(bind:group で 1 つの値に束ねる)</h4>
<label>
  <input type="radio" bind:group={radioOption} value="option1" />
  Option 1
</label>
<label>
  <input type="radio" bind:group={radioOption} value="option2" />
  Option 2
</label>
<label>
  <input type="radio" bind:group={radioOption} value="option3" />
  Option 3
</label>
<p class="binding-out">selected = "{radioOption}"</p>

<h4>④ セレクトボックス(単一選択)</h4>
<select bind:value={selectFruit}>
  <option value="">選択してください</option>
  <option value="apple">Apple</option>
  <option value="banana">Banana</option>
  <option value="orange">Orange</option>
</select>
<p class="binding-out">fruit = "{selectFruit}"</p>

<h4>⑤ セレクトボックス(複数選択、Ctrl/Cmd で選択)</h4>
<select multiple bind:value={multiColors} size="3">
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="blue">Blue</option>
</select>
<p class="binding-out">colors = {JSON.stringify(multiColors)}</p>

<h4>⑥ テキストエリア</h4>
<textarea
  bind:value={textareaContent}
  placeholder="複数行のテキストが入力できます"
  rows="3"
></textarea>
<p class="binding-out">content = "{textareaContent}" ({textareaContent.length} 文字)</p>

<style>
  h4 {
    margin: 1rem 0 0.25rem;
    font-size: 0.85rem;
  }
  label {
    display: inline-block;
    margin: 0.25rem 0.5rem 0.25rem 0;
  }
  input[type="text"],
  input[type="email"],
  input[type="number"],
  select,
  textarea {
    margin-left: 0.25rem;
    padding: 0.25rem 0.4rem;
    border: 1px solid #888;
    border-radius: 3px;
    background: #fff;
    color: #222;
  }
  textarea {
    width: 100%;
    box-sizing: border-box;
    font-family: inherit;
  }
  .binding-out {
    margin: 0.15rem 0 0.5rem 1rem;
    font-family: monospace;
    color: #ff3e00;
    font-size: 0.85rem;
    font-weight: 600;
  }
</style>
`bind:value` の型変換

<input type="number">bind:value すると、文字列ではなく number で変数に値が入ります(デモの「年齢」の typeof 表示で確認できます)。同様に type="checkbox"bind:checkedbooleantype="date"Date 型に自動変換されます。Svelte が要素の type に応じて適切な型変換を内部で行ってくれます。

`bind:group` と `bind:value` の使い分け
  • ラジオボタン: 同じグループの中から 1 つを選ぶ → bind:group={変数} を各 input に書き、value="..." で各選択肢の値を指定
  • チェックボックス(複数選択): 配列を作る → bind:group={配列変数} を各 input に書く
  • セレクトボックス: bind:value={変数}<select> 自体にバインド)

ラジオとセレクトは外見が似ていても バインド方法が異なる 点に注意してください。

技術詳解

Svelte 5では新しい$stateルーンが導入され、リアクティビティの扱い方が変わりました。bind:との違いについて詳しく知りたい場合は、以下の記事を参照してください。

コンポーネントの合成について

Svelte 5では、コンポーネントの合成方法が<slot />からchildrenパターンに変更されました。@renderディレクティブを使用した新しいパターンについて詳しく知りたい場合は、テンプレート構文 - 制御フローとアノテーションページの「@render - Snippetsとchildrenのレンダリング」セクションを参照してください。

まとめ

このページで学んだこと

  • Svelte コンポーネントの 3 つの主要部分(script / markup / style)
  • $state でのリアクティブな状態管理、$props での親子データ受け渡し
  • テンプレート式(式展開)と JS 式の関係(三項演算子・論理 AND・関数呼び出し)
  • イベントハンドリングと修飾子(Svelte 4 → 5 の移行ポイント)
  • 双方向データバインディング(bind:
  • スコープ付きスタイルとグローバルスタイル
  • 動的なスタイルとクラスの適用

ブロック構文({#if} / {#each} / {#await} 等)やアノテーション(@render / @html / @const 等)は テンプレート構文 ページで網羅的に解説しています。

関連トピック
  • DOM 要素を直接操作する必要がある場合は、use: アクション{@attach} を学ぶとより高度な操作が可能になります
  • 本格的な実装例として TODO アプリ実装例svelte5-todo-example リポジトリ・ライブデモ付き)も参照してください

次のステップ

TypeScript統合では、SvelteでTypeScriptを効果的に使用する方法を詳しく学びます。