Yugen UI

MessageScroller

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.

Comes with MessageBubble (incoming / outgoing, avatar, time, delivery status, retry, reactions, automatic grouping of consecutive messages from the same sender, streamed content), MessageReaction, MessageDaySeparator and TypingIndicator. The End key jumps to the newest message. Incoming messages are announced through a polite live region; a streaming message is only announced once it is complete.
Today
Sakura Bot
Hi 👋 I'm Sakura Bot.
Ask me anything about Yugen UI.

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.

TSX
// 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.

You
I've sent the design file.
Seen
Could you check the Sakura colors?
Delivered
I need it before 5 PM.
Sent
Thanks!

Delivery status & retry

sending → sent → delivered → read; error shows a message and a Retry button.

Monday, Oct 19
Alex Chen
The new build went to staging this morning.
Could everyone check the Trace part?
Message me if anything comes up.
You
Checked it, runs smoothly 🎉

Grouping & reactions

Consecutive messages from the same sender are grouped: only the first one shows the avatar, name and time.

Maria
Message 100
Message 101
You
Message 102
Maria
Message 103
Message 104
You
Message 105
Maria
Message 106
Message 107
You
Message 108
Maria
Message 109
Message 110
You
Message 111

Loading older messages

Scroll to the top to call onLoadOlder; the reading position stays put when older messages are inserted.

Sam
Update #1
Priya
Update #2
Sam
Update #3
Priya
Update #4
Sam
Update #5
Priya
Update #6
Sam
Update #7
Priya
Update #8
Sam
Update #9
Priya
Update #10
Sam
Update #11
Priya
Update #12
Sam
Update #13
Priya
Update #14

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

Do
  • 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
  • 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 · TypeDefaultDescription
MessageScroller.onLoadOlder() => void | Promise<void>Default: —Called when scrolling reaches the top; a Promise prevents repeated calls.
MessageScroller.hasOlder / loadingOlderbooleanDefault: —Whether older messages remain; shows a spinner at the top of the list.
MessageScroller.labelstringDefault: "Conversation"Accessible name of the log region.
MessageScroller.bottomThresholdnumberDefault: 48Distance (px) from the bottom that still counts as being at the end.
MessageScroller.announceIncomingbooleanDefault: trueAnnounces incoming messages through a polite live region.
MessageBubble.side"incoming" | "outgoing"Default: "incoming"Someone else's message / your own.
MessageBubble.author / avatar / avatarSrcstring / ReactNode / stringDefault: —Sender; the default avatar is an Avatar built from the name (incoming messages).
MessageBubble.time / dateTimestringDefault: —Displayed time and the ISO value for <time>.
MessageBubble.status / onRetry / errorMessage"sending" | "sent" | "delivered" | "read" | "error"Default: —Delivery status; error shows a Retry button.
MessageBubble.groupPosition"single" | "first" | "middle" | "last"Default: —Overrides the position in the group (computed automatically by default).
MessageBubble.reactionsReactNodeDefault: —Reactions slot (MessageReaction).
MessageBubble.streamingbooleanDefault: —Streaming: blinking caret, aria-busy, not announced through the live region yet.
MessageBubble.appearance"bubble" | "plain"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 · TypeDefault (English)messagesVi
labelstring

Nhãn vùng hội thoại (khi không truyền label).

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 · TypeDefault (English)messagesVi
sendingstring

Nhãn trạng thái gửi — cùng tên với MessageStatus.

Default (English)"Sending"messagesVi"Đang gửi"
sentstringDefault (English)"Sent"messagesVi"Đã gửi"
deliveredstringDefault (English)"Delivered"messagesVi"Đã nhận"
readstringDefault (English)"Seen"messagesVi"Đã xem"
errorstring

Lỗi gửi (khi không truyền errorMessage).

Default (English)"Couldn't send."messagesVi"Không gửi được."
retrystringDefault (English)"Retry"messagesVi"Thử lại"

messages.typingIndicator

Key · TypeDefault (English)messagesVi
label(author?: string) => string

Câu đọc cho trình đọc màn hình (khi không truyền label).

Default (English)(author) => `${author ?? "Someone"} is typing…`messagesVi(author) => `${author ?? "Đối phương"} đang soạn tin…`