<div id="root"></div>
body {
height: 100vh;
margin: 0;
display: grid;
place-items: center;
}
button {
border-radius: 8px;
border: 2px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #f9f9f9;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto #646cff;
}
.c-dialog {
width: 100%;
max-width: 500px;
padding: 0;
border-radius: 10px;
border: none;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
text-align: center;
}
.c-dialog__inner {
padding: 20px;
}
.c-dialog__inner h2 {
font-size: 24px;
font-weight: 700;
}
.c-dialog__inner p {
font-size: 14px;
margin-top: 12px;
}
.c-btnGroup {
width: 100%;
display: flex;
justify-content: center;
gap: 10px;
margin-top: 24px;
}
View Compiled
import React, { useCallback, useEffect, useRef, useState } from "https://esm.sh/react@18";
import ReactDOM from "https://esm.sh/react-dom@18";
const App = () => {
const dialogRef = useRef(null);
const [trigger, setTrigger] = useState(null);
const openModal = useCallback(() => {
if (dialogRef.current) {
setTrigger(document.activeElement);
dialogRef.current.showModal();
document.body.style.overflow = "hidden";
}
}, []);
const closeModal = useCallback(() => {
if (dialogRef.current) {
dialogRef.current.close();
trigger?.focus();
document.body.style.overflow = "";
}
}, [trigger]);
/**
* フォーカストラップ
*/
useEffect(() => {
const dialogEl = dialogRef.current;
const focusableElements = Array.from(
dialogEl?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) ?? []
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
dialogEl?.addEventListener("keydown", handleKeyDown);
return () => {
dialogEl?.removeEventListener("keydown", handleKeyDown);
};
}, [dialogRef]);
/**
* モーダルの外側をクリックしたときにモーダルを閉じる
*/
useEffect(() => {
const dialogEl = dialogRef.current;
const handleBackdropClick = (event) => {
if (event.target === dialogEl && event.target !== dialogEl?.firstChild) {
closeModal();
}
};
dialogEl?.addEventListener("click", handleBackdropClick);
return () => {
dialogEl?.removeEventListener("click", handleBackdropClick);
};
}, [closeModal]);
/**
* モーダルを開いたときにスクロールを禁止する
* (コンポーネントがアンマウントされるときにスクロールを有効に戻す)
*/
useEffect(() => {
return () => {
document.body.style.overflow = "";
};
}, []);
return (
<>
<button onClick={() => openModal()}>Open Modal</button>
<dialog
className="c-dialog"
ref={dialogRef}
aria-labelledby="title"
aria-describedby="desc"
>
<div className="c-dialog__inner">
<h2 id="title">タイトル</h2>
<p id="desc">
このモーダルは、HTMLのdialog要素を使って実装されています。
</p>
<div class="c-btnGroup">
<button onClick={() => closeModal()}>Close Modal</button>
<button
onClick={() => {
alert("成功");
closeModal();
}}
>
アクション
</button>
</div>
</div>
</dialog>
</>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))
View Compiled
This Pen doesn't use any external JavaScript resources.