WebberUI

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.jsonfiles[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型別預設值說明
paragraphsParallelColumnParagraph[]對照內容:段落為外層、句子配對為內層
leftLabelReactNode左欄抬頭
rightLabelReactNode右欄抬頭
activeIdstring | null受控模式:外部指定高亮句 id(傳入即進入受控)
defaultActiveIdstring | nullnull非受控模式的初始高亮句 id
onActiveChange(id: string | null) => void高亮句改變時觸發
trackScrollbooleantrue捲動時追蹤最接近中央的段落並套用作用態
containerRefObject<HTMLElement | null>巢狀捲動容器 ref;省略則以視窗為準
interactivebooleantrue是否啟用句子 hover/focus/點按連動
stickyHeaderbooleantrue欄位抬頭是否吸附於捲動容器頂端
columnClassNamestring套用到每個欄位儲存格的 class
headerClassNamestring套用到抬頭列的 class
classNamestring根容器的 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),支援鍵盤 EnterSpace 釘選,作用中者帶 aria-currentaria-pressed,並具備 focus-visible 外框。
  • 尊重 prefers-reduced-motion:停用連結軸線的過渡與句子底色的 CSS transition,作用態改為即時切換;版面對齊與連動功能維持不變。
  • 可透過 interactive={false} 關閉互動,作為純對照展示版面。

On this page