ナビゲーション

縦セクションナビゲーション

左側ナビに見出し、右側本文を紐づけて、スクロール位置と選択状態を確認するレシピです。

動きを試す

プレビュー

プレビュー設定

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

必要なファイルを選ぶ

コード

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;
        --text: #18212f;
        --muted: #5d6b7a;
        --border: #d7dee8;
        --accent: #245fd6;
        --focus: #0077cc;
      }
      :root[data-theme="dark"] {
        --bg: #0f141a;
        --surface: #171d26;
        --text: #f2f5f8;
        --muted: #aab4c0;
        --border: #354152;
        --accent: #7ea7ff;
        --focus: #65c7ff;
      }
      * { box-sizing: border-box; }
      body {
        margin: 0;
        padding: 18px;
        background: var(--bg);
        color: var(--text);
        font-family: Arial, sans-serif;
      }
      .shell {
        display: grid;
        grid-template-columns: 220px 1fr;
        gap: 12px;
        max-width: 760px;
        margin: 0 auto;
      }
      .nav, .content {
        padding: 12px;
        border: 1px solid var(--border);
        background: var(--surface);
        border-radius: 10px;
      }
      .nav a {
        display: block;
        width: 100%;
        min-height: 44px;
        margin-bottom: 6px;
        padding: 10px 12px;
        border: 1px solid var(--border);
        border-radius: 8px;
        background: var(--surface);
        color: var(--text);
        text-decoration: none;
      }
      .nav a[aria-current="location"] { border-color: var(--accent); font-weight: 700; }
      .section { padding: 10px 0; border-bottom: 1px solid var(--border); }
      .section:last-of-type { border-bottom: 0; }
      .section[data-selected="true"] { border-inline-start: 4px solid var(--accent); padding-inline-start: 10px; }
      .status { min-height: 1.2em; margin: 0; }
      a:focus-visible,
      .section:focus-visible {
        outline: 3px solid var(--focus);
      }
      @media (max-width: 720px) {
        .shell {
          grid-template-columns: 1fr;
        }
      }
      @media (max-width: 360px) {
        body { padding: 10px; }
      }
      @media (prefers-reduced-motion: reduce) {
        * { transition: none !important; }
      }
    </style>
  </head>
  <body>
    <main class="shell">
      <nav class="nav" aria-label="本文セクション">
        <a href="#section-home" data-section-link="focus-home">ホーム</a>
        <a href="#section-usage" data-section-link="all-open">使い方</a>
        <a href="#section-policy" data-section-link="focus-policy">ポリシー</a>
      </nav>
      <section class="content">
        <h1>縦型セクションナビゲーション</h1>
        <section class="section" id="section-home" tabindex="-1">
          <h2>ホーム</h2>
          <p>全体説明と主要な導線を確認します。</p>
        </section>
        <section class="section" id="section-usage" tabindex="-1">
          <h2>使い方</h2>
          <p>画面操作の流れを確認します。</p>
        </section>
        <section class="section" id="section-policy" tabindex="-1">
          <h2>ポリシー</h2>
          <p>利用時の運用条件を確認します。</p>
        </section>
        <p class="status" role="status" data-status></p>
      </section>
    </main>

    <script>
      (() => {
        const params = new URLSearchParams(window.location.search);
        const instance = params.get('instance') ?? 'missing-instance';
        const status = document.querySelector('[data-status]');
        const links = Array.from(document.querySelectorAll('[data-section-link]'));
        const sections = Array.from(document.querySelectorAll('.section'));
        const validStates = new Set(['all-open', 'focus-home', 'focus-policy']);
        const setState = (nextState) => {
          if (!validStates.has(nextState)) return;
          document.documentElement.dataset.state = nextState;
          const activeLink = links.find((link) => link.dataset.sectionLink === nextState);
          for (const link of links) {
            if (link === activeLink) link.setAttribute('aria-current', 'location');
            else link.removeAttribute('aria-current');
          }
          for (const section of sections) {
            section.dataset.selected = String(section.id === activeLink?.hash.slice(1));
          }
          if (status && activeLink) status.textContent = `${activeLink.textContent?.trim()}を選択しています。`;
        };
        const setTheme = (theme) => {
          if (theme === 'dark' || theme === 'light') document.documentElement.dataset.theme = theme;
        };
        for (const link of links) {
          link.addEventListener('click', () => {
            const nextState = link.dataset.sectionLink ?? 'all-open';
            const target = document.querySelector(link.hash);
            setState(nextState);
            queueMicrotask(() => target?.focus({ preventScroll: true }));
          });
        }
        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(validStates.has(params.get('state')) ? (params.get('state') ?? 'all-open') : 'all-open');
        window.parent.postMessage({ source: 'wpl-preview', type: 'wpl-preview-ready', instance }, '*');
      })();
    </script>
  </body>
</html>
プレビューで試せる動き
  • すべて開示: 全セクションを展開して全体を確認できる状態です。
  • ホーム重点: 他の章を表示したまま、ホームの章を現在位置として強調します。
  • ポリシー重点: ポリシー章へ焦点を寄せる状態。

使い分けを確認する

このレシピが合う場面

適する場面

  • 長文ガイドの章構成を一覧で誘導する
  • 運用規定など段落単位確認画面でスクロール誘導する
  • 章ごとの要約を表示しながら進捗を示す

適さない場面

  • 短いコンテンツしかない1ページ
  • 動的に増減する見出しが頻繁に更新される文書