モーダル(native dialog)
ネイティブの <dialog> を使ったモーダル。フォーカストラップと背面スクロールの抑制はブラウザ任せなので、JavaScriptは開閉の数行だけで済みます。
プレビュー
<div class="modal-demo">
<button class="modal-demo__open" type="button" data-modal-open>
モーダルを開く
</button>
<dialog class="modal" aria-labelledby="modal-title">
<form class="modal__form" method="dialog">
<header class="modal__header">
<h2 class="modal__title" id="modal-title">スニペットを削除しますか?</h2>
<button class="modal__close" type="submit" value="cancel" aria-label="閉じる">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</header>
<div class="modal__body">
<p>
この操作は取り消せません。削除するとスニペットのコードとプレビューがすべて失われます。
</p>
</div>
<footer class="modal__footer">
<button class="modal__button" type="submit" value="cancel">キャンセル</button>
<button class="modal__button modal__button--danger" type="submit" value="confirm">
削除する
</button>
</footer>
</form>
</dialog>
</div>/* このスニペット配下だけ box-sizing を揃える(既存のリセットとは競合しません) */
.modal-demo,
.modal-demo *,
.modal-demo *::before,
.modal-demo *::after,
.modal,
.modal *,
.modal *::before,
.modal *::after {
box-sizing: border-box;
}
.modal-demo__open {
padding: 0.75rem 1.25rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #ffffff;
color: #111827;
font: inherit;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
}
.modal-demo__open:hover {
border-color: #2563eb;
color: #2563eb;
}
.modal {
--modal-bg: #ffffff;
--modal-fg: #111827;
--modal-muted: #4b5563;
--modal-border: #e5e7eb;
--modal-accent: #2563eb;
--modal-danger: #dc2626;
width: min(90vw, 420px);
padding: 0;
border: 1px solid var(--modal-border);
border-radius: 12px;
background: var(--modal-bg);
color: var(--modal-fg);
box-shadow: 0 20px 50px rgb(0 0 0 / 25%);
font-size: 0.9375rem;
}
.modal::backdrop {
background: rgb(15 23 42 / 55%);
}
/* 開閉のフェード。非対応ブラウザでは単に即時表示になる */
@media (prefers-reduced-motion: no-preference) {
.modal,
.modal::backdrop {
transition: opacity 0.2s, display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
opacity: 0;
}
.modal[open],
.modal[open]::backdrop {
opacity: 1;
}
@starting-style {
.modal[open],
.modal[open]::backdrop {
opacity: 0;
}
}
}
.modal__form {
margin: 0;
}
.modal__header {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1.25rem 1.25rem 0;
}
.modal__title {
margin: 0;
font-size: 1rem;
font-weight: 700;
line-height: 1.6;
}
.modal__close {
flex: none;
display: grid;
place-items: center;
width: 30px;
height: 30px;
margin-left: auto;
padding: 0;
border: 0;
border-radius: 6px;
background: none;
color: var(--modal-muted);
cursor: pointer;
}
.modal__close svg {
width: 16px;
height: 16px;
}
.modal__close:hover {
background: color-mix(in srgb, var(--modal-fg) 8%, transparent);
}
.modal__body {
padding: 0.875rem 1.25rem 1.25rem;
color: var(--modal-muted);
line-height: 1.9;
}
.modal__body p {
margin: 0;
font-size: 0.875rem;
}
.modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.875rem 1.25rem;
border-top: 1px solid var(--modal-border);
}
.modal__button {
padding: 0.625rem 1rem;
border: 1px solid var(--modal-border);
border-radius: 8px;
background: transparent;
color: var(--modal-fg);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
}
.modal__button:hover {
background: color-mix(in srgb, var(--modal-fg) 6%, transparent);
}
.modal__button--danger {
border-color: var(--modal-danger);
background: var(--modal-danger);
color: #ffffff;
}
.modal__button--danger:hover {
background: var(--modal-danger);
opacity: 0.9;
}
.modal button:focus-visible,
.modal-demo__open:focus-visible {
outline: 2px solid var(--modal-accent);
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.modal {
--modal-bg: #1f2937;
--modal-fg: #f9fafb;
--modal-muted: #d1d5db;
--modal-border: #374151;
--modal-accent: #60a5fa;
--modal-danger: #ef4444;
}
.modal-demo__open {
border-color: #374151;
background: #1f2937;
color: #f9fafb;
}
}
/* ここから下はプレビュー表示用です。コピーして使うときは不要です */
body {
margin: 0;
background: #ffffff;
font-family: system-ui, -apple-system, "Segoe UI", "Hiragino Sans",
"Noto Sans JP", sans-serif;
color: #111827;
}
@media (prefers-color-scheme: dark) {
body {
background: #111827;
color: #f9fafb;
}
}
.modal-demo {
padding: 20px;
}
const dialog = document.querySelector(".modal");
const openButton = document.querySelector("[data-modal-open]");
openButton.addEventListener("click", function () {
// showModal() ならフォーカストラップと背面スクロール抑制はブラウザがやってくれる
dialog.showModal();
});
// 背景(::backdrop)をクリックしたら閉じる
dialog.addEventListener("click", function (event) {
if (event.target === dialog) dialog.close("cancel");
});
dialog.addEventListener("close", function () {
console.log("閉じたときの returnValue:", dialog.returnValue);
});