Parallel Text Columns
對照雙欄版面:雙語/雙版本內容左右對頁並排,句子層級互相配對,捲動自動對齊、hover 同步高亮連動。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<ParallelTextColumns />
安裝
npx shadcn@latest add https://webberui.com/r/parallel-text-columns.json或在 components.json 設定 registries 後,改用 @webberui/parallel-text-columns 安裝。
安裝依賴後,從 registry JSON(/r/parallel-text-columns.json 的 files[0].content)複製 parallel-text-columns.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge使用
內容以段落為外層、句子配對為內層。同一 id 的左右兩句互相連動:滑過任一欄的句子,另一欄的對應句會同步以底色高亮。
import {
ParallelTextColumns,
type ParallelColumnParagraph,
} from "@/components/ui/parallel-text-columns";
const paragraphs: ParallelColumnParagraph[] = [
{
id: "p1",
segments: [
{ id: "s1", left: "The city wakes slowly.", right: "城市緩緩甦醒。" },
{ id: "s2", left: "Trams hum along the avenues.", right: "電車沿著大道低鳴。" },
],
},
];
<ParallelTextColumns
paragraphs={paragraphs}
leftLabel="English"
rightLabel="中文"
/>;若內容位於巢狀捲動容器中,將該容器 ref 透過 container 傳入(頁面級捲動則可省略),捲動時最接近中央的段落會自動套用作用態:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[70vh] overflow-y-auto">
<ParallelTextColumns
paragraphs={paragraphs}
container={scrollerRef}
trackScroll
/>
</div>;受控模式:傳入 activeId 即由外部掌控高亮句(例如與語音播放進度同步),hover/focus 仍會透過 onActiveChange 回報意圖:
const [activeId, setActiveId] = React.useState<string | null>(null);
<ParallelTextColumns
paragraphs={paragraphs}
activeId={activeId}
onActiveChange={setActiveId}
/>;Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
paragraphs | ParallelColumnParagraph[] | — | 對照內容:段落為外層、句子配對為內層 |
leftLabel | ReactNode | — | 左欄抬頭 |
rightLabel | ReactNode | — | 右欄抬頭 |
activeId | string | null | — | 受控模式:外部指定高亮句 id(傳入即進入受控) |
defaultActiveId | string | null | null | 非受控模式的初始高亮句 id |
onActiveChange | (id: string | null) => void | — | 高亮句改變時觸發 |
trackScroll | boolean | true | 捲動時追蹤最接近中央的段落並套用作用態 |
container | RefObject<HTMLElement | null> | — | 巢狀捲動容器 ref;省略則以視窗為準 |
interactive | boolean | true | 是否啟用句子 hover/focus/點按連動 |
stickyHeader | boolean | true | 欄位抬頭是否吸附於捲動容器頂端 |
columnClassName | string | — | 套用到每個欄位儲存格的 class |
headerClassName | string | — | 套用到抬頭列的 class |
className | string | — | 根容器的 class |
型別
interface ParallelSegment {
id: string; // 配對識別碼,左右欄以此連動
left: React.ReactNode;
right: React.ReactNode;
}
interface ParallelColumnParagraph {
id?: string;
segments: ParallelSegment[];
}細節
- 版面採用兩欄 CSS Grid:每個段落輸出左右兩格並落在同一列,因此左右對應段落的頂緣自動對齊,無論兩側文字長短。
- 句子連動以共用的
id驅動:滑過(或聚焦)任一欄的句子,兩側同一id的句子同步套用底色高亮,形成跨欄的「連結」視覺。 - 觸控裝置沒有 hover,改以「點按釘選」達成同樣的連動:點一下釘選、再點一下取消。
trackScroll開啟時,以requestAnimationFrame節流的捲動監聽計算最接近容器中央的段落,替其套用作用態底色與欄間連結軸線;單一時刻只有一段作用。- 受控/非受控雙模式:未傳
activeId時內部自管高亮狀態(hover 優先於釘選);傳入則由你掌控,互動仍會透過onActiveChange回報。
可及性
- 整個對照區塊為
role="group"並帶aria-label,抬頭列標示左右欄語言/版本。 - 每個句子為可聚焦的連動觸發點(
role="button"、tabIndex=0),支援鍵盤Enter/Space釘選,作用中者帶aria-current與aria-pressed,並具備focus-visible外框。 - 尊重
prefers-reduced-motion:停用連結軸線的過渡與句子底色的 CSS transition,作用態改為即時切換;版面對齊與連動功能維持不變。 - 可透過
interactive={false}關閉互動,作為純對照展示版面。