メインナビを <dialog> にしてみた

ボタンを押すと全画面で開くようなスマホのメインナビは <dialog> で実装するべきじゃないか、と思ったのでやってみた。

このブログのことではないです。

マークアップと css

閉じるボタンは <dialog> の中に入れないといけない。

<header>
  <!-- ナビを開くボタン -->
  <button id="nav-open">open</button>
  <dialog open>
    <nav>
      <!-- メインナビの中身略 -->
    </nav>
    <!-- ナビを閉じるボタン -->
    <button id="nav-close">close</button>
  </dialog>
</header>
/* PC時はボタン不要 */
button {
  display: none;
}

@media screen and (max-width: 780px) {
  button {
    display: flex;
  }
}

/* ブラウザによるcssを上書き
   borderとかもあるけど略 */
dialog {
  max-width: none;
  max-height: none;
  width: auto;
  height: auto;
}

PC 時(ビューポートが 780px 以上)を基準としたのでメインナビは <dialog open>、つまり非モーダルで開いた状態になっている。こいつを閉じられても(主にユーザーが)困るのでボタンは隠しておく。

やること

ビューポートを監視する

Resize Observer でやってみた。

const dialog = document.querySelector<HTMLDialogElement>('dialog')!;
const observeTarget = document.querySelector<HTMLElement>('header')!;

const resizeObserver = new ResizeObserver((entries) => {
  entries.forEach((entry) => {
    const entryWidth = entry.borderBoxSize[0].inlineSize;
    dialog.open = entryWidth >= 780;
  });
});
resizeObserver.observe(observeTarget);

ボタン

ボタンで <dialog> を開閉するだけでなく、<html> 要素にも class を付け外しする。

const html = document.querySelector<HTMLElement>('html')!;

document.querySelector<HTMLButtonElement>('#nav-open')?.addEventListener('click', () => {
  html.classList.add('nav-open');
  dialog.showModal();
});

document.querySelector<HTMLButtonElement>('#nav-close')?.addEventListener('click', () => {
  dialog.close();
});

// closeイベントで`<html>`のclassを外す
dialog.addEventListener('close', () => {
  html.classList.remove('nav-open');
});

スクロールバー

<dialog> の中身(<nav> 要素)がビューポートより大きい場合にスクロールバーが二重になってしまう。格好悪いのでビューポート側(<html><body> 要素のどちらか)のスクロールバーを隠す。

html.nav-open {
  &,
  body {
    overflow: hidden;
  }
}

感想

工数的には自力でモーダルを作るよりちょっとだけ楽か、という程度だった。アクセシビリティを考えるとたぶんマシなんだろう。

<dialog> がどこでも置けるのは便利ではある。自力モーダルだと親要素に position が指定されていて脱出できなかったりするからね。

はまったところ

当初 Resize Observer の対象を <html> 要素にしていたけど、スクロールバーを隠す処理のせいでボタンを押すと高さが変化する場合があった。

  1. ビューポート 780px 未満時にナビ開くボタンをプッシュ
  2. <dialog> がモーダルで開く(スクロールバーは隠れる)
  3. <html> 要素の高さが変わったのでリサイズ監視マンにより dialog?.open = entryWidth >= 780 が発動
  4. 幅は変わっていないため <dialog> から [open] が外れる
  5. 非表示のモーダル爆誕

非表示のモーダルは閉じるボタンも見えないし背後の要素に触ることもできない。Esc キーでモーダルを解除するしかない。