Experimental: The AI extension and its component APIs may change in the future. Please give us feedback on your experience with them.
Demos#
See Message scroller for demos of how the conversation scrolls.
Conversation with date and info#
Scroll up to see the button that takes you to the latest message.
Aino is a chatbot for customer service. Do not share personal information with the chatbot. Read more about your privacy.
Some answers are generated with artificial intelligence and are marked with a tag. AI-generated answers can contain errors.
<Conversation style={{ height: '24rem', }} > <DateMarker date="2026-02-02" /> <Info> <P> Aino is a chatbot for customer service. Do not share personal information with the chatbot. Read more about{' '} <Anchor href="https://www.dnb.no/personvern">your privacy</Anchor>. </P> <P top> Some answers are generated with artificial intelligence and are marked with a tag. AI-generated answers can contain errors. </P> </Info> <Message name="Aino" timestamp="14:32"> Hi, I am Aino, the digital assistant of DNB. </Message> <Message>How can I help you?</Message> <Message from="user" name="You" timestamp="14:33"> I see an amount withdrawn from my account that I do not recognize. Can you help me find out what it is? </Message> <Message name="Aino" timestamp="14:33" aiGenerated> Of course. Choose the transaction you want to know more about, and I will show you the details. </Message> </Conversation>
Reading while content changes#
Scroll to earlier messages, then send a new prompt or extend the reply. A new user prompt returns to the end; reading or selecting older content pauses following. Resizing the prompt keeps the latest reply visible when you are following it. Receive message adds an incoming reply; while reading earlier messages, the scroll button shows “New message” and the hook reports unread messages.
Unread messages: no
const UnreadState = () => { const { hasUnreadMessages } = useConversationScrollState('reading-chat') return <P>Unread messages: {hasUnreadMessages ? 'yes' : 'no'}</P> } const Reading = () => { const [messages, setMessages] = useState( Array.from( { length: 12, }, (_, index) => ({ id: 'reading-' + index, from: index % 2 ? ('assistant' as const) : ('user' as const), text: ( 'Message ' + index + '. Read earlier messages while the reply grows. ' ).repeat(4), }) ) ) return ( <> <Flex.Horizontal bottom> <Button onClick={() => setMessages((current) => current.map((message, index) => index === current.length - 1 ? { ...message, text: message.text + ' More reply content.'.repeat(12), } : message ) ) } > Extend reply </Button> <Button onClick={() => setMessages((current) => [ ...current, { id: 'reading-' + current.length, from: 'assistant', text: 'A new message from customer service.', }, ]) } > Receive message </Button> </Flex.Horizontal> <UnreadState /> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', height: '28rem', }} > <Conversation id="reading-chat" scrollBehavior="end" aria-live="off" style={{ flex: '1 1 auto', }} > {messages.map((message) => ( <Message key={message.id} id={message.id} from={message.from} actions={ message.from === 'assistant' && ( <Button size="small">Reply details</Button> ) } > {message.text} </Message> ))} </Conversation> <PromptInput onSubmit={({ value }) => setMessages((current) => [ ...current, { id: 'reading-' + current.length, from: 'user', text: value, }, { id: 'reading-' + (current.length + 1), from: 'assistant', text: 'Your next reply starts here.', }, ]) } /> </div> </> ) } render(<Reading />)
Virtualized history#
This opt-in example keeps a window of 1,000 variable-height messages mounted. Try navigating to message 100, appending a turn, extending the latest reply and prepending history. Use Show full transcript for browser search, selection across the full conversation and screen-reader access to unmounted messages.
The example has no real requests. The application owns reply announcements. See Virtualization for the tradeoffs and driver import.
Current turn: none
Unread messages: no
<VirtualizedDemo />