ナビゲーション

アンカー型目次ナビ

長文を章単位で跳ねる目次の配置と、長い見出しでも崩れない導線を確認するレシピです。

動きを試す

プレビュー

プレビュー設定

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

必要なファイルを選ぶ

コード

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: 16px; background: var(--bg); color: var(--text); font-family: Arial, sans-serif; }
      .layout { max-width: 760px; margin: 0 auto; display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
      .toc, .content { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
      ul { margin: 0; padding-left: 16px; }
      a { color: var(--accent); }
      [data-optional-entry][hidden] { display: none; }
      [aria-current="location"] { font-weight: 700; }
      h2:focus-visible { outline: 3px solid var(--focus); }
      .status { min-height: 1.4em; }
      @media (max-width: 720px) {
        .layout { grid-template-columns: 1fr; }
      }
      @media (max-width: 360px) { body { padding: 10px; } }
      @media (prefers-reduced-motion: reduce) { * { transition-duration: 0.01ms !important; animation: none !important; } }
    </style>
  </head>
  <body>
    <main class="layout">
      <nav class="toc" aria-label="目次">
        <p>目次(簡易)</p>
        <ul>
          <li><a href="#ch1" data-jump="compact">導入</a></li>
          <li><a href="#ch2" data-jump="expanded">運用条件</a></li>
          <li data-optional-entry><a href="#ch3" data-jump="focused">設計ノート</a></li>
        </ul>
      </nav>
      <article class="content">
        <h1>アンカー目次レシピ</h1>
        <section>
          <h2 id="ch1" tabindex="-1">導入</h2>
          <p>簡易目次では主要見出しのみを表示します。</p>
        </section>
        <section>
          <h2 id="ch2" tabindex="-1">運用条件</h2>
          <ul>
            <li>見出し1</li>
            <li>見出し2(長い見出しを折り返せるか確認)</li>
            <li>見出し3(補足ルール)</li>
          </ul>
        </section>
        <section>
          <h2 id="ch3" tabindex="-1">設計ノート</h2>
          <p>現在選択中の章をフォーカス表示します。</p>
        </section>
        <p class="status" data-status role="status"></p>
      </article>
    </main>
    <script>
      (() => {
        const params = new URLSearchParams(window.location.search);
        const instance = params.get('instance') ?? 'missing-instance';
        const links = Array.from(document.querySelectorAll('[data-jump]'));
        const optionalEntries = Array.from(document.querySelectorAll('[data-optional-entry]'));
        const status = document.querySelector('[data-status]');
        const validStates = new Set(['compact', 'expanded', 'focused']);

        const setCurrentLink = (currentLink) => {
          for (const link of links) link.removeAttribute('aria-current');
          currentLink?.setAttribute('aria-current', 'location');
        };

        const setState = (nextState, currentLink = null) => {
          if (!validStates.has(nextState)) return;
          document.documentElement.dataset.state = nextState;
          for (const entry of optionalEntries) entry.hidden = nextState === 'compact';
          setCurrentLink(nextState === 'focused' ? (currentLink ?? links.find((link) => link.dataset.jump === 'focused')) : null);
          if (status) status.textContent = nextState === 'focused' ? '選択した章へ移動しました。' : '目次の表示を更新しました。';
        };

        const setTheme = (theme) => {
          if (theme === 'dark' || theme === 'light') document.documentElement.dataset.theme = theme;
        };

        for (const link of links) {
          link.addEventListener('click', () => {
            const targetId = link.hash.slice(1);
            const target = document.getElementById(targetId);
            if (!target) return;
            setState('focused', link);
            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') ?? 'compact') : 'compact');
        window.parent.postMessage({ source: 'wpl-preview', type: 'wpl-preview-ready', instance }, '*');
      })();
    </script>
  </body>
</html>
プレビューで試せる動き
  • 簡易目次: 主要見出しを2件に絞って表示します。
  • 詳細目次: 3件すべての主要見出しを表示します。
  • 選択中: 現在表示中の見出しへハイライトを移動します。

使い分けを確認する

このレシピが合う場面

適する場面

  • 技術資料・方針文書で目次導線を設計する
  • 長い見出しを含む章移動を確認する
  • 目次から本文位置へ遷移する導線を明確化する

適さない場面

  • ページ内移動が不要な短文ページ
  • リンクの有効性が低いCMS自動生成ページ