Skip to content

Chat Message List

Follow streaming answers while letting people scroll back through the conversation.

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use it for the scrolling part of any chat or assistant screen, with Chat Messages inside it. It owns the scroll behavior people notice when it's wrong: staying with an answer as it grows, stopping when they scroll up to read, and getting them back down. For a feed of events that doesn't stream, use a Timeline or a list with Presence List.

import { ChatMessage, ChatMessageList } from '@youj/react'

export function Conversation({ messages }: { messages: Message[] }) {
  return (
    <ChatMessageList aria-label="Conversation with Youj">
      {messages.map((m) => (
        <ChatMessage key={m.id} role={m.role} content={m.stream ?? m.text} streaming={m.streaming} />
      ))}
    </ChatMessageList>
  )
}

Give every child a stable key. The list fills its parent (flex-1, min-h-0), so put it in a flex column with a height, above the composer. contentClassName sets the width and padding of the column the messages sit in, which is centered at max-w-3xl by default.

useChatMessageList() returns the surrounding list's scrollToBottom(behavior) and its scroll viewport, for a custom jump control or a virtualizer. With a virtualizer, pass animate={false} so messages render as they are and the virtualizer positions the rows itself.

import { useChatMessageList } from '@youj/react'

function BackToLatest() {
  const { scrollToBottom } = useChatMessageList()
  return <Button onClick={() => scrollToBottom('smooth')}>Back to latest</Button>
}

Reference

Propsaria-label, animate, contentClassName… (5)

Props

ChatMessageList props
PropType
childrenReactNode

ChatMessages (keyed), and anything else in the flow: a typing indicator, suggestions.

–
aria-labelstring

Names the conversation for assistive tech.

default "Conversation"

"Conversation"
animateboolean

Animate messages in and out. Turn it off for a virtualizer.

default true

true
contentClassNamestring

Classes for the column the messages sit in (width, padding).

–
jumpLabelstring

The jump button's label.

default "Jump to latest"

"Jump to latest"
Other props pass through: extends ComponentProps<'div'>.
MotionFollow, message enters, others make room… · reduced motion

Motion

Motion tokens
MomentTokens
Followthe scroll glides after new content, retargeting with its velocity
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Message entersfades and drops 6px into its slot (Presence List)
duration.baseease.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Others make room
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Message leavesfades in place
duration.fastease.exit

140ms · cubic-bezier(0.4, 0, 1, 1)

140mscubic-bezier(0.4, 0, 1, 1)
Jump inrises 8px and scales from .96 as it fades in
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Jump outdrops 6px and fades
duration.fastease.exit

140ms · cubic-bezier(0.4, 0, 1, 1)

140mscubic-bezier(0.4, 0, 1, 1)

Reduced motion

Following and jumping are instant, messages appear and disappear in place, and the jump button shows and hides at once.

A token that arrives as new text re-renders only its own message's slot, never the list, so the enter and make-room motion runs only when messages come, go or move.

Reduced motion

The view jumps to new content instead of gliding, and Jump to latest takes you down at once. New messages appear in place, and the button shows and hides without a rise. Sticking, letting go and the announcements are unchanged.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The column is a log with aria-live="off", inside a scroll area named by aria-label (default "Conversation"). A streaming answer is never read token by token.
  • The list announces for its messages through its own live regions: "Youj is responding…" once when a reply starts, the whole answer as plain text when it's complete, and failures assertively. Messages already there when the list mounts are history and aren't announced.
  • The scroll viewport is focusable, so arrow keys, Page Up and Home scroll it, and scrolling up by key lets go of the bottom like a wheel does.
  • Jump to latest is a real button. Pressing it moves focus to the conversation before the button leaves.