会員ログイン
テーマ開発

WordPressで長い目次とハンバーガーメニューをスクロールさせる方法

WordPressの追従目次とハンバーガーメニューを内部スクロールさせる実装を示した図
はじめに
概要

この記事では、WordPressのオリジナルテーマで、長い目次を右サイドバー内だけスクロールさせる方法と、スマートフォンのハンバーガーメニューを画面内でスクロールできるようにする方法を解説します。WebCantaで実際に採用している構成をもとに、PHP、CSS、JavaScriptの役割を分けながら実装します。

※本記事の情報は2026年8月時点のものです。テーマ構成や対象ブラウザに合わせて、クラス名、ヘッダー高、ブレイクポイントを調整してください。
疑問を持つ読者のイラスト読者

右側の目次が長くなっても、ページからはみ出さずにスクロールできるのは、どんな仕組みですか?

著者・木村のプロフィール写真木村

position: stickyで目次の外枠を追従させ、内側のnavへ高さの上限とoverflow-y: autoを指定しています。ページ全体と目次を別々のスクロール領域にするのがポイントです。

話を聞く読者のイラスト読者

スマホのハンバーガーメニューも、項目が多いときは同じ考え方でよいのでしょうか?

著者・木村のプロフィール写真木村

基本は同じです。メニューの高さを100dvhからヘッダー分だけ引き、メニュー内をスクロール可能にします。開いている間は本文側のスクロールを止めます。

疑問を持つ読者のイラスト読者

CSSだけでなく、現在地の表示やメニューの開閉も一緒に理解したいです。

著者・木村のプロフィール写真木村

この記事では、HTMLを出力するPHP、見た目とスクロール領域を作るCSS、状態を切り替えるJavaScriptの順に確認します。最後にアクセシビリティとつまずきやすい点も整理します。

結論:スクロールする領域を分けて設計する

結論からいうと、長い目次とハンバーガーメニューは、どちらも「外側で配置を決め、内側だけをスクロールさせる」と実装しやすくなります。ページ全体のスクロールへ任せるのではなく、表示できる高さを先に決め、その範囲を超えた内容だけにスクロールバーを付けます。

ページ全体と内部スクロールの役割

PCの右側目次では、外枠をstickyで追従させ、内側のnavoverflow-y: autoを指定します。スマホメニューでは、メニュー自体へ画面内に収まる高さとoverflow: autoを指定し、本文のbodyは一時的に固定します。

実装で押さえる4つのポイント

  • 追従目次はposition: stickytopで表示位置を固定する
  • 内部スクロールには高さの上限とoverflow-y: autoをセットで指定する
  • スマホメニューは100dvhを基準にし、開いている間は背面を固定する
  • JavaScriptで現在地、開閉状態、aria-expandedを同期する

WordPressテーマで使うHTML構造

最初に、CSSとJavaScriptが操作する要素をテーマテンプレートから出力します。目次はsidebar.php、ハンバーガーメニューはheader.phpへ置くと役割を分けやすくなります。

sidebar.phpへ追従目次の枠を置く

目次のリストはJavaScriptで生成するため、PHP側には外枠、見出し、ナビゲーション、空のリストを用意します。hiddenを付けておけば、見出しがない記事やJavaScriptが動作しない途中状態で空の枠が表示されません。

<?php
// 投稿ページの右側へ、JavaScriptで生成する追従目次の枠を表示する。
if ( is_singular( 'post' ) ) :
?>
  <section class="sidebar__section article-toc"
    data-article-toc hidden>
    <h2>目次</h2>
    <nav aria-label="追従目次">
      <ol class="article-toc__list"
        data-article-toc-list></ol>
    </nav>
  </section>
<?php endif; ?>

header.phpへ開閉ボタンとメニューを置く

ボタンのaria-controlsとナビゲーションのidを一致させます。視覚的な3本線とは別に、読み上げ用のテキストを用意すると、開くボタンなのか閉じるボタンなのかを伝えられます。

<?php
// スマートフォンの主要カテゴリーを開閉するボタンとナビゲーションを表示する。
?>
<button class="menu-toggle" type="button"
  aria-expanded="false"
  aria-controls="site-navigation">
  <span></span><span></span><span></span>
  <span class="screen-reader-text">メニューを開く</span>
</button>

<nav id="site-navigation" class="site-nav"
  aria-label="主要カテゴリー">
  <?php
  wp_nav_menu(
    array(
      'theme_location' => 'primary',
      'container'      => false,
    )
  );
  ?>
</nav>

右側の追従目次をCSSでスクロールさせる

右側目次では、外枠と内側ナビゲーションへ別々の役割を与えます。外枠は追従位置と全体の上限、内側は実際のスクロールを担当します。

stickyで目次の外枠を追従させる

topには固定・追従ヘッダーの高さより少し大きい値を指定します。WebCantaではヘッダーとの間に余白を持たせるため、10.8remを使用しています。

max-heightとoverflow-yをセットで指定する

overflow-y: autoだけでは、要素の高さが内容に合わせて伸び続ける場合があります。スクロールさせる要素には、必ず高さまたは高さの上限も指定します。

/* 投稿ページの右側目次を追従させ、長い項目だけを内部スクロールさせる。 */
.article-toc {
  position: sticky;
  top: 10.8rem;
  max-height: calc(100vh - 12.8rem);
  padding: 2.2rem;
  overflow: hidden;
}

.article-toc nav {
  max-height: calc(100vh - 22rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.article-toc__link.is-current {
  color: #0874c9;
  background: #edf6ff;
}

overscroll-behaviorで連続スクロールを抑える

overscroll-behavior: containは、目次の端までスクロールしたときに、同じ操作がすぐページ全体へ伝わるのを抑えます。マウスホイールやタッチ操作で、目次を読んでいる途中に本文が大きく動く感覚を減らせます。

JavaScriptで目次を生成し現在地を示す

目次は記事本文のh2h3から生成できます。本文上部とサイドバーに同じdata-article-tocを用意しておけば、1回の処理で両方へ同じ項目を出力できます。

h2とh3からリンクを作る

見出しIDはリンク先になるため、重複させないことが重要です。記事側でttl-1ttl-1-1のように付けておくと、目次と本文の対応を確認しやすくなります。

is-currentとaria-currentを同期する

現在読んでいる見出しに対応するリンクへis-currentを付けると、背景色や文字色で現在地を示せます。同時にaria-current="location"を付けると、支援技術にも現在位置を伝えられます。

(() => {
  'use strict';

  // 投稿本文の見出しから目次を作り、スクロール位置に応じて現在地を更新する。
  const content = document.querySelector('.entry-content');
  const toc = document.querySelector('[data-article-toc]');
  const list = toc?.querySelector('[data-article-toc-list]');
  if (!content || !toc || !list) return;

  const headings = [...content.querySelectorAll('h2, h3')];
  headings.forEach((heading) => {
    const item = document.createElement('li');
    const link = document.createElement('a');
    link.href = `#${heading.id}`;
    link.textContent = heading.textContent.trim();
    link.dataset.tocTarget = heading.id;
    link.className = 'article-toc__link';
    item.append(link);
    list.append(item);
  });
  toc.hidden = false;

  const links = [...list.querySelectorAll('[data-toc-target]')];
  let ticking = false;
  const updateCurrent = () => {
    const offset = 120;
    let current = headings[0];
    headings.forEach((heading) => {
      if (heading.getBoundingClientRect().top <= offset) current = heading;
    });

    links.forEach((link) => {
      const active = link.dataset.tocTarget === current.id;
      link.classList.toggle('is-current', active);
      active
        ? link.setAttribute('aria-current', 'location')
        : link.removeAttribute('aria-current');
    });
    ticking = false;
  };

  window.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(updateCurrent);
  }, { passive: true });
  updateCurrent();
})();

requestAnimationFrameで更新回数を抑える

scrollイベントは短時間に何度も発生します。毎回すぐに全見出しを確認するのではなく、requestAnimationFrame()で画面描画のタイミングへまとめると、不要な処理を増やしにくくなります。

ハンバーガーメニューを画面内でスクロールさせる

スマートフォンでは、ヘッダーの下から画面下端までをメニュー領域にします。メニュー項目がその高さを超えた場合だけ、メニュー内にスクロールが発生します。

ハンバーガーアイコンの基本的な開閉処理から確認したい場合は、先にハンバーガーメニューを実装します。【サンプルコード付き】もご覧ください。本記事では、その基本処理へ内部スクロール、背面固定、状態のリセットを加えます。

100dvhからヘッダー高を引く

100dvhは、スマートフォンのアドレスバーなどで変化する表示領域を考慮した単位です。100vhより実際の見えている高さへ合わせやすいため、全画面メニューに向いています。

メニュー内だけoverflowを有効にする

/* スマートフォンの主要メニューを画面内に収め、項目が多い場合だけスクロールさせる。 */
body.menu-open {
  overflow: hidden;
}

.site-nav {
  position: absolute;
  top: 100%;
  right: 0;
  left: 0;
  height: calc(100dvh - var(--site-header-height));
  padding: 2.4rem 2rem;
  overflow: auto;
  background: #fff;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.8rem);
  transition: 0.2s ease;
}

.site-nav.is-open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

@media (min-width: 1181px) {
  .site-nav {
    position: static;
    height: auto;
    overflow: visible;
    opacity: 1;
    visibility: visible;
    transform: none;
  }
}

開いている間は背面スクロールを止める

メニュー内を動かしているつもりでも本文が動くと、閉じたときに読んでいた位置が変わります。開いている間だけbodymenu-openを付け、背面を固定します。

(() => {
  'use strict';

  // スマートフォンの主要メニューを開閉し、背面固定と読み上げ状態を同期する。
  const button = document.querySelector('.menu-toggle');
  const navigation = document.querySelector('.site-nav');
  const label = button?.querySelector('.screen-reader-text');
  if (!button || !navigation) return;

  const closeMenu = () => {
    button.setAttribute('aria-expanded', 'false');
    navigation.classList.remove('is-open');
    document.body.classList.remove('menu-open');
    if (label) label.textContent = 'メニューを開く';
  };

  button.addEventListener('click', () => {
    const open = button.getAttribute('aria-expanded') !== 'true';
    button.setAttribute('aria-expanded', String(open));
    navigation.classList.toggle('is-open', open);
    document.body.classList.toggle('menu-open', open);
    if (label) label.textContent = open
      ? 'メニューを閉じる'
      : 'メニューを開く';
  });

  navigation.addEventListener('click', (event) => {
    if (event.target.closest('a')) closeMenu();
  });

  window.addEventListener('resize', () => {
    if (window.innerWidth > 1180) closeMenu();
  });
})();

操作しやすさとアクセシビリティを整える

表示できるだけでなく、キーボードや読み上げ環境でも状態が分かるようにします。見た目のクラスとARIA属性を同じ処理で更新すると、状態のずれを防ぎやすくなります。

aria-expandedと読み上げ文言を更新する

閉じているときはaria-expanded="false"と「メニューを開く」、開いているときはtrueと「メニューを閉じる」にします。3本線と×印だけに頼らず、ボタンの目的を文字でも伝えることが大切です。

リンク選択とPC幅への変更で閉じる

メニュー内リンクを選んだ後もメニューが残ると、移動先の内容が見えません。また、開いたまま画面幅をPCサイズへ広げるとbody.menu-openだけが残る場合があります。リンク選択時とブレイクポイント通過時に共通のcloseMenu()を呼び出します。

うまくスクロールしないときの確認方法

スクロールが発生しない場合は、JavaScriptを増やす前にCSSの高さと親要素の状態を確認します。内部スクロールの問題は、対象要素ではなく祖先要素の指定が原因になることもあります。

stickyが効かない場合

stickyの祖先に意図しないoverflow: hiddenoverflow: autoがあると、その祖先が基準になります。サイドバーの親要素まで開発者ツールで確認し、どの要素がスクロールコンテナになっているかを調べてください。

overflowを付けてもスクロールしない場合

対象要素にheightmax-heightblock-sizeのいずれもない場合は、内容に合わせて伸び続けます。開発者ツールでscrollHeightclientHeightを超えているかを確認すると、スクロール可能な状態か判断できます。

スマホで本文まで動く場合

メニューを開いたときにbody.menu-openが付いているか、閉じたときに外れているかを確認します。iOSや長いページで挙動が安定しない場合は、開いた瞬間のスクロール位置を保存してbodyを固定する方法もありますが、まずは今回の最小構成で必要性を判断してください。

まとめ

  • 追従目次は、外枠のstickyと内側ナビゲーションのoverflow-y: autoを分けて指定する
  • 内部スクロールには、max-heightなど表示できる高さの上限が必要
  • ハンバーガーメニューは100dvhからヘッダー高を引き、メニュー内だけをスクロールさせる
  • メニュー表示中は本文を固定し、リンク選択時とPC幅へ戻ったときに状態を解除する
  • 現在地のis-currentaria-current、開閉のaria-expandedをJavaScriptで同期する

MEMBER COMMENTS

コメント(0件)

会員ログイン後にコメントできます

記事への質問や感想をコメントで共有できます。無料会員登録後もこの記事へ戻れます。