Modal basic
タイトル付きの基本モーダル。長文で本文スクロールが必要な場合は modal-basic-scroll を使用。
2023年12月以降、すべてのブラウザーで利用可能です。
- Baseline
- BaselineWidely available
- 対応ブラウザ
- Chrome120+Edge120+Firefox117+Safari17.2+
プレビューを読み込み中
<div class="modal-basic js-modal-basic">
<button class="modal-basic__open js-modal-basic-open" type="button">
モーダルを開く
</button>
<dialog
class="modal-basic__dialog js-modal-basic-dialog"
aria-label="モーダルタイトル"
>
<div class="modal-basic__header">
<h3 class="modal-basic__title">モーダルタイトル</h3>
<button
class="modal-basic__close js-modal-basic-close"
type="button"
aria-label="モーダルを閉じる"
>
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
aria-hidden="true"
>
<path
d="M5 5l12 12M17 5L5 17"
stroke="currentColor"
stroke-width="1.8"
/>
</svg>
</button>
</div>
<p class="modal-basic__text">
ここに説明テキストが入ります。ここに説明テキストが入ります。ここに説明テキストが入ります。
</p>
<div class="modal-basic__footer">
<button
class="modal-basic__button modal-basic__button--secondary js-modal-basic-close"
type="button"
>
キャンセル
</button>
<button
class="modal-basic__button modal-basic__button--primary js-modal-basic-close"
type="button"
>
OK
</button>
</div>
</dialog>
</div>
<div class="modal-basic js-modal-basic">
<button class="modal-basic__open js-modal-basic-open" type="button">
モーダルを開く
</button>
<dialog
class="modal-basic__dialog js-modal-basic-dialog"
aria-label="モーダルタイトル"
>
<div class="modal-basic__header">
<h3 class="modal-basic__title">モーダルタイトル</h3>
<button
class="modal-basic__close js-modal-basic-close"
type="button"
aria-label="モーダルを閉じる"
>
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
aria-hidden="true"
>
<path
d="M5 5l12 12M17 5L5 17"
stroke="currentColor"
stroke-width="1.8"
/>
</svg>
</button>
</div>
<p class="modal-basic__text">
ここに説明テキストが入ります。ここに説明テキストが入ります。ここに説明テキストが入ります。
</p>
<div class="modal-basic__footer">
<button
class="modal-basic__button modal-basic__button--secondary js-modal-basic-close"
type="button"
>
キャンセル
</button>
<button
class="modal-basic__button modal-basic__button--primary js-modal-basic-close"
type="button"
>
OK
</button>
</div>
</dialog>
</div>
/* reset */
:where(h3).modal-basic__title,
:where(p).modal-basic__text {
margin: 0;
font: inherit;
}
:where(button).modal-basic__open,
:where(button).modal-basic__close,
:where(button).modal-basic__button {
box-sizing: border-box;
padding: 0;
font: inherit;
vertical-align: middle;
color: inherit;
text-align: inherit;
appearance: none;
cursor: pointer;
background: none;
border: none;
}
:where(dialog).modal-basic__dialog {
box-sizing: border-box;
padding: 0;
border: none;
}
/* snippet */
.modal-basic {
--modal-background: #fff;
--modal-title-color: #364153;
--modal-text-color: #5e6775;
--modal-close-color: #364153;
--modal-interactive-background: #f5f5f5;
--modal-button-primary-background: #2b36f5;
--modal-button-primary-background-hover: #1f28a8;
--modal-button-primary-color: #fff;
--modal-button-secondary-border: #d1d5dc;
--modal-button-secondary-color: #364153;
--modal-backdrop: rgb(0 0 0 / 50%);
--modal-focus-color: var(--modal-button-primary-background);
--modal-duration: 0.3s;
}
.modal-basic__open {
display: inline-grid;
place-items: center;
padding: 12px 24px;
font-size: 14px;
font-weight: 700;
color: var(--modal-button-primary-color);
background-color: var(--modal-button-primary-background);
border-radius: calc(infinity * 1px);
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-button-primary-background-hover);
}
}
}
.modal-basic__dialog {
--modal-dialog-viewport-gap: 24px;
--modal-dialog-viewport-margin: calc(var(--modal-dialog-viewport-gap) * 2);
width: calc(100% - var(--modal-dialog-viewport-margin));
max-width: 480px;
max-height: calc(100dvh - var(--modal-dialog-viewport-margin));
padding: 24px;
margin: auto;
overflow-y: auto;
background-color: var(--modal-background);
border-radius: 12px;
opacity: 0;
transform: translateY(16px);
transition:
opacity var(--modal-duration),
transform var(--modal-duration);
}
.modal-basic__dialog--visible {
opacity: 1;
transform: translateY(0);
}
.modal-basic__dialog::backdrop {
background-color: var(--modal-backdrop);
opacity: 0;
transition: opacity var(--modal-duration);
}
.modal-basic__dialog--visible::backdrop {
opacity: 1;
}
.modal-basic__header {
display: flex;
column-gap: 16px;
align-items: flex-start;
justify-content: space-between;
}
.modal-basic__title {
font-size: 18px;
font-weight: 700;
line-height: 1.25;
color: var(--modal-title-color);
}
.modal-basic__close {
display: grid;
flex-shrink: 0;
place-items: center;
width: 40px;
height: 40px;
margin-block: -8px;
margin-inline: auto -8px;
color: var(--modal-close-color);
border-radius: 8px;
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-interactive-background);
}
}
}
.modal-basic__text {
margin-top: 16px;
font-size: 14px;
line-height: 1.6;
color: var(--modal-text-color);
}
.modal-basic__footer {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
justify-content: flex-end;
margin-top: 24px;
}
.modal-basic__button {
display: inline-grid;
place-items: center;
min-width: 120px;
padding: 12px 24px;
font-size: 14px;
font-weight: 700;
border-radius: calc(infinity * 1px);
}
.modal-basic__button--primary {
color: var(--modal-button-primary-color);
background-color: var(--modal-button-primary-background);
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-button-primary-background-hover);
}
}
}
.modal-basic__button--secondary {
color: var(--modal-button-secondary-color);
border: 1px solid var(--modal-button-secondary-border);
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-interactive-background);
}
}
}
/* focus */
.modal-basic__open,
.modal-basic__close,
.modal-basic__button {
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--modal-focus-color);
outline-offset: 2px;
}
}
/* reduced motion */
@media (prefers-reduced-motion: reduce) {
.modal-basic__open,
.modal-basic__close,
.modal-basic__button,
.modal-basic__dialog,
.modal-basic__dialog::backdrop {
transition: none;
}
}
/* reset */
:where(h3).modal-basic__title,
:where(p).modal-basic__text {
margin: 0;
font: inherit;
}
:where(button).modal-basic__open,
:where(button).modal-basic__close,
:where(button).modal-basic__button {
box-sizing: border-box;
padding: 0;
font: inherit;
vertical-align: middle;
color: inherit;
text-align: inherit;
appearance: none;
cursor: pointer;
background: none;
border: none;
}
:where(dialog).modal-basic__dialog {
box-sizing: border-box;
padding: 0;
border: none;
}
/* snippet */
.modal-basic {
--modal-background: #fff;
--modal-title-color: #364153;
--modal-text-color: #5e6775;
--modal-close-color: #364153;
--modal-interactive-background: #f5f5f5;
--modal-button-primary-background: #2b36f5;
--modal-button-primary-background-hover: #1f28a8;
--modal-button-primary-color: #fff;
--modal-button-secondary-border: #d1d5dc;
--modal-button-secondary-color: #364153;
--modal-backdrop: rgb(0 0 0 / 50%);
--modal-focus-color: var(--modal-button-primary-background);
--modal-duration: 0.3s;
}
.modal-basic__open {
display: inline-grid;
place-items: center;
padding: 12px 24px;
font-size: 14px;
font-weight: 700;
color: var(--modal-button-primary-color);
background-color: var(--modal-button-primary-background);
border-radius: calc(infinity * 1px);
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-button-primary-background-hover);
}
}
}
.modal-basic__dialog {
--modal-dialog-viewport-gap: 24px;
--modal-dialog-viewport-margin: calc(var(--modal-dialog-viewport-gap) * 2);
width: calc(100% - var(--modal-dialog-viewport-margin));
max-width: 480px;
max-height: calc(100dvh - var(--modal-dialog-viewport-margin));
padding: 24px;
margin: auto;
overflow-y: auto;
background-color: var(--modal-background);
border-radius: 12px;
opacity: 0;
transform: translateY(16px);
transition:
opacity var(--modal-duration),
transform var(--modal-duration);
}
.modal-basic__dialog--visible {
opacity: 1;
transform: translateY(0);
}
.modal-basic__dialog::backdrop {
background-color: var(--modal-backdrop);
opacity: 0;
transition: opacity var(--modal-duration);
}
.modal-basic__dialog--visible::backdrop {
opacity: 1;
}
.modal-basic__header {
display: flex;
column-gap: 16px;
align-items: flex-start;
justify-content: space-between;
}
.modal-basic__title {
font-size: 18px;
font-weight: 700;
line-height: 1.25;
color: var(--modal-title-color);
}
.modal-basic__close {
display: grid;
flex-shrink: 0;
place-items: center;
width: 40px;
height: 40px;
margin-block: -8px;
margin-inline: auto -8px;
color: var(--modal-close-color);
border-radius: 8px;
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-interactive-background);
}
}
}
.modal-basic__text {
margin-top: 16px;
font-size: 14px;
line-height: 1.6;
color: var(--modal-text-color);
}
.modal-basic__footer {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
justify-content: flex-end;
margin-top: 24px;
}
.modal-basic__button {
display: inline-grid;
place-items: center;
min-width: 120px;
padding: 12px 24px;
font-size: 14px;
font-weight: 700;
border-radius: calc(infinity * 1px);
}
.modal-basic__button--primary {
color: var(--modal-button-primary-color);
background-color: var(--modal-button-primary-background);
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-button-primary-background-hover);
}
}
}
.modal-basic__button--secondary {
color: var(--modal-button-secondary-color);
border: 1px solid var(--modal-button-secondary-border);
@media (any-hover: hover) {
transition: background-color var(--modal-duration);
&:hover {
background-color: var(--modal-interactive-background);
}
}
}
/* focus */
.modal-basic__open,
.modal-basic__close,
.modal-basic__button {
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--modal-focus-color);
outline-offset: 2px;
}
}
/* reduced motion */
@media (prefers-reduced-motion: reduce) {
.modal-basic__open,
.modal-basic__close,
.modal-basic__button,
.modal-basic__dialog,
.modal-basic__dialog::backdrop {
transition: none;
}
}
(function () {
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
let instanceIndex = 0;
document.querySelectorAll(".js-modal-basic").forEach((root) => {
const openButton = root.querySelector(".js-modal-basic-open");
const dialog = root.querySelector(".js-modal-basic-dialog");
if (!openButton || !dialog) return;
instanceIndex += 1;
const title = root.querySelector(".modal-basic__title");
if (title) {
const titleId = `modal-basic-title-${instanceIndex}`;
title.id = titleId;
dialog.setAttribute("aria-labelledby", titleId);
}
const visibleClass = "modal-basic__dialog--visible";
let isClosing = false;
let openRafId = 0;
let closeTransitionHandler = null;
let closeFallbackTimer = 0;
function getCloseTransitionMs() {
const duration = getComputedStyle(root)
.getPropertyValue("--modal-duration")
.trim();
if (!duration) return 0;
const ms = duration.endsWith("ms")
? parseFloat(duration)
: parseFloat(duration) * 1000;
return Number.isFinite(ms) ? ms : 0;
}
function cleanupCloseTransition() {
if (closeTransitionHandler) {
dialog.removeEventListener("transitionend", closeTransitionHandler);
closeTransitionHandler = null;
}
if (closeFallbackTimer) {
clearTimeout(closeFallbackTimer);
closeFallbackTimer = 0;
}
}
function finishClose() {
if (!isClosing) return;
cleanupCloseTransition();
dialog.classList.remove(visibleClass);
if (dialog.open) {
dialog.close();
}
isClosing = false;
}
function openDialog() {
if (dialog.open && !isClosing) return;
if (isClosing) {
cleanupCloseTransition();
isClosing = false;
dialog.classList.add(visibleClass);
return;
}
dialog.showModal();
openRafId = requestAnimationFrame(() => {
openRafId = 0;
if (!dialog.open || isClosing) return;
dialog.classList.add(visibleClass);
});
}
function closeDialog() {
if (!dialog.open) {
cleanupCloseTransition();
isClosing = false;
return;
}
if (isClosing) return;
cleanupCloseTransition();
if (openRafId) {
cancelAnimationFrame(openRafId);
openRafId = 0;
dialog.classList.remove(visibleClass);
dialog.close();
return;
}
const hasVisibleClass = dialog.classList.contains(visibleClass);
if (reducedMotion.matches || !hasVisibleClass) {
dialog.classList.remove(visibleClass);
dialog.close();
return;
}
isClosing = true;
dialog.classList.remove(visibleClass);
closeTransitionHandler = (event) => {
if (event.target !== dialog) return;
finishClose();
};
dialog.addEventListener("transitionend", closeTransitionHandler);
closeFallbackTimer = window.setTimeout(
finishClose,
getCloseTransitionMs() + 50,
);
}
openButton.addEventListener("click", openDialog);
dialog.querySelectorAll(".js-modal-basic-close").forEach((button) => {
button.addEventListener("click", closeDialog);
});
dialog.addEventListener("cancel", (event) => {
event.preventDefault();
closeDialog();
});
// ダイアログ本体の外側(バックドロップ)クリックで閉じる
dialog.addEventListener("click", (event) => {
if (event.target !== dialog) return;
const rect = dialog.getBoundingClientRect();
const inDialog =
rect.top <= event.clientY &&
event.clientY <= rect.bottom &&
rect.left <= event.clientX &&
event.clientX <= rect.right;
if (!inDialog) {
closeDialog();
}
});
});
})();
(function () {
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
let instanceIndex = 0;
document.querySelectorAll(".js-modal-basic").forEach((root) => {
const openButton = root.querySelector(".js-modal-basic-open");
const dialog = root.querySelector(".js-modal-basic-dialog");
if (!openButton || !dialog) return;
instanceIndex += 1;
const title = root.querySelector(".modal-basic__title");
if (title) {
const titleId = `modal-basic-title-${instanceIndex}`;
title.id = titleId;
dialog.setAttribute("aria-labelledby", titleId);
}
const visibleClass = "modal-basic__dialog--visible";
let isClosing = false;
let openRafId = 0;
let closeTransitionHandler = null;
let closeFallbackTimer = 0;
function getCloseTransitionMs() {
const duration = getComputedStyle(root)
.getPropertyValue("--modal-duration")
.trim();
if (!duration) return 0;
const ms = duration.endsWith("ms")
? parseFloat(duration)
: parseFloat(duration) * 1000;
return Number.isFinite(ms) ? ms : 0;
}
function cleanupCloseTransition() {
if (closeTransitionHandler) {
dialog.removeEventListener("transitionend", closeTransitionHandler);
closeTransitionHandler = null;
}
if (closeFallbackTimer) {
clearTimeout(closeFallbackTimer);
closeFallbackTimer = 0;
}
}
function finishClose() {
if (!isClosing) return;
cleanupCloseTransition();
dialog.classList.remove(visibleClass);
if (dialog.open) {
dialog.close();
}
isClosing = false;
}
function openDialog() {
if (dialog.open && !isClosing) return;
if (isClosing) {
cleanupCloseTransition();
isClosing = false;
dialog.classList.add(visibleClass);
return;
}
dialog.showModal();
openRafId = requestAnimationFrame(() => {
openRafId = 0;
if (!dialog.open || isClosing) return;
dialog.classList.add(visibleClass);
});
}
function closeDialog() {
if (!dialog.open) {
cleanupCloseTransition();
isClosing = false;
return;
}
if (isClosing) return;
cleanupCloseTransition();
if (openRafId) {
cancelAnimationFrame(openRafId);
openRafId = 0;
dialog.classList.remove(visibleClass);
dialog.close();
return;
}
const hasVisibleClass = dialog.classList.contains(visibleClass);
if (reducedMotion.matches || !hasVisibleClass) {
dialog.classList.remove(visibleClass);
dialog.close();
return;
}
isClosing = true;
dialog.classList.remove(visibleClass);
closeTransitionHandler = (event) => {
if (event.target !== dialog) return;
finishClose();
};
dialog.addEventListener("transitionend", closeTransitionHandler);
closeFallbackTimer = window.setTimeout(
finishClose,
getCloseTransitionMs() + 50,
);
}
openButton.addEventListener("click", openDialog);
dialog.querySelectorAll(".js-modal-basic-close").forEach((button) => {
button.addEventListener("click", closeDialog);
});
dialog.addEventListener("cancel", (event) => {
event.preventDefault();
closeDialog();
});
// ダイアログ本体の外側(バックドロップ)クリックで閉じる
dialog.addEventListener("click", (event) => {
if (event.target !== dialog) return;
const rect = dialog.getBoundingClientRect();
const inDialog =
rect.top <= event.clientY &&
event.clientY <= rect.bottom &&
rect.left <= event.clientX &&
event.clientX <= rect.right;
if (!inDialog) {
closeDialog();
}
});
});
})();