Message Scroller
MessageScroller is a conversation scroll area that sticks to the bottom for new messages, shows “N new messages” while you read older ones, and keeps your place when history loads.
Import
Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.
// npm install yugen-uiimport { MessageScroller, MessageBubble, MessageReaction, MessageDaySeparator, TypingIndicator, ChatComposer,} from "yugen-ui";Variants
Each tile is a standalone example: hover to copy, or press Code to see the source.
Delivery status & retry
sending → sent → delivered → read; error shows a message and a Retry button.
Grouping & reactions
Consecutive messages from the same sender are grouped: only the first one shows the avatar, name and time.
Loading older messages
Scroll to the top to call onLoadOlder; the reading position stays put when older messages are inserted.
New messages while reading older ones
Scroll up, then click “Get a new message”: the list doesn't jump, and a floating button counts the new messages.
Usage
- Give each item a stable key (the message id) so new and older messages are detected correctly.
- Always scroll to the bottom when people send a message themselves (the component does this for side="outgoing").
- Give a failed message a clear way to retry, right on that message.
- Don't scroll down automatically while people are reading older messages. Let the new messages button do that.
- Don't put aria-live on the whole list while text streams. Screen readers would read every fragment.
- Don't render unsanitized HTML or Markdown in a bubble. MessageBubble shows plain text.
Props
| Prop · Type | Default | Description |
|---|---|---|
| MessageScroller | Default: — | Called when scrolling reaches the top; a Promise prevents repeated calls. |
| MessageScroller | Default: — | Whether older messages remain; shows a spinner at the top of the list. |
| MessageScroller | Default: "Conversation" | Accessible name of the log region. |
| MessageScroller | Default: 48 | Distance (px) from the bottom that still counts as being at the end. |
| MessageScroller | Default: true | Announces incoming messages through a polite live region. |
| MessageBubble | Default: "incoming" | Someone else's message / your own. |
| MessageBubble | Default: — | Sender; the default avatar is an Avatar built from the name (incoming messages). |
| MessageBubble | Default: — | Displayed time and the ISO value for <time>. |
| MessageBubble | Default: — | Delivery status; error shows a Retry button. |
| MessageBubble | Default: — | Overrides the position in the group (computed automatically by default). |
| MessageBubble | Default: — | Reactions slot (MessageReaction). |
| MessageBubble | Default: — | Streaming: blinking caret, aria-busy, not announced through the live region yet. |
| MessageBubble | Default: "bubble" | plain: no background, full width, for long AI answers. |
| MessageReaction{ emoji, count?, selected?, label? } | Default: — | Reaction button (aria-pressed). |
| TypingIndicator{ author?, avatar?, label? } | Default: — | Three dots (static with reduced motion), role=status. |
Messages
Text built into the component: labels, aria-labels, screen reader announcements.
English by default. Change it with the messages prop of YugenProvider, by the namespace below; messagesVi has the Vietnamese set. See Interface language.
messages.messageScroller
| Key · Type | Default (English) | messagesVi |
|---|---|---|
| labelstring Nhãn vùng hội thoại (khi không truyền | Default (English)"Conversation" | messagesVi"Cuộc trò chuyện" |
| loadingOlderstring Spinner khi đang tải tin cũ. | Default (English)"Loading earlier messages…" | messagesVi"Đang tải tin nhắn cũ…" |
| jumpToLateststring Tên nút cuộn xuống đáy khi không có tin mới. | Default (English)"Jump to latest message" | messagesVi"Tới tin nhắn mới nhất" |
| newMessages(count: number) => string Chữ trên nút cuộn xuống đáy khi có tin mới. | Default (English)(count) => (count === 1 ? "1 new message" : `${count} new messages`) | messagesVi(count) => `${count} tin nhắn mới` |
messages.messageBubble
| Key · Type | Default (English) | messagesVi |
|---|---|---|
| sendingstring Nhãn trạng thái gửi — cùng tên với | Default (English)"Sending" | messagesVi"Đang gửi" |
| sentstring | Default (English)"Sent" | messagesVi"Đã gửi" |
| deliveredstring | Default (English)"Delivered" | messagesVi"Đã nhận" |
| readstring | Default (English)"Seen" | messagesVi"Đã xem" |
| errorstring Lỗi gửi (khi không truyền | Default (English)"Couldn't send." | messagesVi"Không gửi được." |
| retrystring | Default (English)"Retry" | messagesVi"Thử lại" |
messages.typingIndicator
| Key · Type | Default (English) | messagesVi |
|---|---|---|
| label(author?: string) => string Câu đọc cho trình đọc màn hình (khi không truyền | Default (English)(author) => `${author ?? "Someone"} is typing…` | messagesVi(author) => `${author ?? "Đối phương"} đang soạn tin…` |