navとドロワーにdialogを使う

というごく当たり前の要求を満たしたい。

HTML

<nav> にあたる部分を <dialog> に入れる。また、開くボタンは <dialog> の外に、閉じるボタンは <dialog> の中に入れる。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script type="module" src="./dialog.js"></script>
</head>
<body>
<header>
<p class="logo"><a href="#">Logo</a></p>
<dialog id="nav" aria-label="menu" closedby="any">
<ul class="menu">
<li><a href="#">menu1</a></li>
<li><a href="#">menu2</a></li>
<li><a href="#">menu3</a></li>
</ul>
<button type="button" class="nav-close" id="nav-close" autofocus>
close
</button>
</dialog>
<button type="button" class="nav-open" id="nav-open">open</button>
</header>
</body>
</html>

CSS

PC 時(ビューポート 640px 以上)は <dialog>display:block にして、開閉ボタンを隠す。

<dialog> はブラウザ CSS で position:absolute が付いているので(Firefox だけ?)、static にしておく。

style.css
:is(html, body) {
margin: 0;
padding: 0;
}
.logo {
margin: 0;
}
header {
display: flex;
align-items: center;
gap: 0 1rem;
padding: 0.5rem;
}
.nav-open {
margin: 0 0 0 auto;
}
dialog {
margin: 0;
padding: 0;
border: 0;
}
main {
padding: 1rem 0.5rem;
}
/*
PC幅のとき
- dialogを表示する(display: block)
- 開くボタンを隠す
- 閉じるボタンを隠す
*/
@media (width > 640px) {
:is(header, .menu) {
display: flex;
align-items: center;
gap: 0 1rem;
}
dialog {
display: block;
position: static;
}
.menu {
margin: 0;
padding: 0;
list-style: none;
}
.nav-open {
display: none;
}
.nav-close {
display: none;
}
}
@media (width <= 640px) {
dialog {
inset: 0;
block-size: 100%;
inline-size: 100%;
background: rgb(255 255 255 / 0.75);
backdrop-filter: blur(0.1rem);
}
.menu {
padding: 0.5rem;
}
}

JS

無駄に長いけどやっていることはシンプルで、

dialog.js
const isSp = window.matchMedia("(width <= 640px)");
const dialog = document.getElementById("nav");
/**
* イベントハンドラをセット
* @param {HTMLElement} el
* @param {() => void} fn
*/
const setHandler = (el, fn) => {
el.addEventListener("click", fn);
};
/**
* イベントハンドラを外す
* @param {HTMLElement} el
* @param {() => void} fn
*/
const removeHandler = (el, fn) => {
el.removeEventListener("click", fn);
};
/**
* dialogを開く
*/
const openNav = () => {
if (isSp.matches && dialog instanceof HTMLDialogElement) {
dialog?.showModal();
}
};
/**
* dialogを閉じる
*/
const closeNav = () => {
if (isSp.matches && dialog instanceof HTMLDialogElement) {
dialog?.close();
}
};
const openButton = document.getElementById("nav-open");
if (openButton instanceof HTMLButtonElement) {
setHandler(openButton, openNav);
}
const closeButton = document.getElementById("nav-close");
if (closeButton instanceof HTMLButtonElement) {
setHandler(closeButton, closeNav);
}
isSp.addEventListener("change", (event) => {
if (!(dialog instanceof HTMLDialogElement)) {
return;
}
/**
* SP幅のとき
* - tabindexを外す
*/
if (event.matches) {
dialog.removeAttribute("tabIndex");
}
/**
* PC幅のとき
* - dialogのtabフォーカスを無効にする
* - dialogを閉じ、[open]を外す
*/
if (!event.matches) {
dialog.setAttribute("tabIndex", "-1");
dialog?.close();
dialog?.removeAttribute("open");
}
});
isSp.dispatchEvent(new Event("change"));