ウェブ制作

サイト制作のちょっとしたティップスまとめ

当サイトのリンクには広告が含まれています。

dialogタグで作るモーダルウィンドウ。CSSアニメとiOS対策、アクセシビリティ配慮まで解説

HTML標準のdialogタグを使って、モーダルウィンドウを実装する方法を解説。CSSアニメーションの設定。iOS Safariでの表示崩れ対策。フォーカス制御など、アクセシビリティにも配慮した実装例を紹介します。

ウェブ制作

公開日:2026/07/31

dialogタグは、確認画面やモーダル(※)をHTML標準機能で実装できる要素。

このタグが登場する以前は、モーダルを自作する場合、オーバーレイ要素、背景操作の制御、フォーカス管理、キーボード操作、開閉状態の管理など、開閉ギミックやアクセシビリティ対応をJavaScriptでゴリゴリ書く必要がありました。

dialogタグは、それらモーダル実装で必要になりがちな機能の多くをブラウザ標準の仕組みにお任せすることができます。

ただし、現時点(2026年7月)では一部端末(iOS)だと、CSSアニメーションの設定に少し課題アリ。

今回はそのあたりの対策も含めて、dialogタグを使ったモーダルウィンドウの実装例を紹介します。

(※)なお、本記事では便宜上「モーダル」と表現しますが、厳密にはdialogタグは本来、確認・選択・通知などを促すダイアログのための要素です。
しかし実務では、画像・情報・メニューなどを重ねて表示する「モーダル」用途での活用が期待されるため、本記事ではモーダル実装の文脈で扱います。

デモコード(開閉アニメーション対応版)

See the Pen dialogで作るモーダル(CSSアニメ付き) by MEMORUKA (@memoruka) on CodePen.

今回のデモでは、以下を実装しています。

  • dialogタグによるモーダル表示
  • 背景レイヤーの装飾
  • 背景スクロールの固定
  • 開く・閉じる際の CSSアニメーション(transition)
  • 背景クリック、Esc キー、閉じるボタンでの終了
  • iOS Safariで閉じる際に発生する表示崩れへの対策

HTML

<button type="button" class="demo-dialog-open js-dialog-open" data-dialog-target="demo-dialog">モーダル表示</button>

<dialog id="demo-dialog" aria-labelledby="demo-dialog-title-1">
    <div class="demo-dialog__inner js-dialog__inner">
        <h2 id="demo-dialog-title-1" class="demo-title">モーダル(dialog)</h2>
        <form method="dialog">
            <button type="submit" class="demo-dialog-close" aria-label="閉じる"></button>
        </form>

        <p>
            ここにテキストが入ります。
        </p>
    </div>
</dialog>

HTMLは非常にシンプル。

モーダルを開くボタン(.js-dialog-open)と、対象となるdialogタグを用意するだけで基本構造が完成します。

従来の自作モーダルのように、表示状態を管理するための aria-hiddenaria-modal を細かく付け外しする必要はありません。また、モーダル表示時のオーバーレイ専用ラッパー要素を追加する必要もありません。背景レイヤーは、後述するdialog::backdropで装飾できます。この点も地味に便利。

dialog内部のHTML構造にも、特別な階層ルールはありません。

今回は.demo-dialog__innerをモーダル本体として配置し、見出し、テキストなど、必要な内容を自由に記述できる形にしています。

CSS

/* - 背景固定 */
body:has(dialog[open]) {
    overflow: hidden;
}

/* - タッチ操作の制御 */
.js-dialog-open {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

/* - 初回フォーカス位置の制御 */
.js-dialog__inner:focus {
    outline: none;
}

dialog {
    --transition-speed: .3s;
    --translate-position: 50px;

    /* - 装飾はinner側で行うため、dialog標準の余白・背景・枠線を消す */
    padding: 0;
    background: transparent;
    border: 0;

    opacity: 0;
    transform: translateY(var(--translate-position));
    transition:
        opacity var(--transition-speed),
        transform var(--transition-speed);
    overflow: visible;

    /* - open時の表示状態 */
    &[open] {
        opacity: 1;
        transform: translateY(0);
    }

    /* - 閉じる時はJSでclassを付け、dialogをopenのままフェードアウトさせる */
    &[open].is-closing {
        opacity: 0;
        transform: translateY(0);
    }

    /* - 背景レイヤーの初期状態 */
    &::backdrop {
        opacity: 0;
        background-color: transparent;
        backdrop-filter: blur(0);
        transition:
            opacity var(--transition-speed),
            background-color var(--transition-speed),
            backdrop-filter var(--transition-speed);
    }

    /* - open時の背景レイヤー */
    &[open]::backdrop {
        opacity: 1;
        background-color: rgba(255, 255, 255, 0.3);
        backdrop-filter: blur(5px);
    }

    /* - 閉じる時の背景レイヤー */
    &[open].is-closing::backdrop {
        opacity: 0;
        background-color: transparent;
        backdrop-filter: blur(0);
    }

    /* - open時だけ、下からフェードインする開始状態を定義する */
    @starting-style {
        &[open] {
            opacity: 0;
            transform: translateY(var(--translate-position));
        }

        &[open]::backdrop {
            opacity: 0;
            background-color: transparent;
            backdrop-filter: blur(0);
        }
    }

    /* - モーダル本体の装飾 */
    .demo-dialog__inner {
        position: relative;
        max-width: 750px;
        padding: 2em;
        background: #fff;
        border: 3px solid #333;
        border-radius: 10px;
    }
}

CSSはこんなカンジ。

実際に現場での使用を想定して、必要と思われる要素を詰め込みました。

  • 背景スクロールの固定
  • ::backdropによる背景レイヤーの装飾
  • 表示時のフェードインおよびスライドアップ
  • 閉じる時のフェードアウト
  • モーダル本体の見た目の装飾

body:has(dialog[open])

body:has(dialog[open]) {
    overflow: hidden;
}

dialogが開いている間だけ、背景ページのスクロールを固定します。

dialogそのものの機能ではありませんが、:hasを使えば開閉状態を容易に検知可能。JavaScriptでbodyに状態用クラスを付け外ししなくても、open属性をそのまま状態判定に使えます。

:has()最高!モダンCSSで一番誕生を喜んだ疑似クラスだよ。

尚、PC環境でスクロールバー消失による横ズレが気になる場合は、あわせて次の指定も検討できます。

html {
    scrollbar-gutter: stable;
}

dialog

dialog自体には、余白・背景・枠線のリセットと、表示状態の制御に必要なスタイルを設定しています。

実際のモーダル部分の見た目は、dialogタグでは無く、内側の.demo-dialog__innerに集約。
モーダル本体の視覚的な装飾は、内側の要素へ寄せることで、ダイアログ本来の表示・状態管理と、デザイン用スタイルを分けやすくなります。

dialog[open]

[open]は、dialogが開いている時に付与される属性です。
この状態でopacity: 1transform: translateY(0)を指定し、表示時には下からふわっとフェードインする動きを作っています。

dialog[open].is-closing

閉じる際に使用する状態クラスです。

本来、dialogの閉じる機能にこのような専用クラスは不要です。

しかし通常のclose()を即時実行すると、open属性が外れ、dialogはすぐに非表示になります。そのため、閉じるアニメーションを確実に見せるには少し工夫が必要でした。
このデモでは、まず.is-closingを付与してフェードアウトだけを開始し、アニメーション完了後にclose()を実行します。

dialog::backdrop

showModal()で表示したdialogには、::backdrop疑似要素を使えます。
ここでは、背景の半透明化とぼかしを設定しています。オーバーレイ用のHTML要素を追加せずに、背景レイヤーを作成できるのがdialogの地味に便利な点だと思います。

@starting-style

@starting-styleは、要素が非表示状態から表示状態へ切り替わる際の開始スタイルを定義するための CSS です。
このデモでは、開く時だけ「下からフェードイン」、閉じる時は「その位置のままフェードアウト」というように、アニメ処理を分けたいので、@starting-styleで表示開始時の位置と透明度を定義しています。

.demo-dialog__inner

実質的なモーダル本体です。
最大幅、余白、枠線、角丸など、モーダルのデザインに関するスタイルはここへ集約します。

JavaScript(jQuery)

(function ($) {
    $(function () {

        function dialogOpen() {
            const trigger = $('.js-dialog-open');
            const innerSelector = '.js-dialog__inner';
            const closingClass = 'is-closing';
            const closeOnOverlay = true;
            const closeAnimationDuration = 300; // CSSの var(--transition-speed)の値と一致させる

            function closeDialog(dialog) {
                if (!dialog || !dialog.open || dialog.classList.contains(closingClass)) {
                    return;
                }

                // dialogをopen状態のまま維持し、閉じるアニメーション用classを付与する
                dialog.classList.add(closingClass);

                // アニメーション完了後にネイティブのclose()を実行し、状態を初期化する
                setTimeout(function () {
                    if (dialog.open) {
                        dialog.close();
                    }

                    dialog.classList.remove(closingClass);
                }, closeAnimationDuration);
            }

            // data-dialog-target に指定されたIDのdialogを開く
            trigger.on('click', function () {
                const targetId = $(this).attr('data-dialog-target');
                const dialog = document.getElementById(targetId);

                // 対象が存在しない場合、またはdialog未対応環境では何もしない
                if (!dialog || typeof dialog.showModal !== 'function') {
                    return;
                }

                dialog.classList.remove(closingClass);
                dialog.showModal();

                // フォーカスの制御
                const inner = dialog.querySelector(innerSelector);

                if (inner) {
                    inner.setAttribute('tabindex', '-1');

                    requestAnimationFrame(function () {
                        inner.focus({ preventScroll: true });
                    });
                }

            });

            $('dialog').each(function () {
                const dialog = this;

                // form method="dialog" の即時closeを止め、閉じるアニメーション経由にする
                $(dialog).find('form[method="dialog"]').on('submit', function (event) {
                    event.preventDefault();
                    closeDialog(dialog);
                });

                // Escキーによる即時closeを止め、閉じるアニメーション経由にする
                dialog.addEventListener('cancel', function (event) {
                    event.preventDefault();
                    closeDialog(dialog);
                });
            });

            // 背景クリックを無効にする場合は、ここで処理を終了する
            if (!closeOnOverlay) {
                return;
            }

            // dialog内のinner要素より外側をクリックした場合のみ閉じる
            $('dialog').on('click', function (event) {
                const inner = this.querySelector(innerSelector);

                if (!inner) {
                    return;
                }

                if (!inner.contains(event.target)) {
                    closeDialog(this);
                }
            });
        }
        dialogOpen();

    });
})(jQuery);

JavaScript(jQuery)はこちら。

一見、従来の自作モーダル同様、ゴリゴリJSで制御しているように見えるかもだけど、ここで行っているのは主に補助処理であって、モーダル機能そのものではない。モーダル表示、キーボード操作、基本的なフォーカス管理などは、主にdialogが標準でやってくれるので安心です。

ここでは基本設定と以下の補助動作を追加しています。

  • モーダルを開くボタンと対象dialogの紐付け
  • 背景クリックで閉じる処理
  • 初期フォーカス位置の調整
  • 閉じるアニメーション完了後にclose()を実行する処理

このうち、「開くボタンと対象dialogの紐付け」は必須。「背景クリックで閉じる処理」はユーザビリティ的な配慮。

その他の以下2点は、実務で必要と判断し、独自に用意した処理です。

フォーカスの制御

dialogを開くと、ブラウザはダイアログ内の最初のフォーカス可能な要素へフォーカスを移します。

PCでは、キーボード操作をしていない限りあまり意識することはありませんが、スマホ、特にiOS端末の場合、フォーカスした要素にoutline(フォーカスインジケーター)が表示された状態になります。

結果、「変な線が表示されている」「デザインと違う」といった誤解に基づく指摘を受けました。

outlineはcssで無効化する事も出来ますが、それはそれでアクセシビリティ対応としてマズイよなぁ……。

ということで悩んだ結果、モーダル自身をtabindex="-1"でフォーカス状態にし、その場所のoutlineのみ無効化。実際の操作要素へ移動した後のフォーカス表示は維持することで対処しました。

閉じるアニメーション

閉じる処理は、次の流れで実行しています。

  1. .is-closingを付与し、dialogをopen状態のままフェードアウトさせる
  2. CSS transitionの完了を待つ
  3. 完了後にclose()を実行して、実際にモーダルを閉じる。

つまり、アニメーション中は見た目だけを消しており、dialogそのものはまだ開いた状態です。アニメーション完了後にclose()を実行することで、視覚上の消失と実際の終了タイミングを揃えています。

んで、なんでこんなまどろっこしい事をやっているのかというと、iOS対策のためです。。

close()を即時実行した場合、open属性が外れますが、手元の検証では、WindowsやAndroid環境では close() を実行しても、displayoverlay の離散トランジション※が期待どおりに機能し、CSSだけで閉じるアニメーションまで表現できました。

※離散トランジション
display: none / block等、通常は中間値を計算・変化(補間)できず、一瞬で切り替わってしまうプロパティにアニメーションを対応させる

……が、そうは問屋が卸さなかったのがiPhone。
離散トランジションが効かず、close()を実行した瞬間、CSSが一瞬崩壊 → そのまま閉じる…という挙動になってしまいました。。
そのため、このデモでは閉じる操作をpreventDefault()で止め、即時に閉じず、上記のアニメーション処理を経由させています。

まとめ

以上、dialogタグを使ったモーダルウィンドウの作り方でした。

dialogタグを使うと、モーダル実装で必要になりがちなHTML構造やJavaScriptを大きく減らせます。特にフォーカス管理、Esc キー対応などを標準機能として活用できるのは大きな利点です。

一方で、アニメーションを追加したい場合、現時点ではまだ課題がある模様。

まずはシンプルな確認ダイアログから使い始め、必要に応じて今回のようなアニメーション演出や補助処理を足していくのが扱いやすいと思います。

上に戻る