コピペで完結!タブ切替 #14【スクロールで自動タブ切替できるタブ】
html/css/js
2025.10.07
スクロールで自動タブ切替できるタブ
ページをスクロールすると、表示中のセクションに応じてタブが自動で切り替わるナビゲーションです。
ヘッダー内に配置したタブバーは常に固定表示され、クリックで該当セクションへスムーズスクロール。
読み進めるだけで現在地がハイライトされるので、メガメニューや長文ページに最適です。
- Stickyタブ:ヘッダー内で固定され、常に操作可能
- 自動連動:スクロール位置に応じてタブが切替わる
- 双方向ナビ:クリックでジャンプ/スクロールで自動同期
- アクセシビリティ対応:ARIA属性を正しく付与
- 依存ライブラリなし:CSS/HTML/JSのみで完結
コードについて
本記事のコードは AI(ChatGPT)による生成をベースに作成・調整しています。ご利用の環境でテストの上ご使用ください。
免責
本コードの利用に伴う不具合・損害について、当サイトは責任を負いません。自己責任にてご利用ください。
デモ
概要
Stickyタブの概要説明。常に画面上部に残り、どの位置でもナビゲーション可能です。
仕様
- IntersectionObserverで表示中セクションを検出(センター判定)
- タブクリックで該当セクションへスムーズスクロール
- ARIA属性(
aria-selected
)更新でアクセシブル
FAQ
よくある質問を記載。ライブラリは不要です。
コードをコピーして使おう!
/* =========================================
固定(Sticky)タブ+「見出しが画面の上端に来たら」同期:CSS
-----------------------------------------
・監視対象は .panel 内の h3(見出し)
・h3 が Stickyタブ直下の帯に触れたら該当タブをアクティブに
・h3 に scroll-margin-top を付与してアンカーで隠れないように
========================================= */
/* =========================================
ルートエリアのスタイル
-----------------------------------------
・カラートークンを定義(色や高さはJSからも利用)
・全体のレイアウトやフォントを設定
========================================= */
#sticky-section{
--ink:#0f172a; /* 文字色(本文の基本) */
--muted:#64748b; /* 補助文字色(説明やサブテキスト用) */
--accent:#0b6bff; /* 強調色(アクティブタブや強調箇所に使用) */
--border:#e5e7eb; /* 枠線色(ボーダー用) */
--panel:#ffffff; /* パネルの背景色 */
--sticky-bg:#f8fafc; /* Sticky部分の下地色(タブバー背景) */
--sticky-h:52px; /* ★固定タブの高さ(JSでも参照される値) */
color:var(--ink); /* 全体の文字色を指定 */
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans JP","Yu Gothic UI",sans-serif;
/* システムに依存しない読みやすいフォント */
max-width:960px; /* 横幅を最大960pxに制限(読みやすさ重視) */
margin:0 auto; /* セクションを中央寄せ */
padding:0 16px 64px; /* 左右16px/下64pxの余白を確保 */
box-sizing:border-box; /* パディングやボーダーを幅に含める */
scroll-behavior:smooth; /* アンカー移動時にスムーズスクロール */
}
/* =========================================
Stickyタブバー(常に上部に固定される)
========================================= */
#sticky-section .sticky-tabs{
position:sticky; /* スクロールしても上部に貼り付く */
top:0; /* ページ最上部に固定 */
z-index:20; /* 他の要素より前面に表示 */
background:var(--sticky-bg); /* 背景色(下のコンテンツが透けないように) */
border-bottom:1px solid var(--border); /* 下に仕切り線 */
}
/* =========================================
タブリスト(タブの並び)
========================================= */
#sticky-section .tabs{
display:flex; /* 横並び配置 */
flex-wrap:wrap; /* 幅が足りないとき折り返し */
gap:8px; /* タブ同士の隙間 */
padding:8px 0; /* 上下の余白 */
min-height:var(--sticky-h); /* ★タブバーの高さを明示(JS基準にもなる) */
align-content:center; /* 複数行のとき中央揃え */
}
/* =========================================
タブボタン(個々のタブ)
========================================= */
#sticky-section .tab{
appearance:none; /* ブラウザデフォルトの装飾を無効化 */
border:1px solid var(--border); /* 通常時は薄い枠線 */
border-radius:999px; /* 丸みを最大にしてピル型に */
background:#fff; /* 背景は白 */
color:var(--ink); /* 通常文字色 */
font-size:13px; /* 小さめの文字サイズ */
padding:8px 12px; /* 内側余白 */
cursor:pointer; /* カーソルをポインターに */
transition:background .2s, border-color .2s, color .2s;
/* ホバーやアクティブ時の色変化を滑らかに */
font-weight:500; /* やや太字 */
}
/* アクティブなタブの見た目 */
#sticky-section .tab[aria-selected="true"]{
background:var(--accent); /* 強調色の背景 */
border-color:var(--accent); /* 枠線も強調色に */
color:#fff; /* 文字を白に */
font-weight:600; /* 少し太くして強調 */
}
/* フォーカス時(キーボード操作対応) */
#sticky-section .tab:focus-visible{
outline:2px solid var(--accent); /* アクセント色でアウトライン表示 */
outline-offset:2px; /* 少し外側にずらす */
}
/* =========================================
本文パネル(タブに対応するコンテンツ領域)
========================================= */
#sticky-section .panel{
padding:24px; /* 内側余白を広めに */
border:1px solid var(--border);/* 薄い枠線で囲む */
border-radius:12px; /* 柔らかい角丸 */
background:var(--panel); /* 白背景 */
margin:24px 0 48px; /* 上下の余白(下は広め) */
line-height:1.9; /* 読みやすい行間 */
}
/* パネル内の見出し(監視対象になる要素) */
#sticky-section .panel h3{
margin:0 0 12px; /* 下に余白 */
font-size:18px; /* 少し大きめ */
scroll-margin-top:calc(var(--sticky-h) + 8px);
/* ★アンカー移動時にStickyタブに隠れないよう補正 */
}
/* =========================================
補助テキスト(注釈や説明用)
========================================= */
#sticky-section .muted{
color:var(--muted); /* 補助文字色で控えめに */
font-size:13px; /* 小さめサイズ */
}
<!-- =========================================
固定(Sticky)タブ+見出しが上端に来たら同期:HTML
-----------------------------------------
・タブは data-target で対応セクションの ID を指す
・各セクション先頭の h3 を監視(上端バンドで交差)
========================================= -->
<!-- 下記改行<br>は削除して使用してください。 -->
<br><br><br><br><br><br><br><br><br><br>
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
<section id="sticky-section" aria-label="Stickyタブと本文">
<!-- ▼ Sticky タブナビ -->
<div class="sticky-tabs" id="sticky-tabs">
<div class="tabs" role="tablist" aria-label="記事セクションナビ">
<button class="tab" role="tab" aria-selected="true" data-target="#sec-1">概要</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-2">仕様</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-3">FAQ</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-4">事例</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-5">導入方法</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-6">応用</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-7">注意点</button>
<button class="tab" role="tab" aria-selected="false" data-target="#sec-8">まとめ</button>
</div>
</div>
<!-- ▼ 本文(各パネル。id はタブの data-target と一致) -->
<section id="sec-1" class="panel" role="tabpanel" aria-label="概要">
<h3>概要</h3>
<p class="muted">Sticky タブはスクロール中も上部に固定。見出しが画面の最上部に来たタイミングで該当タブが自動でハイライトされます。</p>
</section>
<section id="sec-2" class="panel" role="tabpanel" aria-label="仕様">
<h3>仕様</h3>
<ul>
<li>IntersectionObserver で「上端バンド」交差を検出</li>
<li>クリック時は scrollIntoView(h3 に scroll-margin-top)</li>
<li>Sticky 高さは CSS カスタムプロパティ(--sticky-h)を参照</li>
</ul>
</section>
<section id="sec-3" class="panel" role="tabpanel" aria-label="FAQ">
<h3>FAQ</h3>
<p>Q. ライブラリ必要?<br>A. いいえ、素の JavaScript で動作します。</p>
</section>
<section id="sec-4" class="panel" role="tabpanel" aria-label="事例">
<h3>事例</h3>
<p>実装事例をいくつか紹介します。</p>
</section>
<section id="sec-5" class="panel" role="tabpanel" aria-label="導入方法">
<h3>導入方法</h3>
<p>設置手順と基本コードを解説します。</p>
</section>
<section id="sec-6" class="panel" role="tabpanel" aria-label="応用">
<h3>応用</h3>
<p>カスタマイズ例や応用パターンを紹介します。</p>
</section>
<section id="sec-7" class="panel" role="tabpanel" aria-label="注意点">
<h3>注意点</h3>
<p>実装時の落とし穴や推奨事項をまとめます。</p>
</section>
<section id="sec-8" class="panel" role="tabpanel" aria-label="まとめ">
<h3>まとめ</h3>
<p>ポイントを振り返ります。</p>
</section>
</section>
<!-- 下記改行<br>は削除して使用してください。 -->
<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
/* =========================================
固定(Sticky)タブ+見出しが上端に来たら同期:JS
-----------------------------------------
・対象は #sticky-section 配下
・クリック:data-target のセクションの h3 位置へスクロール
・スクロール:IntersectionObserver で「見出しが指定帯に入った瞬間」を検出
└ ★ rootMargin の設定値で「どこでアクティブにするか」を決定
・リサイズ時は Observer を作り直して帯位置を再計算
========================================= */
(function initStickyTopBand(){
const root = document.getElementById('sticky-section'); // セクション全体のルート要素
if(!root) return; // 見つからなければ何もしない(保険)
const tabWrap = document.getElementById('sticky-tabs'); // タブ群のラッパー
const tabs = Array.from(tabWrap.querySelectorAll('.tab')); // すべてのタブボタンを取得
/* -----------------------------------------
1. セクションとその中の h3 見出しを収集
----------------------------------------- */
const sections = Array.from(root.querySelectorAll('.panel[id]')); // idを持つパネル群
const headings = sections.map(sec => ({ id: sec.id, h: sec.querySelector('h3') }))
.filter(x => x.h); // h3が存在するものだけ残す
/* -----------------------------------------
2. id と対応タブをひも付ける Map を作成
----------------------------------------- */
const idToTab = new Map();
tabs.forEach(t => {
const sel = t.getAttribute('data-target'); // 例: "#sec-3"
if(sel) idToTab.set(sel.slice(1), t); // "#"を除いたidをキーに登録
});
/* -----------------------------------------
3. クリックイベント:タブを押すと
対応するセクションの h3 にスクロール
----------------------------------------- */
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const sel = tab.getAttribute('data-target'); // data-target="#sec-1"
const target = sel ? document.querySelector(sel + ' h3') : null;
if(target) target.scrollIntoView({ behavior:'smooth', block:'start' });
// h3に scroll-margin-top があるので Stickyに隠れない
});
});
/* -----------------------------------------
4. 現在アクティブなタブ管理
----------------------------------------- */
let current = null; // 現在選択されているタブ
let io = null; // IntersectionObserver の参照
// 指定idのタブをアクティブ化
function activateTabById(id){
const next = idToTab.get(id);
if(!next || next === current) return; // 無効 or 既に同じなら無視
current = next;
tabs.forEach(t => t.setAttribute('aria-selected', String(t === next)));
// aria-selected="true" を付与してCSSで見た目を変える
}
// "12px" → 12 のように数値化するヘルパー
function parsePx(v){
const n = parseFloat(String(v).trim().replace('px',''));
return Number.isFinite(n) ? n : 0;
}
/* -----------------------------------------
5. IntersectionObserver を設定
★ここが「どこでアクティブにするか」を決める核心部分
- rootMargin で「上端から何px下」を監視帯にするかを指定
- entry.isIntersecting=true になった瞬間にタブ切り替え
----------------------------------------- */
function setupObserver(){
// 既存Observerがあれば解除
if(io){ io.disconnect(); io = null; }
// CSS変数 --sticky-h を取得
const cs = getComputedStyle(root);
const stickyH = parsePx(cs.getPropertyValue('--sticky-h')) || 52;
// ★上端バンドのサイズ計算
// → この値を変えれば「上端」「中腹」「中央」など発火位置を変えられる
const topBand = stickyH + 1;
const bottomBand = Math.max(0, window.innerHeight - topBand);
// Observer作成
io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if(!entry.isIntersecting) return; // ★バンドに入っていなければ無視
// ★見出しがバンドに入った瞬間に「対応タブをアクティブ化」
const sec = entry.target.closest('.panel[id]');
if(sec) activateTabById(sec.id);
});
},{
root:null, // ビューポート基準
rootMargin:`-${topBand}px 0px -${bottomBand}px 0px`,
// ★rootMarginが「監視帯(トリガー位置)」を決定
threshold:0
});
// すべてのh3を監視開始
headings.forEach(({h}) => io.observe(h));
}
/* -----------------------------------------
6. 初期化とリサイズ対応
画面サイズや向きが変わったら再計算
----------------------------------------- */
setupObserver();
window.addEventListener('resize', setupObserver);
window.addEventListener('orientationchange', setupObserver);
})();
コピペで完結!タブ切替 #14【スクロールで自動タブ切替できるタブ】
コメント