|CSS+JS Simple|Fullpage with Scroll Snap

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/