目次とは

記事の見出しから自動的に作られ、クリックすると該当の見出しへ移動できるリンク一覧。

概要

長い記事の冒頭やサイドに表示される、見出しの一覧です。手作業で並べることもありますが、 記事内の h2 や h3 などの見出しを JavaScript で拾い集めて自動的に作る実装もよく使われます。 各リンクをクリックすると、対応する見出しの位置までページ内を移動します。スクロールに合わせて、今読んでいる場所のリンクをハイライトすると、さらに使いやすくなります。

別名

表記ゆれ:目次、TOC、ページ内目次、追従目次、table of contents

英語の “Table of Contents” の頭文字を取って「TOC」と呼ばれることが開発者の間ではよくあります。上部や横に張り付いたまま表示する実装は「追従目次」とも呼ばれます。

AI への伝え方

「記事の見出しから目次を自動生成して」で通じます。どの見出しレベル(h2 だけか h3 まで含むか)を対象にするか、 スクロールに合わせてハイライトしてほしいかを伝えると仕様がぶれません。

似たパーツとの違い

「追従サイドバー」は目次に限らず様々な内容をスクロールに合わせて張り付けておく仕組みで、目次はその中身の一例です。 1 本のリンクだけで特定の場所へ移動する仕組みは「アンカーリンク」と呼び、目次はそのアンカーリンクを見出しの数だけまとめて自動生成したものと言えます。

デモ

コード

HTML
<div class="toc-demo">
  <nav class="toc-demo__toc" id="toc-demo-toc" aria-label="目次"></nav>
  <article class="toc-demo__article">
    <h2 id="toc-demo-sec1">はじめに</h2>
    <p>この記事の導入部分です。目次は、下の見出しから自動的に生成されます。</p>
    <p>目次のリンクをクリックすると、対応する見出しまでなめらかにスクロールします。</p>
    <h2 id="toc-demo-sec2">準備するもの</h2>
    <p>ここでは準備するものについて説明します。ダミーの本文が続きます。</p>
    <p>スクロールを続けると、目次の中で今読んでいる場所のリンクがハイライトされます。</p>
    <h2 id="toc-demo-sec3">実装の手順</h2>
    <p>ここでは実装の手順について説明します。ダミーの本文が続きます。</p>
    <p>さらに本文が続きます。見出しごとにセクションを分けています。</p>
    <h2 id="toc-demo-sec4">まとめ</h2>
    <p>ここでまとめを述べます。これで本文は終わりです。</p>
    <p>目次に戻って、他の見出しもクリックしてみてください。</p>
  </article>
</div>
CSS
html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

.toc-demo {
  font-family: sans-serif;
  color: #333333;
}

.toc-demo__toc {
  position: sticky;
  top: 0;
  margin: -24px -24px 20px;
  padding: 14px 24px;
  background: #ffffff;
  border-bottom: 1px solid #e6e6e6;
  z-index: 10;
}

.toc-demo__toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
}

.toc-demo__toc a {
  display: inline-block;
  padding: 4px 2px;
  color: #666666;
  text-decoration: none;
  font-size: 13px;
  border-bottom: 2px solid transparent;
}

.toc-demo__toc a:hover {
  color: #0017c1;
}

.toc-demo__toc a[aria-current="true"] {
  color: #0017c1;
  font-weight: bold;
  border-bottom-color: #0017c1;
}

.toc-demo__article {
  line-height: 1.8;
  min-height: 1400px;
}

.toc-demo__article h2 {
  margin: 32px 0 12px;
  font-size: 17px;
  scroll-margin-top: 64px;
}

.toc-demo__article p {
  margin: 0 0 14px;
}
JS
const toc = document.getElementById("toc-demo-toc");
const headings = document.querySelectorAll(".toc-demo__article h2");

// 見出しから目次のリンク一覧を自動生成する
const list = document.createElement("ul");
headings.forEach((heading) => {
  const li = document.createElement("li");
  const link = document.createElement("a");
  link.href = `#${heading.id}`;
  link.textContent = heading.textContent;
  li.appendChild(link);
  list.appendChild(li);
});
toc.appendChild(list);

const links = toc.querySelectorAll("a");

// 現在表示中の見出しに合わせて、対応するリンクをハイライトする
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      const link = toc.querySelector(`a[href="#${entry.target.id}"]`);
      if (!link) return;
      if (entry.isIntersecting) {
        links.forEach((item) => item.removeAttribute("aria-current"));
        link.setAttribute("aria-current", "true");
      }
    });
  },
  { rootMargin: "-64px 0px -70% 0px" }
);

headings.forEach((heading) => observer.observe(heading));