ナビゲーション

モバイルドロワー

モバイル用に主要導線を抽出し、開閉・背景固定・戻る操作を確認できるドロワーメニューです。

動きを試す

プレビュー

プレビュー設定

プレビューを準備しています。

必要なファイルを選ぶ

コード

index.html

利用前に変更する箇所

  • メニュー項目とアイコン群を運用文脈へ変える

<!doctype html>
<html lang="ja" data-theme="light">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>モバイルナビゲーションドロワー</title>
    <style>
      :root { --bg:#f7f8fa; --surface:#ffffff; --surface-subtle:#eff2f6; --text:#18212f; --muted:#5d6b7a; --border:#d7dee8; --accent:#245fd6; --focus:#0077cc; }
      :root[data-theme="dark"] { --bg:#0f141a; --surface:#171d26; --surface-subtle:#202834; --text:#f2f5f8; --muted:#aab4c0; --border:#354152; --accent:#7ea7ff; --focus:#65c7ff; }
      * { box-sizing: border-box; }
      body { margin: 0; padding: 16px; font-family: Arial, sans-serif; background: var(--bg); color: var(--text); }
      body[data-drawer-open="true"] { overflow: hidden; }
      .layout { max-width: 700px; margin: 0 auto; }
      .header { display: flex; justify-content: space-between; align-items: center; padding: 10px; border:1px solid var(--border); border-radius: 12px; background: var(--surface); }
      .drawer { position: fixed; inset: 0; }
      .drawer[hidden] { display: none; }
      .backdrop {
        position: absolute; inset: 0; background: rgba(15,20,26,.35); opacity: 0; transition: opacity .15s ease;
      }
      .panel {
        position: absolute; top: 0; right: 0; width: min(280px, 80%); height: 100%; background: var(--surface); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform .15s ease;
        padding: 12px;
      }
      body[data-drawer-open="true"] .backdrop { opacity: 1; }
      body[data-drawer-open="true"] .panel { transform: translateX(0); }
      button { min-height: 44px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); padding: 8px 12px; }
      .status { min-height: 1.4em; margin-top: 8px; }
      .no-script-navigation { margin-top: 16px; padding: 12px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); }
      button:focus-visible { outline: 3px solid var(--focus); }
      [data-state-panel][hidden] { display: none; }
      @media (max-width: 360px) { body { padding: 10px; } }
      @media (prefers-reduced-motion: reduce) {
        * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
      }
    </style>
  </head>
  <body>
    <main class="layout">
      <section class="header">
        <h1>モバイルドロワー</h1>
        <button type="button" id="toggle" aria-controls="mobileDrawer" aria-expanded="false">メニューを開く</button>
      </section>
      <div id="content-overview" data-state-panel="closed">
        <p>閉じた状態。ページはそのまま使えます。</p>
      </div>
      <div id="content-usage" data-state-panel="open" hidden>
        <p>開いた状態。移動候補を一覧表示します。</p>
      </div>
      <div id="content-settings" data-state-panel="selected" hidden>
        <p>項目を選択しました。メニューを閉じて本文に復帰します。</p>
      </div>
      <div class="status" role="status" data-status></div>
    </main>

    <noscript>
      <nav class="layout no-script-navigation" aria-label="JavaScriptなしのメニュー">
        <p>JavaScriptを使わずに、各項目の概要を確認できます。</p>
        <ul>
          <li><a href="#fallback-content-overview">概要</a></li>
          <li><a href="#fallback-content-usage">利用方法</a></li>
          <li><a href="#fallback-content-settings">設定</a></li>
        </ul>
        <section id="fallback-content-overview">
          <h2>概要</h2>
          <p>ページ全体の案内を確認します。</p>
        </section>
        <section id="fallback-content-usage">
          <h2>利用方法</h2>
          <p>項目一覧から必要な案内へ移動します。</p>
        </section>
        <section id="fallback-content-settings">
          <h2>設定</h2>
          <p>設定に関する案内を確認します。</p>
        </section>
      </nav>
    </noscript>

    <aside class="drawer" id="mobileDrawer" data-drawer hidden>
      <div class="backdrop" id="backdrop" data-backdrop></div>
      <div class="panel">
        <h2>メニュー</h2>
        <ul style="padding-left: 18px">
          <li><a href="#content-overview">概要</a></li>
          <li><a href="#content-usage">利用方法</a></li>
          <li><a href="#content-settings">設定</a></li>
        </ul>
        <button type="button" data-drawer-close>メニューを閉じる</button>
      </div>
    </aside>

    <script>
      (() => {
        const params = new URLSearchParams(window.location.search);
        const instance = params.get('instance') ?? 'missing-instance';
        const states = new Set(['closed', 'open', 'selected']);
        const status = document.querySelector('[data-status]');
        const panels = Array.from(document.querySelectorAll('[data-state-panel]'));
        const drawer = document.querySelector('[data-drawer]');
        const toggle = document.getElementById('toggle');
        const backdrop = document.querySelector('[data-backdrop]');
        const closeButton = document.querySelector('[data-drawer-close]');
        const links = Array.from(drawer.querySelectorAll('a'));

        const updateStateDisplay = (nextState, message) => {
          document.documentElement.dataset.state = nextState;
          for (const panel of panels) panel.hidden = panel.dataset.statePanel !== nextState;
          if (status) status.textContent = message;
        };

        const openDrawer = () => {
          drawer.hidden = false;
          document.body.dataset.drawerOpen = 'true';
          toggle.setAttribute('aria-expanded', 'true');
          updateStateDisplay('open', 'メニューを開きました。');
          links[0]?.focus();
        };

        const closeDrawer = ({ selectedLabel = '', restoreFocus = true } = {}) => {
          drawer.hidden = true;
          delete document.body.dataset.drawerOpen;
          toggle.setAttribute('aria-expanded', 'false');
          const nextState = selectedLabel ? 'selected' : 'closed';
          updateStateDisplay(nextState, selectedLabel ? `${selectedLabel}を選択しました。` : 'メニューを閉じました。');
          if (restoreFocus) toggle.focus();
        };

        const setState = (nextState, { restoreFocus = false } = {}) => {
          if (!states.has(nextState)) return;
          if (nextState === 'open') openDrawer();
          else if (nextState === 'selected') closeDrawer({ selectedLabel: '設定', restoreFocus });
          else closeDrawer({ restoreFocus });
        };
        const setTheme = (theme) => {
          if (theme === 'dark' || theme === 'light') document.documentElement.dataset.theme = theme;
        };
        toggle.addEventListener('click', () => {
          if (drawer.hidden) openDrawer();
          else closeDrawer();
        });
        closeButton.addEventListener('click', () => closeDrawer());
        backdrop.addEventListener('click', () => closeDrawer());
        for (const link of links) {
          link.addEventListener('click', (event) => {
            event.preventDefault();
            closeDrawer({ selectedLabel: link.textContent?.trim() ?? '' });
          });
        }
        drawer.addEventListener('keydown', (event) => {
          if (event.key === 'Escape') {
            event.preventDefault();
            closeDrawer();
            return;
          }
          if (event.key !== 'Tab') return;
          const controls = [...links, closeButton];
          const first = controls[0];
          const last = controls.at(-1);
          if (event.shiftKey && document.activeElement === first) {
            event.preventDefault();
            last?.focus();
          } else if (!event.shiftKey && document.activeElement === last) {
            event.preventDefault();
            first?.focus();
          }
        });
        window.addEventListener('message', (event) => {
          if (event.source !== window.parent || typeof event.data !== 'object' || event.data === null) return;
          if (event.data.source !== 'wpl-host' || event.data.instance !== instance) return;
          if (event.data.type === 'wpl-preview-state') setState(event.data.value);
          if (event.data.type === 'wpl-preview-theme') setTheme(event.data.value);
        });
        setTheme(params.get('theme') === 'dark' ? 'dark' : 'light');
        setState(states.has(params.get('state')) ? (params.get('state') ?? 'closed') : 'closed', { restoreFocus: false });
        window.parent.postMessage({ source: 'wpl-preview', type: 'wpl-preview-ready', instance }, '*');
      })();
    </script>
  </body>
</html>
プレビューで試せる動き
  • 閉じた状態: メニュー非表示で本文のみ提示する
  • 開いた状態: 本文を固定しメニュー項目を表示する
  • 選択済み: 項目選択後にメニューが戻る状態

使い分けを確認する

このレシピが合う場面

適する場面

  • 画面幅が狭い際に主要導線をまとめる
  • モバイル時の背景スクロール抑止を確認する
  • メニュー項目選択後の状態復帰を検証する

適さない場面

  • 常に複数ペインが必要なデスクトップ主体画面
  • ドラッグ式サイドメニューが必要な高度な操作