AI 應用元件

對話捲動容器

對話用的捲動容器:串流中貼底自動跟隨,離開底部時浮出「新訊息」膠囊,載入歷史不跳位,可程式捲到任一則訊息。

這是 WebberUI Pro 元件

下方的安裝指令需要授權才能執行,未授權時會回 401。

怎麼安裝 Pro 元件 →查看方案 →

AI 對話與聊天室最難處理的往往不是訊息本身,而是捲軸:回覆逐字串流時要一路貼著底部,使用者往上翻舊訊息時又不能被硬拉回去;往上載入更早的訊息時,畫面不能突然跳一大段。這個容器把這些行為一次做好——在底部附近就平滑跟隨新內容,離開底部則在下方浮出「新訊息」膠囊;捲到頂部會觸發載入並補償捲動位置;另外提供命令式 API,可以捲到底部或捲到任一則訊息並短暫高亮。

把這個效果裝進你的專案

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

npx webberui@latest add "https://webberui.com/r/chat-scroller.json?t=<安裝 token>"

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

Playground

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

48
200
400
編輯中的呼叫預設屬性
<ChatScroller/>

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

安裝

npx webberui@latest add "https://webberui.com/r/chat-scroller.json?t=<安裝 token>"

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

使用

import * as React from "react";
import {
  ChatScroller,
  type ChatScrollerHandle,
} from "@/components/ui/chat-scroller";

export function Conversation({ messages, hasMore, loadOlder }) {
  const scrollerRef = React.useRef<ChatScrollerHandle>(null);

  return (
    <>
      <ChatScroller
        ref={scrollerRef}
        className="h-[480px] p-4"
        hasMore={hasMore}
        onLoadMore={loadOlder}
        onAtBottomChange={(atBottom) => console.log("atBottom", atBottom)}
      >
        {messages.map((m) => (
          <ChatScroller.Message key={m.id} id={m.id} className="py-1.5">
            {m.text}
          </ChatScroller.Message>
        ))}
      </ChatScroller>

      <button
        type="button"
        onClick={() =>
          scrollerRef.current?.scrollToMessage("m3", { highlight: true })
        }
      >
        跳到第 3 則
      </button>
    </>
  );
}

Props

Prop型別預設值說明
childrenReact.ReactNode—訊息節點;每則訊息外層需帶 data-message-id,或用 ChatScroller.Message 包裝
followThresholdnumber48距離底部多少 px 內視為「在底部」;在底部時新內容會平滑跟隨
durationnumber200近距離平滑捲動的時長(ms),同時寫入 --wb-chat-scroller-duration
longDurationnumber400捲動距離超過一個視窗高度時使用的時長(ms)
onLoadMore() => Promise<void>—捲到頂部 loadMoreThreshold 內時觸發;Promise 未完成前不會重複觸發,頂部顯示小 spinner
loadMoreThresholdnumber120距離頂部多少 px 內觸發 onLoadMore
hasMorebooleanfalse是否還有更早的訊息;false 時不觸發 onLoadMore
newMessageLabelstring"新訊息"新訊息膠囊的文字
unreadCountnumber—外部提供的未讀數;大於 0 時膠囊顯示「3 則新訊息」,且離開底部時就會出現
onAtBottomChange(atBottom: boolean) => void—是否在底部的狀態改變時回呼
ariaLabelstring"對話紀錄"捲動容器的無障礙名稱
refReact.Ref<ChatScrollerHandle>—命令式 API(React 19 起 ref 直接當 prop 傳入)
classNamestring—套用在捲動容器上;高度由你決定,例如 h-[480px]

ChatScrollerHandle

欄位型別預設值說明
scrollToBottom(opts?: { smooth?: boolean }) => voidsmooth: true捲到最底部
scrollToMessage(id: string, opts?: ChatScrollerScrollToMessageOptions) => void—捲到 data-message-id 等於 id 的訊息;找不到就不動
isAtBottom() => boolean—依目前幾何計算是否在底部

ChatScrollerScrollToMessageOptions

欄位型別預設值說明
align"start" | "center" | "end""center"目標訊息停在捲動區的上緣、中間或下緣
smoothbooleantrue是否平滑捲動
highlightbooleanfalse抵達時讓訊息背景以琥珀色淡入淡出一次(共 1.2 秒)

ChatScroller.Message

欄位型別預設值說明
idstring—訊息 id,輸出為 data-message-id(不寫成 DOM 的 id,避免重複 id)
classNamestring—額外 class;其餘 div 屬性會原樣透傳

另外具名匯出 ChatScrollerProps、ChatScrollerHandle、ChatScrollerScrollToMessageOptions、ChatScrollerMessageProps 型別。

細節

  • 初次掛載直接到底:在第一次繪製前把 scrollTop 設為 scrollHeight,不播動畫,使用者不會看到從頂部滑下來的過程
  • 跟隨或提示:用 ResizeObserver 觀察內容容器(不是捲動容器)。內容長高時,若變化前在底部就以 duration 毫秒的 ease-out 補間跟到底;串流中每個字都會觸發一次,前一段補間還沒結束就從目前位置重新起算,不會抖動。若不在底部則不動,改為浮出新訊息膠囊
  • 自寫補間,不用 CSS smooth:scroll-behavior: smooth 的時長由瀏覽器決定、無法控制,這裡一律用 requestAnimationFrame 補間;距離超過一個視窗高度時改用 longDuration。補間途中只要使用者滾輪、觸控或按 PageUp 等捲動鍵,補間立即取消,以使用者的位置為準
  • 載入歷史不跳位:觸發 onLoadMore 前記下 scrollHeight 與 scrollTop,新訊息插入頂部後把增加的高度補回 scrollTop,畫面上正在看的訊息停在原處。overflow-anchor: auto 只當輔助(Safari 不支援),實際補償不依賴它
  • 高亮:scrollToMessage 的 highlight 直接在目標節點加上 bg-amber-100/60 dark:bg-amber-500/10,淡入、停留後淡出,1.2 秒後移除,訊息元件不需要配合

可及性

  • 捲動容器為 role="log"、aria-live="polite"、aria-relevant="additions",螢幕閱讀器會讀出新加入的訊息;aria-label 預設「對話紀錄」
  • 捲動容器 tabIndex={0},可用鍵盤聚焦後以方向鍵、PageUp/PageDown 捲動,聚焦時顯示 focus-visible 外框;載入歷史期間設 aria-busy
  • 新訊息膠囊是 type="button",aria-label 含未讀數與「捲到最新」;點擊後焦點交回捲動容器,不會因膠囊消失而遺失
  • 載入中的 spinner 為 role="status",附「正在載入較早的訊息」的隱藏文字
  • 使用者系統開啟「減少動態效果」時,所有捲動(跟隨、跳到最新、跳到訊息)都瞬間完成,膠囊只淡入淡出不位移縮放,高亮也直接上色不漸變