|CSS+JS Simple|Fullpage with Scroll Snap

本篇教學文章將示範如何運用 CSS 滾動捕捉技術(scroll snap),並結合 IntersectionObserver 來偵測當前畫面中可見的 section,進而觸發 CSS 動畫效果與選單的高亮顯示,打造一個全屏網頁。
以下是導覽列 html 部分,其中每個 <li> 中的 <a> 具有 data-scroll 屬性,該屬性與後續內容區塊的 class 相對應,方便用 JavaScript 來進行滾動連結設定。
html
內容區塊包在 .scrollable-container 容器中,每個區塊使用 <section> 定義,並依序分配 class 與 data-view。每個區塊內可以包含 <article>,用來放置標題與內文,並有一個背景元素 <div class="bg"> 用以顯示背景圖。
html
Section1
Section2
Section3
Section3 文章內容...
在 :root 中定義全局變數,導覽列字級大小與行高,方便後續統一管理與扣除導覽列高度。scrollable-container 設定全螢幕高度(扣除導覽列高度),並啟用了 scroll-snap-type:block mandatory; 功能,確保用戶在滾動時自動捕捉到每個 section,overflow:hidden auto; 僅y軸需要時出現捲軸,並將 .scrollable-container::-webkit-scrollbar 的 width 設為 0。
css
:root{
--FontSize-menu: min(4.5vw, 1.5rem);
--LineHeight-menu: 2.5;
}
.scrollable-container{
height: calc(100vh - var(--FontSize-menu) * var(--LineHeight-menu));
scroll-snap-type: block mandatory; /* 強制捕捉 */
-webkit-overflow-scrolling: touch; /* 行動裝置平滑滾動 */
overflow: hidden auto;
}
/* 隱藏 WebKit 瀏覽器的捲軸 */
.scrollable-container::-webkit-scrollbar{
width: 0px;
}
每個 section 皆設定全螢幕高度(扣除導覽列高度)並啟用 scroll snap 對齊。container-type 屬性使其成為一個容器,可供 @container 規則使用;scroll-snap-align 與 scroll-snap-stop 分別指定捕捉時的對齊方式及強制捕捉效果。
css
section{
height: calc(100vh - var(--FontSize-menu) * var(--LineHeight-menu));
container-type: scroll-state; /* 定義容器類型 */
scroll-snap-align: start; /* 對齊起始點 */
scroll-snap-stop: always; /* 強制捕捉到每個點 */
overflow: hidden;
}
article 當有放內文段落 <p> 時。才設定 overflow-y:auto; ,達到內文超出範圍才出現捲軸。
css
article{ width:100%; display:flex; flex-direction:column; place-content:center;}
article:has(p){ height:100%; padding-block:min(10vw, 5rem); overflow-y:auto;
place-content:center flex-start;
}
設定 section.in-view CSS 動畫效果,之後用 IntersectionObserver 監測 section 添加 in-view 類名觸發 CSS 動畫效果。
css
section.in-view h1, section.in-view article h1{ transform:scale(1); opacity:1; filter:blur(0px);}
section.in-view .bg{ filter:grayscale(20%) blur(3px) brightness(0.5); background-size:auto 125%;}
section.in-view article p:nth-of-type(1){ transform:translateY(0%); opacity:1; filter:blur(0px);}
section.in-view article p:nth-of-type(2){ transform:translateY(0%); opacity:1; filter:blur(0px);}
section.in-view article p:nth-of-type(3){ transform:translateY(0%); opacity:1; filter:blur(0px);}
section.in-view article p:nth-of-type(4){ transform:translateY(0%); opacity:1; filter:blur(0px);}
以下 JavaScript 主要負責處理以下功能:點選導覽列中的 <a> 元素時,會依據該元素的 data-scroll 屬性來找到對應的內容區塊,然後呼叫 scrollIntoView({ behavior: "smooth" }) 方法實現平滑滾動效果。這樣可以確保用戶點選後畫面不會突然跳轉,而是平滑過渡到對應區塊。
script
$("ul.menu li").click(function () {
let goto;
if ($(this).find("a").length > 0) {
goto = $(this).find("a").data("scroll");// 存在<a>子元素
} else {
goto = $(this).data("scroll"); // 沒有<a>子元素
}
const point = $(`.${goto}`)[0];
if (point) {
point.scrollIntoView({ behavior: "smooth" });
}
});
接下來,我們要用 IntersectionObserver 偵測當前畫面可見的 section。當 section 進入視窗時,給予 in-view 類名,以觸發 CSS 動畫效果,並找到對應的選單 li,加上 active,同步高亮顯示。
script
/* section 和 menu 同步加上 class,IntersectionObserver */
document.addEventListener("DOMContentLoaded", () => {
// 設定參數物件
const config = {
targetSelector: "section", // 監測的目標元素
menuItemSelector: "ul.menu li", // 選單項目
inViewClass: "in-view", // section 可見時的類名
activeClass: "active", // 選單項目啟動時的類名
observerOptions: { // IntersectionObserver 選項
rootMargin: "0px 0px 0px 0px",
threshold: 0.5
}
};
// 初始化變數
let lastActiveLi = null;
const { targetSelector, menuItemSelector, inViewClass, activeClass, observerOptions } = config;
// 建立 IntersectionObserver
const observer = new IntersectionObserver((entries) => {
let newActiveLi = null;
// 處理每個 entry
entries.forEach((entry) => {
// 切換 section 的 in-view 類名
entry.target.classList.toggle(inViewClass, entry.isIntersecting);
// 尋找對應的選單項目
const sectionId = entry.target.dataset.view;
const menuItem = document.querySelector(
`${menuItemSelector} a[data-scroll="${sectionId}"]`
)?.parentElement;
// 如果 section 可見,更新 newActiveLi
if (entry.isIntersecting && menuItem) {
newActiveLi = menuItem;
}
});
// 更新選單的 active 狀態
if (newActiveLi && newActiveLi !== lastActiveLi) {
document.querySelectorAll(`${menuItemSelector}.${activeClass}`)
.forEach(li => li.classList.remove(activeClass));
newActiveLi.classList.add(activeClass);
lastActiveLi = newActiveLi;
}
}, observerOptions);
// 監測所有 section 元素
document.querySelectorAll(targetSelector)
.forEach(element => observer.observe(element));
});
圖素來源:
なちゃ ┊︎ まいにち素材配布(https://x.com/sozaiya_create/)