Pull Quote Lift
段落中的句子捲入視口時,以共享元素動畫抽出、放大為跨欄 pull quote,離開視口時退回行內位置。
載入預覽⋯
安裝
npx shadcn@latest add https://webberui.com/r/pull-quote-lift.json或在 components.json 設定 registries 後,改用 @webberui/pull-quote-lift 安裝。
安裝依賴後,從 registry JSON(/r/pull-quote-lift.json 的 files[0].content)複製 pull-quote-lift.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge使用
import { PullQuoteLift, LiftQuote } from "@/components/ui/pull-quote-lift";
<PullQuoteLift>
<p>
雜誌編輯會把最關鍵的一句話抽出來放大成引言。
<LiftQuote>動畫不是裝飾,而是介面對使用者說話的方式。</LiftQuote>
網頁同樣適用這個技巧。
</p>
</PullQuoteLift>PullQuoteLift 包住整段文章,LiftQuote 包住要抽出的句子。句子捲入視口時會「浮出」放大為跨欄 pull quote(附裝飾引號與上下分隔線),離開視口時退回原文行內位置。
Props
PullQuoteLift
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 文章內容,段落中可包含一或多個 LiftQuote |
className | string | — | 外層容器的 className |
LiftQuote
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 要抽出成 pull quote 的句子 |
quoteClassName | string | — | 追加到抽出後 blockquote 的 className |
細節
- layoutId FLIP 原理:行內的
motion.span與抽出後的motion.blockquote共用同一個layoutId。切換時 Motion 會快照舊元素的位置與尺寸,讓新元素從舊的邊界框以 transform 過渡到自己的最終位置(FLIP),因此句子看起來是從行內「飛出」到引言區塊。 - 字級 scale morph:FLIP 是以 scale 內插兩個尺寸差異很大的盒子,過渡期間文字會被暫時縮放變形(由行內字級 morph 到引言大字級)。這是共享元素動畫的固有特性,動畫結束後即恢復精確排版。
- pull quote 的渲染位置:抽出後的
blockquote以 portal 渲染到文章尾端的插槽,而不是句子所在的<p>內——<blockquote>不能巢狀在<p>裡,portal 同時讓行內與引言兩側在同一次 commit 內切換,FLIP 快照才銜接得上。 - 佔位策略:抽出後行內保留淡化的原句佔位,段落排版不會重排,
useInView觀測的元素尺寸因此保持穩定,避免在視口邊界來回震盪。 - 巢狀捲動容器:
useInView預設以瀏覽器視口為根,IntersectionObserver 會自動計入祖先overflow的裁切,所以放在固定高度的捲動容器內也能正常觸發。若容器會在動畫進行時捲動,建議把容器改為motion元素並加上layoutScroll,讓 FLIP 量測正確(見示範原始碼)。 - 往返動畫:引言區塊包在
AnimatePresence內,回捲離開視口時會在退場的同時把位置交還給重新掛載的行內 span,兩者交叉淡接。
可及性
- 使用者系統開啟「減少動態效果」時,不做任何動畫:原句留在段落中,pull quote 直接靜態顯示
- 句子永遠留在段落內(抽出時為淡化版本),文章的朗讀順序恆定;抽出後的
blockquote是裝飾性複本,帶aria-hidden,螢幕閱讀器不會重複朗讀