Sticky Reveal
吸頂圖文對照揭示:左側文字隨捲動流動,右側視覺吸附固定並交叉切換。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add sticky-revealSticky Reveal 是用 React、Tailwind CSS 與 Motion 做的捲動吸頂圖文對照(sticky scroll)區塊:文字欄照常捲動,視覺框以 CSS position: sticky 固定,再由 IntersectionObserver 判斷目前段落,切換對應的圖片。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<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 | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | ReactNode | — | 放入一組 StickyRevealItem |
mediaSide | "left" | "right" | "right" | 視覺區位置(左文右圖或左圖右文) |
stickyTop | number | 24 | 吸頂視覺框距容器頂端的偏移(px) |
activeIndex | number | — | 受控模式:由外部指定作用段落索引 |
onActiveChange | (index: number) => void | — | 作用段落改變時觸發 |
container | RefObject<HTMLElement | null> | — | 巢狀捲動容器 ref;省略則以視窗為準 |
showProgress | boolean | true | 顯示可點擊的段落進度圓點 |
stepClassName | string | "min-h-[70vh]" | 每段文字容器的 class(撐開捲動距離) |
mediaClassName | string | — | 吸頂視覺框的 class(覆寫比例、圓角等) |
className | string | — | 根容器的 class |
StickyRevealItem
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
media | ReactNode | — | 對照顯示的視覺內容(圖片或圖形) |
children | ReactNode | — | 隨捲動流動的文字內容 |
className | string | — | 附加到該段文字容器的 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。
有考慮減少動態效果嗎?
有。偵測到「減少動態效果」時,交叉淡入、縮放與模糊的過渡都會停用,點擊進度圓點改為直接跳轉,吸頂版面不變。