Skip to main content

Gemini-like Chatbot Scroll

The example below simulates a conversation with a chatbot. The scroll behavior simulates the Gemini chatbot, where each new question is scrolled to the top, so that a space is preallocated for the answer. The streaming response is simulated with setInterval.

Key Points

  • Each question gets aligned to the top of the viewport when it is sent.
  • The data.map method is used to stream the incoming response.
  • The autoscrollToBottomBehavior is set to smooth to animate the scroll when the streaming response scroll beyond the visible area.

Live Example

Live Editor

interface Message {
  key: string
  text: string
  user: 'me' | 'other'
}

let idCounter = 0

function randomMessage(user: Message['user']): Message {
  return { user, key: `${idCounter++}`, text: randTextRange({ min: user === 'me' ? 20 : 100, max: 200 }) }
}

const ItemContent: VirtuosoMessageListProps<Message, null>['ItemContent'] = ({ data }) => {
  const ownMessage = data.user === 'me'
  return (
    <div style={{ paddingBottom: '2rem', display: 'flex' }}>
      <div
        style={{
          maxWidth: '80%',
          marginLeft: data.user === 'me' ? 'auto' : undefined,

          background: ownMessage ? '#0253B3' : '#F0F0F3',
          color: ownMessage ? 'white' : 'black',
          borderRadius: '1rem',
          padding: '1rem',
        }}
      >
        {data.text}
      </div>
    </div>
  )
}

function App() {
  const virtuoso = React.useRef<VirtuosoMessageListMethods<Message>>(null)

  return (
    <div className="tall-example" style={{ height: 500, display: 'flex', flexDirection: 'column', fontSize: '70%' }}>
      <VirtuosoMessageListLicense licenseKey="">
        <VirtuosoMessageList<Message, null>
          ref={virtuoso}
          style={{ flex: 1 }}
          computeItemKey={({ data }) => data.key}
          initialLocation={{ index: 'LAST', align: 'end' }}
          ItemContent={ItemContent}
        />
      </VirtuosoMessageListLicense>

      <button
        onClick={() => {
          const myMessage = randomMessage('me')
          virtuoso.current?.data.append([myMessage], ({ scrollInProgress, atBottom }) => {
            return {
              index: 'LAST',
              align: 'start',
              behavior: atBottom || scrollInProgress ? 'smooth' : 'auto',
            }
          })

          setTimeout(() => {
            const botMessage = randomMessage('other')
            virtuoso.current?.data.append([botMessage])

            let counter = 0
            const interval = setInterval(() => {
              if (counter++ > 20) {
                clearInterval(interval)
              }
              virtuoso.current?.data.map((message) => {
                  return message.key === botMessage.key ? { ...message, text: message.text + ' ' + randPhrase() } : message
                },
                'smooth'
              )
            }, 150)
          }, 1000)
        }}
      >
        Ask the bot a question!
      </button>
    </div>
  )
}

render(<App />)
Result
Loading...