モーダルダイアログ
ネイティブ<dialog>要素を使ったモーダル。フォーカストラップやESC閉じがブラウザ標準で動作し、確認や課金導線に使えます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:プランアップグレードの確認をモーダルで表示 -->
<div class="app">
<header class="app__bar">
<span class="app__logo">💼 FlowDesk</span>
<nav class="app__nav"><a>ダッシュボード</a><a>メンバー</a><a class="is-active">プラン</a></nav>
</header>
<main class="app__main">
<div class="plan">
<div class="plan__head"><span class="plan__name">Pro プラン</span><span class="plan__badge">人気</span></div>
<p class="plan__price">¥1,480<span>/ ユーザー / 月</span></p>
<ul class="plan__feats">
<li>無制限のプロジェクト</li>
<li>高度な権限管理</li>
<li>優先サポート</li>
</ul>
<button class="btn btn--primary" data-open>Proにアップグレード</button>
</div>
</main>
<!-- ネイティブ dialog 要素でモーダル化 -->
<dialog class="modal" data-modal>
<form method="dialog" class="modal__inner">
<div class="modal__icon">↑</div>
<h2 class="modal__title">Proプランへ変更しますか?</h2>
<p class="modal__text">現在のFreeプランからProへ変更します。次回請求日より <strong>¥1,480 / ユーザー</strong> が適用されます。いつでも変更・解約が可能です。</p>
<div class="modal__actions">
<button class="btn btn--ghost" value="cancel" data-close>キャンセル</button>
<button class="btn btn--primary" value="ok" data-confirm>アップグレード</button>
</div>
</form>
</dialog>
<div class="toastline" data-result></div>
</div>
CSS
/* FlowDesk SaaS テーマ */
:root{--navy:#0f1b34;--blue:#4f7cff;--ink:#1d2740;--line:#e3e8f2;--muted:#6b7794;--bg:#f4f6fb}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--ink)}
.app{max-width:560px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column}
.app__bar{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--navy);color:#fff}
.app__logo{font-weight:700;letter-spacing:.02em}
.app__nav{display:flex;gap:16px;font-size:.8rem;color:#aab6d6}
.app__nav .is-active{color:#fff;font-weight:600}
.app__main{flex:1;display:grid;place-items:center;padding:20px}
.plan{background:#fff;border:1px solid var(--line);border-radius:16px;padding:22px 24px;width:min(320px,100%);box-shadow:0 10px 30px rgba(15,27,52,.08)}
.plan__head{display:flex;align-items:center;gap:10px}
.plan__name{font-weight:700;font-size:1.1rem}
.plan__badge{background:#eaf0ff;color:var(--blue);font-size:.68rem;font-weight:700;padding:3px 9px;border-radius:999px}
.plan__price{margin:10px 0 14px;font-size:1.9rem;font-weight:800;color:var(--navy)}
.plan__price span{font-size:.78rem;font-weight:500;color:var(--muted)}
.plan__feats{list-style:none;margin:0 0 18px;padding:0;font-size:.86rem;color:var(--muted)}
.plan__feats li{padding:5px 0 5px 22px;position:relative}
.plan__feats li::before{content:"✓";position:absolute;left:0;color:var(--blue);font-weight:800}
.btn{appearance:none;border:none;cursor:pointer;font:inherit;font-weight:600;padding:11px 18px;border-radius:10px;transition:filter .2s,background .2s}
.btn--primary{background:var(--blue);color:#fff;width:100%}
.btn--primary:hover{filter:brightness(1.06)}
.btn--ghost{background:#fff;border:1px solid var(--line);color:var(--muted)}
/* モーダル本体 */
.modal{border:none;border-radius:18px;padding:0;width:min(360px,92vw);box-shadow:0 24px 60px rgba(15,27,52,.3)}
.modal::backdrop{background:rgba(15,27,52,.45);backdrop-filter:blur(2px)}
.modal[open]{animation:pop .25s cubic-bezier(.34,1.4,.6,1)}
@keyframes pop{from{opacity:0;transform:translateY(12px) scale(.96)}to{opacity:1;transform:none}}
.modal__inner{padding:26px 24px;text-align:center}
.modal__icon{width:48px;height:48px;margin:0 auto 12px;border-radius:50%;background:#eaf0ff;color:var(--blue);font-size:1.4rem;font-weight:800;display:grid;place-items:center}
.modal__title{margin:0 0 8px;font-size:1.15rem}
.modal__text{margin:0 0 20px;font-size:.86rem;line-height:1.7;color:var(--muted)}
.modal__actions{display:flex;gap:10px}
.modal__actions .btn{flex:1}
.toastline{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);display:flex;flex-direction:column;gap:8px;align-items:center}
.toastline .ok{background:var(--navy);color:#fff;padding:10px 18px;border-radius:10px;font-size:.84rem;box-shadow:0 8px 24px rgba(15,27,52,.25);animation:rise .3s ease}
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.modal[open],.toastline .ok{animation:none}}
JavaScript
// ネイティブ<dialog>でモーダルを開閉(フォーカストラップ/ESCは標準動作)
const dialog = document.querySelector('[data-modal]');
const openBtn = document.querySelector('[data-open]');
const result = document.querySelector('[data-result]');
openBtn?.addEventListener('click', () => {
if (dialog && typeof dialog.showModal === 'function') dialog.showModal();
});
// 確定時に簡単なフィードバックを表示
dialog?.addEventListener('close', () => {
if (dialog.returnValue === 'ok' && result) {
const toast = document.createElement('div');
toast.className = 'ok';
toast.textContent = '✓ Proプランへ変更しました';
result.appendChild(toast);
setTimeout(() => toast.remove(), 2600); // 自動で消す
}
});
コード
HTML
<!-- モーダル:native <dialog> でフォーカストラップとESC閉じを実現 -->
<div class="stage">
<button class="btn btn--open" data-open>プランをアップグレード</button>
<dialog class="modal" data-modal>
<div class="modal__glow" aria-hidden="true"></div>
<button class="modal__x" data-close aria-label="閉じる">×</button>
<span class="modal__badge">PRO</span>
<h2 class="modal__title">プレミアムにアップグレード</h2>
<p class="modal__lead">広告非表示・無制限の保存・優先サポート。いつでも解約できます。</p>
<ul class="modal__list">
<li>クラウド保存 無制限</li>
<li>高解像度エクスポート</li>
<li>限定テーマ 40種以上</li>
</ul>
<div class="modal__actions">
<button class="btn btn--ghost" data-close>あとで</button>
<button class="btn btn--cta" data-close>月額980円ではじめる</button>
</div>
</dialog>
</div>
CSS
:root{
--bg:#0b1020;
--card:#141a2e;
--cta:#f59e0b;
--accent:#818cf8;
--text:#e7ebf5;
--muted:#9aa3bd;
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;
display:grid;place-items:center;padding:24px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:
radial-gradient(700px 360px at 50% 120%,#27306a,transparent),
var(--bg);
}
.btn{
font:inherit;font-weight:600;cursor:pointer;border:none;border-radius:12px;
padding:13px 20px;transition:transform .15s,box-shadow .25s,background .2s;
}
.btn:active{transform:translateY(1px)}
.btn--open{
color:#0b1020;
background:linear-gradient(135deg,#fbbf24,#f59e0b);
box-shadow:0 12px 30px -10px rgba(245,158,11,.6);
}
.btn--open:hover{box-shadow:0 16px 36px -10px rgba(245,158,11,.8)}
/* dialog のデフォルトを解除して独自スタイル */
.modal{
position:relative;
width:min(380px,92vw);
border:1px solid rgba(129,140,248,.25);
border-radius:20px;
padding:28px 26px 24px;
background:var(--card);
color:var(--text);
overflow:hidden;
box-shadow:0 40px 80px -24px rgba(0,0,0,.7);
}
.modal::backdrop{
background:rgba(6,10,22,.6);
backdrop-filter:blur(4px);
}
/* 開閉アニメーション */
.modal[open]{animation:pop .3s cubic-bezier(.2,.9,.3,1.2)}
@keyframes pop{from{opacity:0;transform:translateY(14px) scale(.96)}to{opacity:1;transform:none}}
.modal__glow{
position:absolute;inset:-40% 30% auto -10%;height:160px;
background:radial-gradient(closest-side,rgba(129,140,248,.45),transparent);
filter:blur(10px);
}
.modal__x{
position:absolute;top:12px;right:14px;
width:34px;height:34px;border:none;border-radius:50%;cursor:pointer;
background:rgba(255,255,255,.06);color:var(--muted);font-size:22px;line-height:1;
}
.modal__x:hover{background:rgba(255,255,255,.14);color:#fff}
.modal__badge{
display:inline-block;font-size:.7rem;font-weight:700;letter-spacing:.14em;
color:#0b1020;background:linear-gradient(135deg,#a5b4fc,#818cf8);
padding:4px 10px;border-radius:999px;
}
.modal__title{margin:12px 0 6px;font-size:1.3rem}
.modal__lead{margin:0 0 16px;color:var(--muted);line-height:1.65;font-size:.92rem}
.modal__list{margin:0 0 22px;padding:0;list-style:none;display:grid;gap:8px}
.modal__list li{position:relative;padding-left:26px;font-size:.92rem}
.modal__list li::before{
content:"✓";position:absolute;left:0;top:-1px;
width:18px;height:18px;border-radius:50%;
display:grid;place-items:center;font-size:.7rem;font-weight:700;
color:#0b1020;background:var(--accent);
}
.modal__actions{display:flex;gap:10px}
.btn--ghost{flex:0 0 auto;background:rgba(255,255,255,.07);color:var(--text)}
.btn--ghost:hover{background:rgba(255,255,255,.13)}
.btn--cta{flex:1;color:#0b1020;background:linear-gradient(135deg,#fbbf24,#f59e0b)}
@media (prefers-reduced-motion:reduce){.modal[open]{animation:none}}
JavaScript
// native <dialog> を利用(フォーカストラップ/ESCはブラウザ標準)
const modal = document.querySelector('[data-modal]');
const openBtn = document.querySelector('[data-open]');
if (modal && openBtn) {
const open = () => {
// showModal が無い環境でも落ちないようガード
if (typeof modal.showModal === 'function') modal.showModal();
else modal.setAttribute('open', '');
};
const close = () => {
if (typeof modal.close === 'function') modal.close();
else modal.removeAttribute('open');
};
openBtn.addEventListener('click', open);
// data-close を持つ全ボタンで閉じる
modal.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', close));
// 背景(バックドロップ)クリックで閉じる
modal.addEventListener('click', (e) => {
if (e.target === modal) close();
});
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「モーダルダイアログ」の効果を追加してください。
# 追加してほしい効果
モーダルダイアログ(UIコンポーネント)
ネイティブ<dialog>要素を使ったモーダル。フォーカストラップやESC閉じがブラウザ標準で動作し、確認や課金導線に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- モーダル:native <dialog> でフォーカストラップとESC閉じを実現 -->
<div class="stage">
<button class="btn btn--open" data-open>プランをアップグレード</button>
<dialog class="modal" data-modal>
<div class="modal__glow" aria-hidden="true"></div>
<button class="modal__x" data-close aria-label="閉じる">×</button>
<span class="modal__badge">PRO</span>
<h2 class="modal__title">プレミアムにアップグレード</h2>
<p class="modal__lead">広告非表示・無制限の保存・優先サポート。いつでも解約できます。</p>
<ul class="modal__list">
<li>クラウド保存 無制限</li>
<li>高解像度エクスポート</li>
<li>限定テーマ 40種以上</li>
</ul>
<div class="modal__actions">
<button class="btn btn--ghost" data-close>あとで</button>
<button class="btn btn--cta" data-close>月額980円ではじめる</button>
</div>
</dialog>
</div>
【CSS】
:root{
--bg:#0b1020;
--card:#141a2e;
--cta:#f59e0b;
--accent:#818cf8;
--text:#e7ebf5;
--muted:#9aa3bd;
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;
display:grid;place-items:center;padding:24px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:
radial-gradient(700px 360px at 50% 120%,#27306a,transparent),
var(--bg);
}
.btn{
font:inherit;font-weight:600;cursor:pointer;border:none;border-radius:12px;
padding:13px 20px;transition:transform .15s,box-shadow .25s,background .2s;
}
.btn:active{transform:translateY(1px)}
.btn--open{
color:#0b1020;
background:linear-gradient(135deg,#fbbf24,#f59e0b);
box-shadow:0 12px 30px -10px rgba(245,158,11,.6);
}
.btn--open:hover{box-shadow:0 16px 36px -10px rgba(245,158,11,.8)}
/* dialog のデフォルトを解除して独自スタイル */
.modal{
position:relative;
width:min(380px,92vw);
border:1px solid rgba(129,140,248,.25);
border-radius:20px;
padding:28px 26px 24px;
background:var(--card);
color:var(--text);
overflow:hidden;
box-shadow:0 40px 80px -24px rgba(0,0,0,.7);
}
.modal::backdrop{
background:rgba(6,10,22,.6);
backdrop-filter:blur(4px);
}
/* 開閉アニメーション */
.modal[open]{animation:pop .3s cubic-bezier(.2,.9,.3,1.2)}
@keyframes pop{from{opacity:0;transform:translateY(14px) scale(.96)}to{opacity:1;transform:none}}
.modal__glow{
position:absolute;inset:-40% 30% auto -10%;height:160px;
background:radial-gradient(closest-side,rgba(129,140,248,.45),transparent);
filter:blur(10px);
}
.modal__x{
position:absolute;top:12px;right:14px;
width:34px;height:34px;border:none;border-radius:50%;cursor:pointer;
background:rgba(255,255,255,.06);color:var(--muted);font-size:22px;line-height:1;
}
.modal__x:hover{background:rgba(255,255,255,.14);color:#fff}
.modal__badge{
display:inline-block;font-size:.7rem;font-weight:700;letter-spacing:.14em;
color:#0b1020;background:linear-gradient(135deg,#a5b4fc,#818cf8);
padding:4px 10px;border-radius:999px;
}
.modal__title{margin:12px 0 6px;font-size:1.3rem}
.modal__lead{margin:0 0 16px;color:var(--muted);line-height:1.65;font-size:.92rem}
.modal__list{margin:0 0 22px;padding:0;list-style:none;display:grid;gap:8px}
.modal__list li{position:relative;padding-left:26px;font-size:.92rem}
.modal__list li::before{
content:"✓";position:absolute;left:0;top:-1px;
width:18px;height:18px;border-radius:50%;
display:grid;place-items:center;font-size:.7rem;font-weight:700;
color:#0b1020;background:var(--accent);
}
.modal__actions{display:flex;gap:10px}
.btn--ghost{flex:0 0 auto;background:rgba(255,255,255,.07);color:var(--text)}
.btn--ghost:hover{background:rgba(255,255,255,.13)}
.btn--cta{flex:1;color:#0b1020;background:linear-gradient(135deg,#fbbf24,#f59e0b)}
@media (prefers-reduced-motion:reduce){.modal[open]{animation:none}}
【JavaScript】
// native <dialog> を利用(フォーカストラップ/ESCはブラウザ標準)
const modal = document.querySelector('[data-modal]');
const openBtn = document.querySelector('[data-open]');
if (modal && openBtn) {
const open = () => {
// showModal が無い環境でも落ちないようガード
if (typeof modal.showModal === 'function') modal.showModal();
else modal.setAttribute('open', '');
};
const close = () => {
if (typeof modal.close === 'function') modal.close();
else modal.removeAttribute('open');
};
openBtn.addEventListener('click', open);
// data-close を持つ全ボタンで閉じる
modal.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', close));
// 背景(バックドロップ)クリックで閉じる
modal.addEventListener('click', (e) => {
if (e.target === modal) close();
});
}
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。