Svelteに限らずWEBアプリケーションを構築しているとモーダルウィンドウを利用したいという場面が多々あります。モーダルウィンドウはSvelteに限らず利用頻度も高いこともあってネット上には数えきれないほどの記事が存在しますが本文書ではSvelteに特化してモーダルウィンドの実装方法をシンプルなコードを利用して説明しています。モーダルウィンドウの実装はSvelteの基本的な知識を身に付けるためのいい教材なのでSvelteを学び始めの方はぜひ本書を読みながら自作のモーダルウィンドウにチャレンジしてください。

モーダルウィンドウの作成を通してSvelteの以下の機能を理解することができます。

  • クリックイベントの設定(click, keydownイベント)
  • ifディレクティブの設定
  • Transitionの設定
  • svelte:window
  • 背景のスクロールの停止(bodyタグ、overflowプロパティの利用)
  • コンポーネント間でのデータの受け渡し(props, dispatcher)
  • Slotの設定
  • Component Bindingsによるデータの受け渡し
  • writable storesの利用

モーダルウィンドウとは

モーダルウィンドウは画面中央にウィンドウが表示され、表示されたウィンドウ以外の背景を薄暗く表示させることでユーザに伝えたい内容を際立たせるための技術です。Webの世界ではさまざまな場所で利用されているので、モーダルウィンドウという言葉にピンとこない人でもこの技術に触れたことのない人はいないでしょう。あるサイトにアクセスすると急に画面中央にモーダルウィンドウが表示され消し方がわからないといった経験をしたことのある人も多いかと思います。英語ではmodalという単語を使うので検索する場合はmodalと検索することで簡単に情報を見つけることができます。

Svelteによるモーダルウィンドウの作成

Svelte環境の構築

SvelteのドキュメントではSvelteをローカル環境で動作確認を行いたい場合はSvelteKitのプロジェクトを作成することを勧めています。SvelteKitは主にUIを担当するSvelteにルーティングやSSR (Server Side Rendering)などの機能が追加されたフルスタックフレームワークです。複数のページを持つアプリケーションを構築したい場合にはSvelteKitを利用することになります。

SvelteKitを利用することもできますが本文書ではViteを利用してプロジェクトの作成を行います。ViteではSvelte以外のフレームワーク/ライブラリを選択することができますが– –template svelteをつけてコマンドを実行することでSvelteのプロジェクトを作成してくれます。


 % npm create vite@latest svelte-modal -- --template svelte

Scaffolding project in /Users/mac/Desktop/svelte-modal...

Done. Now run:

  cd svelte-modal
  npm install
  npm run dev

プロジェクトの作成が完了したら完了後に作成されるフォルダに移動してnpm installコマンドを実行します。npm intallコマンドを実行するとpackage.jsonに記述されている情報を元にJavaScriptのパッケージのインストールを行います。

プロジェクトフォルダ直下のsrcフォルダにあるApp.svelteファイルを利用してモーダルウィンドウの作成を行なっていきます。デフォルトで全体に適用されているCSSを解除するためにmain.jsファイルのimport文を削除しておきます。


import App from './App.svelte';

const app = new App({
  target: document.getElementById('app'),
});

export default app;

クリックボタンの作成

モーダルウィンドウはWEBサイトにアクセスした後に一定時間経過したら急に表示されるものから情報を登録しようとした場合にボタンをクリックすると入力フォームが表示されるものなどいろいろな箇所で利用されています。本文書ではボタンをクリックすると表示されるシンプルなモーダルウィンドウを作成します。そのため最初にボタン要素を追加します。


<button>Click</button>

ボタン要素を追加後にnpm run devコマンドを実行して開発サーバを起動します。

clickボタンの表示
clickボタンの表示

画像にClickボタンが表示されましたがボタンをクリックしても何も変化はありません。

モーダルウィンドウの作成

モーダルウィンドウの作成方法にはいくつもありますが本文書ではベースとオーバーレイとコンテンツの3つの要素を作成して行います。

最初にオーバーレイとコンテンツが入るベース部分をdiv要素で作成してbaseクラスを設定します。Svelteではsvelteファイルにstyleタグを追加することでそのコンポーネント内のみで適用するclassを設定することができます。positionにfixedを設定してtop, left, right, bottomを0に設定することで画面全体に広がる領域を設定することができます。


<button>Click</button>
<div class="base"></div>

<style>
  .base {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
</style>

次にオーバーレイを設定します。オーバーレイはモーダルウィンドウの背景に設定しブラウザの画面全体に覆われる幕です。一般的にオーバーレイはモーダルウィンドウと同時に表示され半透明で半透明部分の領域をクリックすると表示されているモーダルウィンドウが非表示となります。


<button>Click</button>
<div class="base">
  <div class="overlay" />
</div>

<style>
  .base {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
  .overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: gray;
    opacity: 0.5;
  }
</style>

opacityを設定しているので”Click”ボタンを確認することができますが”Click”ボタンをクリックすることはできません。

オーバーレイの設定
オーバーレイの設定

オーバーレイの後にコンテンツの要素を追加します。


<button>Click</button>
<div class="base">
  <div class="overlay" on:click={handleClick} />
  <div class="content">コンテンツ</div>
</div>

<style>
  .base {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
  .overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: gray;
    opacity: 0.5;
  }
  .content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: white;
  }
</style>

コンテンツの要素はベース要素で設定したfixedを基準にpositionをabsoluteで設定を行い、top、left、transformを利用して画面中央に表示させています。

コンテンツを中央に表示
コンテンツを中央に表示

HTMLとCSSの設定で半透明のオーバーレイの中央にコンテンツを表示することができました。

クリックイベントの設定

Svelteで要素に対してイベントを設定したい場合にはon:イベント名で行うことができます。ここで利用するのはclickイベントなのでon:clickを利用します。

オーバーレイにクリックイベントを設定します。 クリックイベントにはclose関数を設定しています。


<script>
  const close = () => {
    console.log('click');
  };
</script>

<button>Click</button>
<div class="base">
  <div class="overlay" on:click={close} />
  <div class="content">コンテンツ</div>
</div>

<style>
//略

半透明の領域のみクリックするとブラウザのコンソールに”click”が表示されることを確認してください。中央のコンテンツをクリックしても半透明の領域の上に表示されているためclickイベントは発生しません。

表示・非表示の切り替え

モーダルウィンドウの表示・非表示を行うるようにするためにReactiveな変数showを定義します。デフォルト値をtrueとしてclose関数が実行されるとfalseになるように設定します。showの値によって表示・非表示を切り替えれるようにifディレクティブを利用します。showがtrueの場合はifディレクティブに囲まらた部分が表示されます。


<script>
  let show = true;
  const close = () => {
    show = false;
  };
</script>

<button>Click</button>
{#if show}
  <div class="base">
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}

<style>
//略

設定後、オーバーレイをクリックするとモーダルウィンドウが非表示になることを確認してください。

Clickボタンをクリックすると今度はモーダルウィンドウが表示されるようにClickボタンにclickイベントを設定します。showのデフォルト値をtrueからfalseに変更しておきます。


<script>
  let show = false;

  const open = () => {
    show = true;
  };

  const close = () => {
    show = false;
  };
</script>

<button on:click={open}>Click</button>
{#if show}
  <div class="base">
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}

<style>
//略

最初はClickボタンのみ表示されています。

clickボタンの表示
clickボタンの表示

Clickボタンをクリックするとモーダルウィンドウが表示されます。

コンテンツを中央に表示
コンテンツを中央に表示

オーバーレイの領域をクリックするとモーダルウィンドウは非表示となります。

clickボタンの表示
clickボタンの表示

Transitionの設定

モーダルウィンドウの表示・非表示の切り替えに動きをつけるためにTransitionの設定を行います。設定はシンプルでifディレクティブで表示・非表示が切り替わる要素にtransition:fadeを設定するだけです。

ベースの要素にtransition:fadeを追加してfadeをsvelte/transitionからimportします。


<script>
  import { fade } from 'svelte/transition';
//略
</script>

<button on:click={open}>Click</button>
{#if show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}
//略

設定後Clickボタンをクリックすると先ほどまでとは異なり、ゆっくりとモーダルウィンドウが表示され、オーバーレイをクリックするとゆっくりとモーダルウィンドウが非表示となります。

デフォルトの動作を変更したい場合にはオプションを設定することができます。オプションにはdelayとdurationとeasingを設定することができますがここではdelayとdurationを設定します。Transitonが開始するまで300ms待ち、3秒かけて表示、非表示が切り替わります。非常にゆっくりとした動作となります。このようにオプションを利用して表示・非表示の時間を制御することができます。


<iv class="base" transition:fade={{ delay: 300, duration: 3000 }}>

svelte:windowによるキーイベント設定

Svelteではブラウザのwindowオブジェクトにも簡単にイベントを設定することができます。例えばEscapeキーを押して場合にモーダルウィンドウを非表示にするといったことも可能です。svelte:windowタグを追加してkeydownイベントを設定します。キーを押すとkeyDownClose関数が実行されます。


<script>
  import { fade } from 'svelte/transition';
//略
  const keyDownClose = (e) => {
    console.log(e);
  };
</script>

<svelte:window on:keydown={keyDownClose} />
<button on:click={open}>Click</button>
{#if show}
  <div class="base">
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}

<style>
//略

どのキーが押されたかもkeyDownClose関数の引数で渡されるe(イベント)の中身を確認することでわかります。

“a”、”Enter”、”Escape”、”Shift”を連続で押した場合に表示されるイベントの内容です。

キーイベントの確認
キーイベントの確認

このようにkyeとcodeを見るだけでどのキーを押したのかがわかります。

ここではEscapeキーを押した場合にモーダルウィンドウを非表示にしたいので以下のように設定することができます。


const keyDownClose = (e) => {
  if (e.code === 'Escape') close();
};

背景のスクロールを停止

ここまでの設定ではモーダルウィンドウが表示されていない時Clickボタンしか画面に表示されていませんが通常はブラウザの高さを超えるコンテンツがページの中に含まれています。モーダルウィンドウが表示されていない場合には画面のスクロールが必要となりますがモーダルウィンドウが表示されている場合にはスクロールを止めたい場合があります。その場合にはbodyタグにoverflowのhiddenを設定することでスクロールを止めることができます。

button要素の下にpタグを追加してheightが2000pxの領域を追加します。


<button on:click={open}>Click</button>
<p style="height:2000px;background-color:aqua">Long Content</p>
{#if show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}

モーダルウィンドウが表示されてもスクロールが可能なのでブラウザの右にスクロールバーが表示されClickボタンが画面上から見えなくなっています。

スクロールが行われる
スクロールが行われる

bodyタグへはdocument.bodyでアクセスすることが可能なのでstyle属性でoverflowの値を設定します。


const open = () => {
  document.body.style.overflow = 'hidden';
  show = true;
};

const close = () => {
  document.body.style.overflow = '';
  show = false;
};

設定後はモーダルウィンドウを表示するためopen関数が実行されるとoverflowの値がhiddenになるためスクロールバーが非表示となりスクロールを行うことができません。

overflow:hiddenによるスクロールの停止
overflow:hiddenによるスクロールの停止

オーバーレイをクリックするとモーダルウィンドウが非表示となり、overflowの値が””になるのでスクロールの停止は解除されスクロール可能となります。

Modalコンポーネントの作成

ここまでの設定でモーダルウィンドウを実装することができましたがモーダルのコードを再利用することができません。他の場所でもモーダルウィンドウを利用できるようにコンポーネント化します。

srcフォルダにあるlibフォルダにcomponentsフォルダを作成してModal.svelteファイル作成してApp.svelteからモーダルウィンドウのタグを移動させます。


<script>
  import { fade } from 'svelte/transition';
</script>

{#if show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}

<style>
  .base {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
  .overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: gray;
    opacity: 0.5;
  }
  .content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: white;
  }
</style>

App.svelteファイルでは作成したModalコンポーネントをimportします。


<script>
  import Modal from './lib/components/Modal.svelte';

  let show = false;

  const open = () => {
    document.body.style.overflow = 'hidden';
    show = true;
  };

  const close = () => {
    document.body.style.overflow = '';
    show = false;
  };

  const keyDownClose = (e) => {
    if (e.code === 'Escape') close();
  };
</script>

<svelte:window on:keydown={keyDownClose} />

<button on:click={open}>Click</button>
<p style="height:2000px;background-color:aqua">Long Content</p>
<Modal />

ブラウザで動作確認するとModal.svelteファイルのshowが定義されていないためエラーとなります。showについてはpropsを利用してAppコンポーネントからModalコンポーネントに渡します。

Modal.svelteではexport letでpropsで受け取る変数を定義します。


<script>
  import { fade } from 'svelte/transition';
  export let show;
</script>

App.svelteではModalタグに{show}を追加します。show={show}でも可能です。


<Modal {show} />

Clickボタンを押すとモーダルウィンドウが表示されるなりますがオーバーレイをクリックしてもclose関数が設定されていないのでモーダルウィンドウが非表示になることはありません。

Modalコンポーネントでclose関数を追加してshowの値を更新すればいいと考えるかもしれませんがpropsを渡された子コンポーネント(Appコンポーネントから見るとModalコンポーネントは子コンポーネントにあたる)では直接propsの値を更新してはいけません。

dispatcherの設定

親から子へはpropsを利用してデータを渡すことができますが子から親にデータを渡したい場合にはdispatch関数によるイベントを利用することができます。

dispatch関数はsvelteからimportするcreateEventDispatcher関数を利用して作成します。dispatch関数の引数には任意のイベント名を設定することができます。ここではcloseEventというイベント名をつけています。


<script>
  import { fade } from 'svelte/transition';
  import { createEventDispatcher } from 'svelte';

  export let show;
  const dispatch = createEventDispatcher();

  const close = () => {
    dispatch('closeEvent');
  };
</script>

{#if show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">コンテンツ</div>
  </div>
{/if}
//略

オーバーレイをクリックするとclose関数が実行されdispatch関数によりcloseEventが発生します。

発生したcloseEventイベントを親コンポーネントで検知する必要があります。closeEventという名前のイベントはon:closeEventで検知することができます。


<Modal {show} on:closeEvent={close}/>

on:closeEventによりcloseEventイベントが発生するとclose関数が実行されます。Modal.svelteとApp.svleteのclose関数は同じ名前なだけで関連はありません。わかりにくい場合は任意の名前をつけてください。Modalコンポーネントを分けることで再利用可能なモーダルウィンドウになりましjた。

dispatherの仕組みを利用することで子から親にイベントを通してデータを渡すことができました。dispatch関数の第二引数を使うことでデータを渡すことも可能です。

データの渡し方

Modalの実装ではデータを渡す必要はありませんが試しにdispatch関数を利用した場合にどのようにデータを送って受け取るのか確認します。

dispatch関数の第二引数にオブジェクトでmessageプロパティと値を設定します。


const close = () => {
  dispatch('closeEvent', {
    message: 'click overlay',
  });
};

App.svelteではclose関数の引数で受け取るeventを通して渡されたデータを確認することができます。


const close = (e) => {
  console.log(e);
  document.body.style.overflow = '';
  show = false;
};

ブラウザのコンソールに表示されるCustomEventのdetailプロパティでdispatch関数で設定したオブジェクトの中身を確認することができます。イベントを介してdispatch関数で設定したデータを受け取れることがわかりました。

dispatch関数から渡された値の確認
dispatch関数から渡された値の確認

slotの利用

モーダルウィンドウに表示させる内容をModalコンポーネントの中で設定していましたがコンポーネントを再利用する場合にモーダルウィンドウに表示させる内容をModalコンポーネントの外から設定したい場合にslotを利用することができます。

Modalコンポーネントでこれまで”コンテンツ”と設定していた場所にslotタグを追加します。


{#if show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">
      <slot />
    </div>
  </div>
{/if}

ModalコンポーネントをimportしたApp.svelteではModalタグの間に表示させたいコンテンツを追加します。Modalタグの間に挿入したコンテンツがModalコンポーネントのslotタグの部分に表示されます。


<Modal {show}>
  <div style="color:red">コンテンツ</div>
</Modal>

動作確認を行うことモーダルウィンドウのコンテンツには赤色の文字列が表示されます。

赤色の文字列がモーダルウィンドウに表示
赤色の文字列がモーダルウィンドウに表示

Component bindings

propsとdispatch関数を利用することでコンポーネント化したモーダルウィンドウの表示・非表示を制御できることがわかりましたがその他にもコンポーネント間でデータを受け渡しする方法があります。その中の一つにComponent bindingsを利用する方法があります。

Component bindingsを利用して定義した変数showを渡し、Modalコンポーネント側でshowの値を更新することができます。Componentという名前がついている通りコンポーネントで利用することができます。

App.svelteのModalタグでshowの変数をbindします。


<Modal bind:value={show}>
  <div style="color:red">コンテンツ</div>
</Modal>

Modalコンポーネント側ではpropsのvalueとしてshowの値を受け取ることができます。close関数ではvalueの値を更新することができます。


<script>
  import { fade } from 'svelte/transition';
  export let value;

  const close = () => {
    value = false;
  };
</script>

{#if value}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">
      <slot />
    </div>
  </div>
{/if}
//略

bind:value:{show}をbind:showに変更することもできます。


<Modal bind:show>
  <div style="color:red">コンテンツ</div>
</Modal>

bind:showを設定した場合にはpropsの名前はshowとなります。


<script>
  import { fade } from 'svelte/transition';
  export let show;

  const close = () => {
    show = false;
  };
</script>

{#if show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">
      <slot />
    </div>
  </div>
{/if}
//略

Component bindingを利用してもpropsとdispatch関数の時と同様に表示・非表示を切り替えることができます。

writable storeの利用

コンポーネント間でデータの共有はwritable storeを利用して行うことができます。propsとdispatch関数、Component bindingの場合は親子関係を持つコンポーネントのデータの送受信でしたがwritable storeを利用した場合はアプリケーション内のすべてのコンポーネントから共有できるデータとなります。

srcフォルダにstoreフォルダを作成してstores.jsファイルを作成します。showという変数名でwritable storeを設定してexportします。デフォルト値にfalseを設定しています。


import { writable } from 'svelte/store';

export const show = writable(false);

App.svelteとModal.svelteでshowをimportします。writable storeなのでどちらのコンポーネントでも値を取得すること、更新することができます。writableで作成したオブジェクトにはset, update, subscribeのメソッドを持っておりsetによって下記のように値を設定することができます。showという名前の変数の中にfalseが入っているわけではありません。


<script>
  import Modal from './lib/components/Modal.svelte';
  import { show } from './store/stores';

  const open = () => {
    document.body.style.overflow = 'hidden';
    show.set(true);
  };

  const close = (e) => {
    document.body.style.overflow = '';
    show.set(false);
  };

  const keyDownClose = (e) => {
    if (e.code === 'Escape') close();
  };
</script>

<svelte:window on:keydown={keyDownClose} />

<button on:click={open}>Click</button>
<p style="height:2000px;background-color:aqua">Long Content</p>
<Modal>
  <div style="color:red">コンテンツ</div>
</Modal>

showに保存されている値についてはshowの名前の前に$をつけて$showでアクセスすることができます。


<script>
  import { fade } from 'svelte/transition';
  import { show } from '../../store/stores';

  const close = () => {
    show.set(false);
  };
</script>

{#if $show}
  <div class="base" transition:fade>
    <div class="overlay" on:click={close} />
    <div class="content">
      <slot />
    </div>
  </div>
{/if}

<style>
//略

writable storesを利用してコンポーネント間で共有したshowの値を利用してモーダルウィンドウの表示・非表示を切り替えることができます。

シンプルなモーダルウィンドウの作成を通してSvelteの各種機能の基礎知識を学ぶことができました。