rapidUI

Message Scroller

Scroll container for chat messages that sticks to the bottom as new messages arrive.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/message-scroller.json

Installs to @/components/ui/message-scroller.

Manual installation

Install the dependencies:

pnpm add @shadcn/react@^0.3.1 lucide-react@^1.48.0

Add the components it builds on: button.

Copy the source into your project:

components/ui/message-scroller.tsx
'use client'

import { MessageScroller as MessageScrollerPrimitive } from '@shadcn/react/message-scroller'
import { ArrowDownIcon } from 'lucide-react'
import type * as React from 'react'

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

function MessageScrollerProvider(props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>) {
	return <MessageScrollerPrimitive.Provider {...props} />
}

function MessageScroller({ className, ...props }: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
	return (
		<MessageScrollerPrimitive.Root
			data-slot="message-scroller"
			className={cn('group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden', className)}
			{...props}
		/>
	)
}

function MessageScrollerViewport({
	className,
	...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
	return (
		<MessageScrollerPrimitive.Viewport
			data-slot="message-scroller-viewport"
			className={cn(
				'size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent data-pending-scroll:invisible',
				className,
			)}
			{...props}
		/>
	)
}

function MessageScrollerContent({
	className,
	...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
	return (
		<MessageScrollerPrimitive.Content
			data-slot="message-scroller-content"
			className={cn('flex h-max min-h-full flex-col gap-6', className)}
			{...props}
		/>
	)
}

function MessageScrollerItem({
	className,
	scrollAnchor = false,
	...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
	return (
		<MessageScrollerPrimitive.Item
			data-slot="message-scroller-item"
			scrollAnchor={scrollAnchor}
			className={cn('min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]', className)}
			{...props}
		/>
	)
}

function MessageScrollerButton({
	direction = 'end',
	className,
	children,
	render,
	variant = 'secondary',
	size = 'icon-sm',
	...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
	Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
	return (
		<MessageScrollerPrimitive.Button
			data-slot="message-scroller-button"
			data-direction={direction}
			data-variant={variant}
			data-size={size}
			direction={direction}
			className={cn(
				'absolute left-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180',
				className,
			)}
			render={render ?? <Button variant={variant} size={size} />}
			{...props}
		>
			{children ?? (
				<>
					<ArrowDownIcon />
					<span className="sr-only">{direction === 'end' ? 'Scroll to end' : 'Scroll to start'}</span>
				</>
			)}
		</MessageScrollerPrimitive.Button>
	)
}

export {
	MessageScrollerProvider,
	MessageScroller,
	MessageScrollerViewport,
	MessageScrollerContent,
	MessageScrollerItem,
	MessageScrollerButton,
}
export {
	useMessageScroller,
	useMessageScrollerScrollable,
	useMessageScrollerVisibility,
} from '@shadcn/react/message-scroller'

A scroll container for chat transcripts built on @shadcn/react. It anchors new turns, follows the live edge while messages stream in, preserves the scroll position when history is prepended and shows a scroll-to-end button. Wrap it in MessageScrollerProvider and give it a height-constrained parent.

Features

  • Scroll anchoring on turn boundaries via scrollAnchor
  • Auto-scroll that follows streamed messages
  • Default opening position: start, end or last anchor
  • Preserves position when older messages are prepended
  • Scroll-to-end and scroll-to-start buttons
  • Hooks: useMessageScroller, useMessageScrollerScrollable and useMessageScrollerVisibility

Usage

import { Message } from '@/components/ui/message'
import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from '@/components/ui/message-scroller'

export function Transcript({ messages }: { messages: { id: string; role: string }[] }) {
  return (
    <div className="h-[480px]">
      <MessageScrollerProvider autoScroll>
        <MessageScroller>
          <MessageScrollerViewport>
            <MessageScrollerContent>
              {messages.map((message) => (
                <MessageScrollerItem key={message.id} messageId={message.id} scrollAnchor={message.role === 'user'}>
                  <Message>{/* ... */}</Message>
                </MessageScrollerItem>
              ))}
            </MessageScrollerContent>
          </MessageScrollerViewport>
          <MessageScrollerButton />
        </MessageScroller>
      </MessageScrollerProvider>
    </div>
  )
}

Examples

Default

A conversation with a case officer in a fixed-height chat window. Scroll up to reveal the scroll-to-end button.

Open At Last Anchor

Opens at the last user turn and keeps part of the previous turn visible.

Live Conversation

Send a message and watch auto-scroll follow the officer's reply.

Jump To Message

Jump controls built with useMessageScroller and useMessageScrollerVisibility.

With Markers And Attachments

Date markers and a document attachment inside the transcript.

Accessibility

  • Give the viewport an aria-label describing the conversation
  • The viewport is keyboard scrollable and focusable
  • The scroll button includes a screen reader label and is hidden when inactive
  • New content is exposed to assistive technology through the content region