- メインナビの中身を複数書きたくない
- でもアクセシブルなドロワーを使いたい
というごく当たり前の要求を満たしたい。
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 にしておく。
: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>は常に「閉じた」状態にする
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"));