BLOG
Vanilla JSで追従ナビゲーションを作ってみた!
INDEX
説明
Vanilla JS(ライブラリやフレームワークを使用しない、シンプルなJavaScript)とHTML、CSSのみを使った、追従ナビゲーションを作成します。
追従の挙動に加え、
- MV(メインビジュアル)とフッターに近づいた際は、CSSの
transform
プロパティを使って非表示にする - 画面に表示されているsectionに応じて、追従ナビゲーションのテキストカラーが自動で変わる
といった仕様を追加しようと思います。
今回は、scrollイベントを使った追従ナビゲーション、Intersection Observerを使った追従ナビゲーションの2パターンで作成してみました。
完成版1(scroll イベントでの実装)
完成版2(Intersection Observerでの実装)
実装手順 HTML / CSS
まずはマークアップから準備します。
マークアップはscrollイベントを使用した例、Intersection Observerを使用した例ともに同じものを使用します。
手順1(HTML / ページの枠組作成)
とあるコーポレートサイトのTopページと仮定し、MV、フッターを配置します。section
タグにはそれぞれid
属性を付与します。
<body>
<header class="mv">
<h1>MV</h1>
</header>
<main>
<section class="about" id="about">
<h2>ABOUT</h2>
</section>
<section class="service" id="service">
<h2>SERVICE</h2>
</section>
<section class="topics" id="topics">
<h2>TOPICS</h2>
</section>
<section class="recruit" id="recruit">
<h2>RECRUIT</h2>
</section>
<section class="news" id="news">
<h2>NEWS</h2>
</section>
<section class="contact" id="contact">
<h2>CONTACT</h2>
</section>
<footer id="footer">
</footer>
</main>
</body>
手順2(HTML / 追従ナビゲーション部分作成)
追従ナビゲーションのHTMLを記述します。
追従ナビゲーションの各a
タグには、手順1でsection
タグに付与したid
属性と同じid
名のhref
属性を付与します。
また、初期ロード時にハイライト表示を適用しておきたい追従ナビゲーションのa
タグに、is-current
というclass名を追加しておきます。
<body>
<header class="mv">
<h1>MV</h1>
</header>
<main>
<nav id="page-nav" class="page-nav">
<ul class="page-nav__list">
<li class="page-nav__item">
<a class="page-nav__link is-current" href="#about">
<span class="page-nav__text">ABOUT</span>
</a>
</li>
<li class="page-nav__item">
<a class="page-nav__link" href="#service">
<span class="page-nav__text">SERVICE</span>
</a>
</li>
<li class="page-nav__item">
<a class="page-nav__link" href="#topics">
<span class="page-nav__text">TOPICS</span>
</a>
</li>
<li class="page-nav__item">
<a class="page-nav__link" href="#recruit">
<span class="page-nav__text">RECRUIT</span>
</a>
</li>
<li class="page-nav__item">
<a class="page-nav__link" href="#news">
<span class="page-nav__text">NEWS</span>
</a>
</li>
<li class="page-nav__item">
<a class="page-nav__link" href="#contact">
<span class="page-nav__text">CONTACT</span>
</a>
</li>
</ul>
</nav>
<section class="about" id="about">
<h2>ABOUT</h2>
</section>
<section class="service" id="service">
<h2>SERVICE</h2>
</section>
<section class="topics" id="topics">
<h2>TOPICS</h2>
</section>
<section class="recruit" id="recruit">
<h2>RECRUIT</h2>
</section>
<section class="news" id="news">
<h2>NEWS</h2>
</section>
<section class="contact" id="contact">
<h2>CONTACT</h2>
</section>
<footer id="footer">
</footer>
</main>
</body>
手順3(CSS / スタイル調整)
追従ナビゲーションにposition: fixed;
を付与し、画面上部に固定します。
X軸(横方向の位置)は画面に対し、中央になるよう、transform: translateX(-50%);
を付与します。
また、追従ナビゲーションを隠すためのclassとして、今回は.is-hidden
というclass名を、表示されているセクションに応じてテキストカラーが変化するためのclass名として、.is-current
を用意します。
.page-nav{
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 1080px;
z-index: 100;
margin-inline: auto;
transition-duration: 0.3s;
&__list{
padding: 15px 45px;
margin-inline: auto;
list-style-type: none;
display: flex;
justify-content: space-between;
align-items: center;
gap: 30px;
}
&__link {
color: #000;
text-decoration: none;
}
}
.is-hidden {
opacity: 0;
transform: translateX(-50%) translateY(-50%);
transition-duration: 0.3s;
}
.is-current {
color: red;
position: relative;
}
※SCSS記法を使用しています。
実装手順 JavaScript (scrollイベント編)
手順4(JavaScript / 追従ナビゲーションを隠す処理)
JavaScriptのscrollイベントを使って、追従ナビゲーションを隠す挙動を実装します。
scrollイベントの関数内で、ページナビゲーション、フッター(追従ナビゲーションを隠すトリガー)、id
名about
がついたsection
タグ(追従ナビゲーションを表示させるトリガー)をローカル変数として宣言します。
また、if (!pageNav || !footer || !firstSection || navLinks.length === 0) return;
で、いずれかの要素がページ上に存在しない場合は処理自体を実行しないようにしておきます。
document.addEventListener("DOMContentLoaded", () => {
const pageNav = document.getElementById('page-nav');
const footer = document.getElementById('footer');
const firstSection = document.getElementById('about');
const navLinks = document.querySelectorAll(".page-nav__link");
// 対象の要素が存在しない場合は処理をスキップ
if (!pageNav || !footer || !firstSection || navLinks.length === 0) return;
});
updateNavVisibility
関数内で、フッターからの距離が500px
、または、最初のsection
からの距離が0px
の場合、追従ナビゲーションに対して、CSSのclass名の.is-hidden
をclassList.toggle
で付け替える処理を行います。
// スクロール量
let lastScrollY = window.scrollY;
// スクロールイベントの連続発火を防ぐためのフラグ
let isTicking = false;
// 追従ナビゲーションの表示/非表示を切り替える関数
const updateNavVisibility = () => {
const footerTop = footer.getBoundingClientRect().top;
const firstSectionTop = firstSection.getBoundingClientRect().top;
const isInview = footerTop <= 500 || firstSectionTop > 0;
pageNav.classList.toggle('is-hidden', isInview);
}
const onScroll = () => {
if (isTicking) return;
lastScrollY = window.scrollY;
isTicking = true;
// 1フレーム待ってからinviewAnimationを実行
requestAnimationFrame(() => {
updateNavVisibility();
updateCurrentSection();
isTicking = false;
});
}
// スクロールイベントの登録
window.addEventListener('scroll', onScroll);
これで、scrollに応じて特定のトリガー要素が画面上に表示された場合に、追従ナビゲーションの表示/非表示を切り替えることができます。
手順5(JavaScript / 表示位置に合わせたハイライトを追加)
scrollイベントの関数内で、全てのa
タグから、class名is-current
を削除する記述と、ハイライト表示のトリガーとなるsection
要素を取得します。
const updateCurrentSection = () => {
navLinks.forEach((link) => {
link.classList.remove('is-current')
const sectionId = link.getAttribute('href')
const section = document.querySelector(sectionId)
})
}
次に、scrollイベント内の関数に、画面に表示されているsection
を判定する処理を記述します。
こちらでは、追従ナビゲーションのaタグのhref
属性、section
タグのid
属性をローカル変数として宣言します。
現在のスクロール位置がそのsection
内にある場合、if文を使って対応するa
タグにclass名is-current
を追加してハイライトを適用します。
const updateCurrentSection = () => {
navLinks.forEach((link) => {
link.classList.remove('is-current')
const sectionId = link.getAttribute('href')
const section = document.querySelector(sectionId)
if (section) {
const sectionTop = section.offsetTop - 240
const sectionBottom = sectionTop + section.offsetHeight
if (lastScrollY >= sectionTop && lastScrollY <= sectionBottom) {
link.classList.add('is-current')
}
}
})
}
updateCurrentSection()
追従ナビゲーションを隠す処理と、現在位置のハイライトの付け替えを行う処理をまとめたJavaScriptは下記になります。
document.addEventListener("DOMContentLoaded", () => {
const pageNav = document.getElementById('page-nav');
const footer = document.getElementById('footer');
const firstSection = document.getElementById('about');
const navLinks = document.querySelectorAll(".page-nav__link");
if (!pageNav || !footer || !firstSection || navLinks.length === 0) return;
let lastScrollY = window.scrollY;
let isTicking = false;
const updateNavVisibility = () => {
const footerTop = footer.getBoundingClientRect().top;
const firstSectionTop = firstSection.getBoundingClientRect().top;
const isInview = footerTop <= 500 || firstSectionTop > 0;
pageNav.classList.toggle('is-hidden', isInview);
}
const updateCurrentSection = () => {
navLinks.forEach(link => {
link.classList.remove("is-current");
const sectionId = link.getAttribute("href");
const section = document.querySelector(sectionId);
if (section) {
const sectionTop = section.offsetTop - 240;
const sectionBottom = sectionTop + section.offsetHeight;
if (lastScrollY >= sectionTop && lastScrollY <= sectionBottom) {
link.classList.add("is-current");
}
}
});
}
const onScroll = () => {
if (isTicking) return;
lastScrollY = window.scrollY;
isTicking = true;
requestAnimationFrame(() => {
updateNavVisibility();
updateCurrentSection();
isTicking = false;
});
}
window.addEventListener('scroll', onScroll);
updateNavVisibility();
updateCurrentSection();
});
これで、scrollイベントを使用した、追従ナビゲーションの完成です!
実装手順 JavaScript (Intersection Observer編)
手順4(JavaScript / 追従ナビゲーションを隠す処理)
続きまして、Intersection Observerを使った実装の解説を行います。
(ユーザーがスクロールを行うたびに発生するscrollイベントに対して、要素を監視し、要素が画面に入ったことで実行されるIntersection Observerの方がパフォーマンスが高く、現在主流とされています。)
Intersection Observerも同様に、ナビゲーションと、追従ナビゲーションを隠すトリガー(今回はフッター)と、上方向の基準地点(今回は#about
)変数を用意します。
Intersection Observerのopsionを、options
という変数をオブジェクトで用意します。今回は初期値をそのまま引用しました。
それぞれのoptionの中身としては、
root
: ビューポートを基準にするためnull
を設定。rootMargin
: 追加のマージンを設定しないため0px
を設定。threshold
: 要素が10%表示されたときにコールバックを呼び出すため0.1
を設定。
といった設定基準となります。
// Intersection Observerの option
const options = { root: null, rootMargin: '0px', threshold: 0.1 };
toggleNavVisibility
関数で、entries
(監視対象の要素に関する情報の配列)を受け取り、if文で先ほど宣言した変数と合致しているかを判別します。
IntersectionObserverに上記関数とオプションを登録し、Footer
とFirstSection
の監視を実行します。
const toggleNavVisibility = (entries) => {
entries.forEach((entry) => {
if (entry.target === Footer || entry.target === FirstSection) {
pageNav.classList.toggle('is-hidden', entry.isIntersecting)
}
})
}
const navObserver = new IntersectionObserver(toggleNavVisibility, options)
navObserver.observe(Footer)
navObserver.observe(FirstSection)
手順5(JavaScript / 表示位置に合わせたハイライトを追加)
document.querySelector
を使用して、現在のentry.target
(監視対象のセクション)のid
属性に対応するナビゲーションリンクを取得します。
また、テンプレートリテラル(`
)を使用して、href
属性のセレクタ文字列を生成しています。
const updateLinkState = (entries) => {
entries.forEach((entry) => {
const link = document.querySelector(`.page-nav__link[href="#${entry.target.id}"]`)
if (link) {
if (entry.isIntersecting) {
link.classList.add('is-current')
} else {
link.classList.remove('is-current', 'is-hidden')
}
}
})
}
const linkObserver = new IntersectionObserver(updateLinkState, options)
document.querySelectorAll('section').forEach((section) => linkObserver.observe(section))
これで、Intersection Observer でもハイライト付きナビゲーションを実装することができました!
現在表示されているsection
要素に応じて、対応する追従ナビゲーションのリンクにハイライトが適用されているはずです。
追従ナビゲーションの表示/非表示の判定範囲や、.is-current
が付与されたときのハイライトは、デザインに合わせてご自身でご調整ください!