MessageScroller
A bounded, auto-sticking chat log that keeps the newest message in view.
Overview
MessageScroller is the scroll viewport for a chat log. It stacks its children vertically and, while
the reader is at the bottom, keeps the newest message in view as content grows — pausing the moment
they scroll up to read history, and resuming when they return to the bottom. Constrain its height
via style.
Anatomy
A vertical flex stack inside a scroll viewport with a themed scrollbar (comp.messageScroller.*).
After each update it checks whether the reader is near the bottom; if so, it pins scrollTop to the
newest message. Fill it with Message rows.
Variants & States
No visual variants — the one behavioral toggle is stickToBottom.
| State | Status | Notes |
|---|---|---|
| Following (at bottom) | Implemented | New messages auto-scroll into view. |
| Reading history (scrolled up) | Implemented | Auto-scroll pauses until the reader returns to the bottom. |
| stickToBottom=false | Implemented | Opts out of auto-scrolling entirely. |
| Disabled | Not implemented | — |
| Loading | Not implemented | — |
Usage
Do
- Constrain the height via style (maxHeight) — the bound is what makes the log scroll.
- Fill it with Message rows so avatars, authors, and bubbles stay consistent.
- Keep stickToBottom on for live chat so new messages appear without manual scrolling.
Don't
- Don't force-scroll the reader back to the bottom while they're reading history — the pause behavior is intentional.
- Don't nest another vertical scroll region inside it on the same axis.
Accessibility
- Respect the reader’s scroll position: auto-scroll only resumes when they choose to return to the bottom, so focus and reading are never yanked away.
- Consider a
role="log"(oraria-live="polite") on the region when messages arrive live, so new content is announced without stealing focus. - The themed scrollbar uses standard
scrollbar-*properties and degrades to the native scrollbar. - See the Accessibility Guide for shared conventions.
Content
N/A — the scroller has no content model of its own. It bounds and follows whatever message rows you place inside.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
stickToBottom | boolean | true | Keep the view pinned to the newest message while the reader is at the bottom. |
...rest | HTMLAttributes<HTMLDivElement> | — | Native <div> attributes pass through; use style to set maxHeight. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
stick-to-bottom | boolean | true | Reflected attribute; keep the view pinned to the newest message while the reader is at the bottom. |
scrollToBottom() | () => void | — | Method that jumps the viewport to the newest message. |
(default slot) | slot | — | The vertical stack of <gh-message> rows. Set max-height on the host. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
stickToBottom | boolean | true | Keep the view pinned to the newest message while the reader is at the bottom. |
children | ReactNode | — | The vertical stack of messages. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { MessageScroller } from '@ghds/react/message-scroller';
import { Message } from '@ghds/react/message';
<MessageScroller style={{ maxHeight: 320 }}>
{messages.map((m) => (
<Message key={m.id} side={m.side}>
{/* … MessageAvatar / MessageContent / Bubble … */}
</Message>
))}
</MessageScroller>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/message-scroller';
import '@ghds/web-components/message';
// Imperative: scroller.scrollToBottom() jumps to the newest message
</script>
<gh-message-scroller style="max-height: 320px;">
<gh-message side="received">
<span slot="author">Bo Ram</span>
<gh-bubble variant="received">Hey — are we still on for tomorrow?</gh-bubble>
</gh-message>
</gh-message-scroller>
// React Native — @ghds/react-native
import { MessageScroller } from '@ghds/react-native/message-scroller';
import { Message } from '@ghds/react-native/message';
<MessageScroller style={{ maxHeight: 320 }}>
{messages.map((m) => (
<Message key={m.id} side={m.side}>{/* … */}</Message>
))}
</MessageScroller>;
Live Demo
React and Web Components render live below, driven by the same tokens — toggle dark mode in the
header to see both respond identically. React Native can’t run in a browser without additional
react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.