テーマカラーの定義を更新しました

最新のCSSを確認

Modal

Modalは、画面全体にオーバーレイを重ねて、特定の状態やコンテンツを最前面に提示するコンポーネントです。
ユーザーに対して明示的な情報を提示したり、オプションを選択させるときに使用できます。

プロパティ

Modalは、以下のプロパティをサポートしています。

名前 デフォルト値 説明
open boolean false Modal を表示するかどうか。
dismissible boolean true 背景クリックおよびEscキーで閉じる動作を無効にするかどうか。
hideCloseButton boolean false trueのとき、閉じるボタンを非表示にします。
unstyled boolean false trueのとき、モーダル本体のスタイルを適用しません。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。

modals/Modal.svelte
        <!--
@component
## 概要
- ユーザーに対して情報を提示したり、オプションを選択させるためのコンポーネントです

## 機能
- 任意のコンテンツを配置できます
- 外側クリックやEscキーで閉じるかどうかを制御できます
- 閉じるボタンを表示できます
- モーダル本体を非表示にするかどうか
- モーダルを開いたとき、`data-auto-focus` 属性を付与した要素に自動でフォーカスします。フォーカスしたい要素に付与してください。付与しない場合は閉じるボタン、それもなければモーダル枠自体にフォーカスします

## Props
- dismissible: falseを指定すると、背景クリックおよびEscキーでモーダルが閉じなくなります 初期値はtrue
- hideCloseButton: trueを指定することで閉じるボタンを非表示にすることができます
- unstyled: trueを指定するとモーダル本体のスタイルを適用しません

## Usage
```svelte
<Modal bind:open={isOpen}>
  {@render children()}
</Modal>
```
-->

<script module lang="ts">
  import type { Snippet } from 'svelte';
  import type { ClassValue } from 'svelte/elements';
  import { cva, type VariantProps } from 'class-variance-authority';

  export const modalVariants = cva('relative focus:outline-none', {
    variants: {
      /** モーダル本体のスタイルを適用しないかどうか */
      unstyled: {
        true: [],
        false: ['p-6 bg-surface border border-border rounded-lg shadow-lg'],
      },
    },
    defaultVariants: {
      unstyled: false,
    },
  });

  export type ModalVariants = VariantProps<typeof modalVariants>;

  export interface ModalProps extends ModalVariants {
    /** Modalが開いているかどうか */
    open: boolean;
    /** 背景クリックおよびEscキーで閉じる動作を有効にするかどうか */
    dismissible?: boolean;
    /** 閉じるボタンを非表示にするかどうか */
    hideCloseButton?: boolean;
    /** モーダル本体を非表示にするかどうか */
    unstyled?: boolean;
    /** クラス */
    class?: ClassValue;
    children: Snippet<[]>;
  }
</script>

<script lang="ts">
  import Button from '$lib/components/ui/atoms/Button.svelte';
  import { X } from '@lucide/svelte';
  import { fade, scale } from 'svelte/transition';

  let { class: className, open = $bindable(false), dismissible = true, unstyled = false, hideCloseButton = false, children }: ModalProps = $props();

  let modalElement = $state<HTMLElement>();

  let modalVariantsClass = $derived(modalVariants({ class: className, unstyled }));

  $effect(() => {
    let trigger_element: HTMLElement | null = null;
    if (open) {
      // モーダルを閉じた後にフォーカスを元の要素に戻すため、開く前のフォーカス先を保存
      trigger_element = document.activeElement instanceof HTMLElement ? document.activeElement : null;
      document.body.classList.add('overflow-hidden');
      document.addEventListener('click', handleClickOutside);
      document.addEventListener('keydown', handleKeyDown);
      focusInitialElement();
    }

    return () => {
      document.body.classList.remove('overflow-hidden');
      document.removeEventListener('click', handleClickOutside);
      document.removeEventListener('keydown', handleKeyDown);
      // モーダルを開く前にフォーカスしていた要素にフォーカスを戻す
      trigger_element?.focus();
    };
  });

  /** data-auto-focus 属性を持つ要素にフォーカスする、なければ閉じるボタン、それもなければモーダル枠自体にフォーカスする */
  function focusInitialElement() {
    const target = modalElement?.querySelector<HTMLElement>('[data-auto-focus]')
      ?? modalElement?.querySelector<HTMLElement>('[data-close-button]')
      ?? modalElement;
    target?.focus();
  }

  /** modal外をクリックしたときにmodalを閉じる */
  function handleClickOutside(event: MouseEvent) {
    if (!modalElement) return;
    if (!(event.target instanceof HTMLElement)) return;
    if (!modalElement.contains(event.target) && dismissible) {
      open = false;
    }
  }

  /** モーダル内のフォーカス可能な要素を取得する */
  function getFocusableElements(): HTMLElement[] {
    if (!modalElement) return [];
    return Array.from(modalElement.querySelectorAll<HTMLElement>('a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])'));
  }

  /** フォーカストラップ・escキーでmodalを閉じる */
  function handleKeyDown(event: KeyboardEvent) {
    // このモーダル内にフォーカスがなければ何もしない(複数モーダル対応)
    if (!modalElement?.contains(document.activeElement)) return;
    if (event.key === 'Escape' && dismissible) {
      open = false;
      return;
    }
    // フォーカストラップ: Tab/Shift+Tabでモーダル内のフォーカス可能要素を循環させる
    if (event.key === 'Tab') {
      const focusable = getFocusableElements();
      if (focusable.length === 0) {
        event.preventDefault();
        return;
      }
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (document.activeElement === modalElement) {
        // モーダル枠自体にフォーカスがある場合 → 先頭/末尾に移動
        event.preventDefault();
        (event.shiftKey ? last : first).focus();
      }
      else if (event.shiftKey && document.activeElement === first) {
        // 先頭でShift+Tab → 末尾に移動
        event.preventDefault();
        last.focus();
      }
      else if (!event.shiftKey && document.activeElement === last) {
        // 末尾でTab → 先頭に移動
        event.preventDefault();
        first.focus();
      }
    }
  }
</script>

<div class="contents" data-rabee-ui="modal">
  {#if open}
    <div class="fixed inset-0 z-40 bg-overlay-dark/50" transition:fade={{ duration: 150 }}></div>
    <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
      <div class={modalVariantsClass} role="dialog" aria-modal="true" bind:this={modalElement} tabindex="-1" transition:scale={{ start: 0.9, duration: 150 }}>
        {@render children()}
        {#if !hideCloseButton}
          <Button class="absolute top-2 right-2 p-2" tone="ghost" variant="secondary" size="small" isSquare onclick={() => (open = false)} data-close-button>
            <X size="1rem" />
          </Button>
        {/if}
      </div>
    </div>
  {/if}
</div>

      

依存コンポーネント

Modalを使うときは、以下のコンポーネントもダウンロードが必要です。

使い方


サンプル

Default

Defaultでの表示です。

HideCloseButton

閉じるボタンが非表示の状態です。

With Input,Select

InputSelectなどの要素と組み合わせることもできます。

Alert Dialog

ユーザーへの情報の通知のみを目的するシーンは、モーダルを閉じるアクションのみを提供するAlert Dialogとして使用できます。

Confirm Dialog

データの削除確認など、注意喚起が必要なシーンでは、モーダルを閉じる機能を無くすことでConfirm Dialogとして使用できます。

Focus Trap

inert属性を使用して、モーダルが開いている間は背景の要素にフォーカスできないようにしています。Tabキーやマウスクリックで背景の入力欄やボタンにフォーカスが移ることを防ぎます。

Loading

非同期処理の実行中など、画面遷移や再操作を防ぎたいシーンでは、Spinnerと組み合わせて使うことでLoadingとして使用できます。