滾動效果

Sticky Reveal

吸頂圖文對照揭示:左側文字隨捲動流動,右側視覺吸附固定並交叉切換。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add sticky-reveal

還沒有專案,或安裝遇到問題?看快速開始 →

Sticky Reveal 是用 React、Tailwind CSS 與 Motion 做的捲動吸頂圖文對照(sticky scroll)區塊:文字欄照常捲動,視覺框以 CSS position: sticky 固定,再由 IntersectionObserver 判斷目前段落,切換對應的圖片。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

24
編輯中的呼叫預設屬性
<StickyReveal/>

完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。

適用情境

  • SaaS 產品頁逐步介紹功能,每段文字對應一張產品截圖
  • 新手導覽或「運作方式」區塊,依步驟切換示意圖
  • 案例研究、年度回顧等長文,讓圖表跟著段落換
  • 硬體或 App 介紹頁,依序展示不同畫面或角度
  • 不適合:需要依捲動進度逐格播放的連續動畫,改用 Image Sequence Scrub

安裝

npx webberui@latest add https://webberui.com/r/sticky-reveal.json

或在 components.json 設定 registries 後,改用 @webberui/sticky-reveal 安裝。

使用

以 StickyRevealItem 宣告每一段對照,media 放右側吸頂視覺、children 放左側文字:

import { StickyReveal, StickyRevealItem } from "@/components/ui/sticky-reveal";

<StickyReveal>
  <StickyRevealItem media={<img src="/design.jpg" alt="設計" className="h-full w-full object-cover" />}>
    <h3 className="text-2xl font-semibold">設計</h3>
    <p className="mt-2 text-neutral-600">從語意化 token 出發,全站節奏一致。</p>
  </StickyRevealItem>

  <StickyRevealItem media={<img src="/ship.jpg" alt="發佈" className="h-full w-full object-cover" />}>
    <h3 className="text-2xl font-semibold">發佈</h3>
    <p className="mt-2 text-neutral-600">一鍵安裝,內建 reduced-motion。</p>
  </StickyRevealItem>
</StickyReveal>

若內容位於巢狀捲動容器中,將該容器 ref 透過 container 傳入(頁面級捲動則可省略):

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[80vh] overflow-y-auto">
  <StickyReveal container={scrollerRef} stickyTop={16}>
    {/* ...StickyRevealItem */}
  </StickyReveal>
</div>

範例

左圖右文、橫式視覺框

mediaSide="left" 把視覺移到左側;mediaClassName 覆寫預設的 3:4 直式比例,stepClassName 縮短每段的捲動距離。

<StickyReveal
  mediaSide="left"
  mediaClassName="aspect-video"
  stepClassName="min-h-[50vh]"
  stickyTop={96}
>
  <StickyRevealItem media={<img src="/dashboard.png" alt="營運儀表板" className="h-full w-full object-cover" />}>
    <h3 className="text-xl font-semibold">即時儀表板</h3>
    <p className="mt-2 text-neutral-600">訂單、營收與庫存集中在同一頁。</p>
  </StickyRevealItem>
  <StickyRevealItem media={<img src="/reports.png" alt="報表匯出" className="h-full w-full object-cover" />}>
    <h3 className="text-xl font-semibold">排程報表</h3>
    <p className="mt-2 text-neutral-600">每週一早上自動寄出上週數據。</p>
  </StickyRevealItem>
</StickyReveal>

頁面有固定導覽列時,把 stickyTop 設成導覽列高度,視覺框才不會被蓋住。

受控模式:同步外部步驟標籤

傳入 activeIndex 後由你掌控目前段落;捲動時仍會透過 onActiveChange 回報,記得把值寫回 state。

const steps = ["連結帳號", "匯入資料", "邀請團隊"];
const [step, setStep] = React.useState(0);

<p className="text-sm text-neutral-500">步驟:{steps[step]}</p>
<StickyReveal activeIndex={step} onActiveChange={setStep} showProgress={false}>
  {/* ...StickyRevealItem */}
</StickyReveal>

用 useStickyReveal 做自訂進度

useStickyReveal() 在 StickyRevealItem 的內容裡可以讀到 active 與 total,適合做「02 / 04」這類段落編號。

"use client";

import { useStickyReveal } from "@/components/ui/sticky-reveal";

function StepCounter() {
  const { active, total } = useStickyReveal();
  return (
    <span className="font-mono text-xs text-neutral-500">
      {String(active + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}
    </span>
  );
}

Props

StickyReveal

Prop型別預設值說明
childrenReactNode—放入一組 StickyRevealItem
mediaSide"left" | "right""right"視覺區位置(左文右圖或左圖右文)
stickyTopnumber24吸頂視覺框距容器頂端的偏移(px)
activeIndexnumber—受控模式:由外部指定作用段落索引
onActiveChange(index: number) => void—作用段落改變時觸發
containerRefObject<HTMLElement | null>—巢狀捲動容器 ref;省略則以視窗為準
showProgressbooleantrue顯示可點擊的段落進度圓點
stepClassNamestring"min-h-[70vh]"每段文字容器的 class(撐開捲動距離)
mediaClassNamestring—吸頂視覺框的 class(覆寫比例、圓角等)
classNamestring—根容器的 class

StickyRevealItem

Prop型別預設值說明
mediaReactNode—對照顯示的視覺內容(圖片或圖形)
childrenReactNode—隨捲動流動的文字內容
classNamestring—附加到該段文字容器的 class

細節

  • 採用 IntersectionObserver 監看每段文字,以捲動容器「中央細帶」判定作用段落,單一時刻只有一段作用。
  • 所有 media 疊放在同一個吸頂框內,作用者淡入、非作用者淡出並帶輕微縮放與模糊,形成交叉揭示。
  • 受控/非受控雙模式:未傳 activeIndex 時內部自管狀態;傳入則由你掌控,捲動仍會透過 onActiveChange 回報。
  • StickyRevealItem 僅作宣告,實際佈局由 StickyReveal 讀取其 props 統一渲染,以便集中管理吸頂與交叉切換。
  • 可用 useStickyReveal() 在子樹中讀取 active / total / isStatic,打造自訂進度指示。

可及性

  • 尊重 prefers-reduced-motion:停用交叉淡入的過渡與模糊、進度圓點過渡,點擊定位改為即時跳轉;版面(吸頂)維持不變。
  • 非作用中的視覺層標記 aria-hidden,避免輔助科技重複朗讀。
  • 進度圓點為原生 <button>,可鍵盤聚焦與觸發,作用中者帶 aria-current,點擊平滑捲回該段。
  • 附帶 aria-live="polite" 區域即時播報「目前第 N 段,共 M 段」。

常見問題

可以在 Next.js App Router 使用嗎?

可以。檔案開頭已經有 "use client",在 Server Component 頁面裡照一般方式 import 即可。IntersectionObserver 只在 effect 裡建立,不影響伺服器端渲染。

放在 overflow 捲動的容器裡為什麼不會切換?

IntersectionObserver 預設以視窗為基準。內容在巢狀捲動容器裡時,把該容器的 ref 傳給 container,判定與點擊進度圓點的捲動都會改用那個容器。

怎麼調整視覺框的比例與每段長度?

視覺框預設是 aspect-[3/4] 的圓角框,用 mediaClassName 覆寫;每段文字預設 min-h-[70vh],用 stepClassName 調整。段落越短,切換越頻繁。

手機上的版面是什麼樣子?

元件沒有內建斷點,任何寬度下都維持左右兩欄,視覺欄固定佔 2/5 寬。窄螢幕若要改成上下排列,需要自行修改原始碼中的版面 class。

有考慮減少動態效果嗎?

有。偵測到「減少動態效果」時,交叉淡入、縮放與模糊的過渡都會停用,點擊進度圓點改為直接跳轉,吸頂版面不變。