タブ型ナビ(下線追従)
ナビをタブのように扱い、選択中の項目へ下線インジケーターがスライド移動するヘッダー。現在地が一目で分かるため、セクションの多いLPやアプリ風サイトに合います。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:SaaS LPのタブ型ナビ -->
<div class="htn-frame">
<header class="htn-head">
<a class="htn-logo" href="#" onclick="return false">◇ FlowDesk</a>
<nav class="htn-tabs" id="htnTabs">
<button class="htn-tab is-active" type="button">概要</button>
<button class="htn-tab" type="button">機能</button>
<button class="htn-tab" type="button">料金</button>
<button class="htn-tab" type="button">導入事例</button>
<span class="htn-ink" id="htnInk" aria-hidden="true"></span>
</nav>
<button class="htn-cta" type="button">無料登録</button>
</header>
<div class="htn-stage">
<h1>段取りを、ひとつに。</h1>
<p>選択中のセクションへ下線がスライド移動します。</p>
</div>
</div>
CSS
/* FlowDesk(SaaS):タブ型ナビ(下線追従)の再スキン */
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }
.htn-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #0a0f22; }
.htn-head { display: flex; align-items: center; gap: 24px; height: 62px; padding: 0 24px; background: #111835; border-bottom: 1px solid #20294a; }
.htn-logo { font-size: 18px; font-weight: 800; color: #fff; text-decoration: none; }
.htn-tabs { position: relative; display: flex; gap: 4px; height: 100%; align-items: center; }
.htn-tab { font: inherit; font-size: 13.5px; font-weight: 600; color: #97a0c4; background: none; border: none; cursor: pointer; padding: 8px 14px; border-radius: 8px; transition: color .2s ease; }
.htn-tab:hover { color: #cdd4ee; }
.htn-tab.is-active { color: #fff; }
.htn-ink { position: absolute; bottom: 8px; left: 0; width: 0; height: 3px; border-radius: 3px; background: linear-gradient(90deg, #4f6bff, #22d3ee); transition: left .32s cubic-bezier(.2,.8,.2,1), width .32s cubic-bezier(.2,.8,.2,1); }
.htn-cta { margin-left: auto; font: inherit; font-size: 12.5px; font-weight: 700; color: #0a0f22; background: #4f6bff; border: none; padding: 9px 17px; border-radius: 999px; cursor: pointer; transition: filter .15s ease, transform .15s ease; }
.htn-cta:hover { filter: brightness(1.08); transform: translateY(-1px); }
.htn-stage { display: grid; place-content: center; height: calc(100% - 62px); text-align: center; color: #e6e9f7; padding: 0 24px; }
.htn-stage h1 { margin: 0; font-size: 26px; font-weight: 800; }
.htn-stage p { margin: 12px 0 0; font-size: 13px; line-height: 1.8; color: #97a0c4; }
@media (prefers-reduced-motion: reduce) { .htn-ink, .htn-tab, .htn-cta { transition: none; } }
JavaScript
// (デモと同じフックを流用)下線インジケーターを選択タブへ移動+自動巡回
(() => {
const tabs = document.getElementById('htnTabs');
const ink = document.getElementById('htnInk');
if (!tabs || !ink) return;
const items = [...tabs.querySelectorAll('.htn-tab')];
function moveTo(el) { items.forEach(t => t.classList.toggle('is-active', t === el)); ink.style.left = el.offsetLeft + 'px'; ink.style.width = el.offsetWidth + 'px'; }
const init = items.find(t => t.classList.contains('is-active')) || items[0];
requestAnimationFrame(() => moveTo(init));
let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
items.forEach(t => t.addEventListener('click', () => { auto = false; moveTo(t); }));
if (auto) {
let i = 0, dir = 1;
const tick = () => { if (!auto) return; i += dir; if (i >= items.length - 1) dir = -1; else if (i <= 0) dir = 1; moveTo(items[i]); setTimeout(tick, 1600); };
setTimeout(tick, 1500);
}
})();
実装ガイド
使いどころ
セクションの多いLPやアプリ風サイトで、現在地を明確に示したいヘッダーに。選択タブへ下線インジケーターがスライド移動します。
実装時の注意点
選択タブの offsetLeft / offsetWidth を測り、下線要素の left / width に反映。requestAnimationFrame で初期位置を確定し、自動デモではタブを巡回します。
対応ブラウザ
offsetLeft/Width と transform/transition は全モダンブラウザで安定。フォント読み込み後の幅変化に備え rAF で再計測すると確実です。
よくある失敗
Webフォントの遅延読み込みでタブ幅が変わると下線がずれるため、load 後に再計測を。レスポンシブで折り返すと下線位置が崩れるので、狭幅では別UI(ドロップダウン等)へ切り替えるのが安全です。
応用例
タブ切替で本文(パネル)も切り替える、スクロール連動で現在セクションを自動ハイライト(scrollspy)、下線をピル背景に変えるなどの展開ができます。
コード
HTML
<!-- タブ型ナビ:選択へ下線インジケーターがスライド -->
<div class="htn-frame">
<header class="htn-head">
<a class="htn-logo" href="#" onclick="return false">⬡ Flux</a>
<nav class="htn-tabs" id="htnTabs">
<button class="htn-tab is-active" type="button">ホーム</button>
<button class="htn-tab" type="button">特徴</button>
<button class="htn-tab" type="button">料金</button>
<button class="htn-tab" type="button">FAQ</button>
<span class="htn-ink" id="htnInk" aria-hidden="true"></span>
</nav>
<button class="htn-cta" type="button">登録</button>
</header>
<div class="htn-stage">
<h1>タブ型ナビ(下線追従)</h1>
<p>選択中の項目へ下線がスライド移動します。<br>(自動でも切り替えを実演します)</p>
</div>
</div>
CSS
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }
.htn-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #0f1320; }
.htn-head { display: flex; align-items: center; gap: 24px; height: 62px; padding: 0 24px; background: #141a2c; border-bottom: 1px solid #222a42; }
.htn-logo { font-size: 18px; font-weight: 800; color: #fff; text-decoration: none; }
.htn-tabs { position: relative; display: flex; gap: 4px; height: 100%; align-items: center; }
.htn-tab { font: inherit; font-size: 13.5px; font-weight: 600; color: #9aa3c0; background: none; border: none; cursor: pointer; padding: 8px 14px; border-radius: 8px; transition: color .2s ease; }
.htn-tab:hover { color: #cdd4ec; }
.htn-tab.is-active { color: #fff; }
.htn-ink {
position: absolute; bottom: 8px; left: 0; width: 0;
height: 3px; border-radius: 3px;
background: linear-gradient(90deg, #6366f1, #22d3ee);
transition: left .32s cubic-bezier(.2,.8,.2,1), width .32s cubic-bezier(.2,.8,.2,1);
}
.htn-cta { margin-left: auto; font: inherit; font-size: 12.5px; font-weight: 700; color: #0f1320; background: #22d3ee; border: none; padding: 9px 17px; border-radius: 999px; cursor: pointer; transition: filter .15s ease, transform .15s ease; }
.htn-cta:hover { filter: brightness(1.08); transform: translateY(-1px); }
.htn-stage { display: grid; place-content: center; height: calc(100% - 62px); text-align: center; color: #e6e9f7; padding: 0 24px; }
.htn-stage h1 { margin: 0; font-size: 26px; font-weight: 800; }
.htn-stage p { margin: 12px 0 0; font-size: 13px; line-height: 1.8; color: #9aa3c0; }
@media (prefers-reduced-motion: reduce) { .htn-ink, .htn-tab, .htn-cta { transition: none; } }
JavaScript
// 選択タブの位置に下線インジケーターを移動。プレビューでは自動で巡回
(() => {
const tabs = document.getElementById('htnTabs');
const ink = document.getElementById('htnInk');
if (!tabs || !ink) return;
const items = [...tabs.querySelectorAll('.htn-tab')];
function moveTo(el) {
items.forEach(t => t.classList.toggle('is-active', t === el));
ink.style.left = el.offsetLeft + 'px';
ink.style.width = el.offsetWidth + 'px';
}
// 初期位置
const init = items.find(t => t.classList.contains('is-active')) || items[0];
requestAnimationFrame(() => moveTo(init));
let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
items.forEach(t => t.addEventListener('click', () => { auto = false; moveTo(t); }));
if (auto) {
let i = 0, dir = 1;
const tick = () => {
if (!auto) return;
i += dir;
if (i >= items.length - 1) dir = -1; else if (i <= 0) dir = 1;
moveTo(items[i]);
setTimeout(tick, 1600);
};
setTimeout(tick, 1500);
}
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「タブ型ナビ(下線追従)」の効果を追加してください。
# 追加してほしい効果
タブ型ナビ(下線追従)(ヘッダー)
ナビをタブのように扱い、選択中の項目へ下線インジケーターがスライド移動するヘッダー。現在地が一目で分かるため、セクションの多いLPやアプリ風サイトに合います。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- タブ型ナビ:選択へ下線インジケーターがスライド -->
<div class="htn-frame">
<header class="htn-head">
<a class="htn-logo" href="#" onclick="return false">⬡ Flux</a>
<nav class="htn-tabs" id="htnTabs">
<button class="htn-tab is-active" type="button">ホーム</button>
<button class="htn-tab" type="button">特徴</button>
<button class="htn-tab" type="button">料金</button>
<button class="htn-tab" type="button">FAQ</button>
<span class="htn-ink" id="htnInk" aria-hidden="true"></span>
</nav>
<button class="htn-cta" type="button">登録</button>
</header>
<div class="htn-stage">
<h1>タブ型ナビ(下線追従)</h1>
<p>選択中の項目へ下線がスライド移動します。<br>(自動でも切り替えを実演します)</p>
</div>
</div>
【CSS】
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }
.htn-frame { position: relative; width: 100%; height: 380px; overflow: hidden; background: #0f1320; }
.htn-head { display: flex; align-items: center; gap: 24px; height: 62px; padding: 0 24px; background: #141a2c; border-bottom: 1px solid #222a42; }
.htn-logo { font-size: 18px; font-weight: 800; color: #fff; text-decoration: none; }
.htn-tabs { position: relative; display: flex; gap: 4px; height: 100%; align-items: center; }
.htn-tab { font: inherit; font-size: 13.5px; font-weight: 600; color: #9aa3c0; background: none; border: none; cursor: pointer; padding: 8px 14px; border-radius: 8px; transition: color .2s ease; }
.htn-tab:hover { color: #cdd4ec; }
.htn-tab.is-active { color: #fff; }
.htn-ink {
position: absolute; bottom: 8px; left: 0; width: 0;
height: 3px; border-radius: 3px;
background: linear-gradient(90deg, #6366f1, #22d3ee);
transition: left .32s cubic-bezier(.2,.8,.2,1), width .32s cubic-bezier(.2,.8,.2,1);
}
.htn-cta { margin-left: auto; font: inherit; font-size: 12.5px; font-weight: 700; color: #0f1320; background: #22d3ee; border: none; padding: 9px 17px; border-radius: 999px; cursor: pointer; transition: filter .15s ease, transform .15s ease; }
.htn-cta:hover { filter: brightness(1.08); transform: translateY(-1px); }
.htn-stage { display: grid; place-content: center; height: calc(100% - 62px); text-align: center; color: #e6e9f7; padding: 0 24px; }
.htn-stage h1 { margin: 0; font-size: 26px; font-weight: 800; }
.htn-stage p { margin: 12px 0 0; font-size: 13px; line-height: 1.8; color: #9aa3c0; }
@media (prefers-reduced-motion: reduce) { .htn-ink, .htn-tab, .htn-cta { transition: none; } }
【JavaScript】
// 選択タブの位置に下線インジケーターを移動。プレビューでは自動で巡回
(() => {
const tabs = document.getElementById('htnTabs');
const ink = document.getElementById('htnInk');
if (!tabs || !ink) return;
const items = [...tabs.querySelectorAll('.htn-tab')];
function moveTo(el) {
items.forEach(t => t.classList.toggle('is-active', t === el));
ink.style.left = el.offsetLeft + 'px';
ink.style.width = el.offsetWidth + 'px';
}
// 初期位置
const init = items.find(t => t.classList.contains('is-active')) || items[0];
requestAnimationFrame(() => moveTo(init));
let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
items.forEach(t => t.addEventListener('click', () => { auto = false; moveTo(t); }));
if (auto) {
let i = 0, dir = 1;
const tick = () => {
if (!auto) return;
i += dir;
if (i >= items.length - 1) dir = -1; else if (i <= 0) dir = 1;
moveTo(items[i]);
setTimeout(tick, 1600);
};
setTimeout(tick, 1500);
}
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。