Message Scroller
ui-message-scroller is the scrolling area of a chat. It starts scrolled to the bottom and stays pinned there as
messages are added, but only while the user is already at (or within threshold pixels of) the bottom. When the user
scrolls up to read history, new messages no longer move the view and a "Jump to latest" button appears. Clicking it
scrolls smoothly back down.
Give the scroller a height with the height attribute (in spacing units, height="60" is
calc(var(--spacing) * 60)) or let a flex or grid layout size it. It is always 100% wide. The viewport has
role="log" and aria-live="polite".
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Message Scroller demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/bubble.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/message-scroller.html"></import-element>
<import-element from="./message-scroller-demo.html"></import-element>
<message-scroller-demo></message-scroller-demo>
</body>
</html><custom-element name="message-scroller-demo">
<script>
const chat = useState({
next: 6,
messages: [
{ id: 1, align: "start", text: "Are we still on for the launch review?" },
{ id: 2, align: "end", text: "Yes, 3pm in the big room." },
{ id: 3, align: "start", text: "Great. Can you bring the metrics?" },
{ id: 4, align: "end", text: "Already in the deck." },
{ id: 5, align: "start", text: "Perfect, see you there." },
],
});
const REPLIES = ["On my way.", "Running five minutes late.", "Room changed to 4B.", "Starting now."];
function send() {
const text = REPLIES[chat.next % REPLIES.length];
chat.messages.push({ id: chat.next, align: chat.next % 2 ? "start" : "end", text });
chat.next++;
}
</script>
<style>
:host {
display: grid;
gap: calc(var(--spacing) * 3);
width: 100%;
max-width: calc(var(--spacing) * 112);
}
ui-message-scroller {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
.messages {
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 2);
padding: calc(var(--spacing) * 4);
}
</style>
<ui-message-scroller height="60">
<div class="messages">
<repeat-list of="#chat.messages" as="message" key="id">
<ui-bubble variant="muted" _align="#message.align"><slot name="#message.text"></slot></ui-bubble>
</repeat-list>
</div>
</ui-message-scroller>
<ui-button variant="outline" onclick="send()">Receive a message</ui-button>
</custom-element>Scroll up in the log and click "Receive a message". The view stays where it is and "Jump to latest" appears.
Examples
Threshold
threshold is how close to the bottom, in pixels, still counts as "at the bottom" (default 48). Use 0 to only
follow new messages when the view is exactly at the end.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Message Scroller demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
<style>
.demo-medium {
width: min(100%, calc(var(--spacing) * 120));
}
.demo-stack {
display: grid;
gap: calc(var(--spacing) * 4);
}
</style>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/avatar.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/bubble.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/marker.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/message-scroller.html"></import-element>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/message.html"></import-element>
<div class="demo-medium">
<ui-message-scroller threshold="0" height="40">
<div class="demo-stack">
<ui-marker variant="separator">Monday</ui-marker>
<ui-message>
<ui-message-avatar><ui-avatar><ui-avatar-fallback>AL</ui-avatar-fallback></ui-avatar></ui-message-avatar>
<ui-bubble variant="muted">Morning! Standup moved to 9:30.</ui-bubble>
</ui-message>
<ui-message align="end">
<ui-message-avatar><ui-avatar><ui-avatar-fallback>ME</ui-avatar-fallback></ui-avatar></ui-message-avatar>
<ui-bubble align="end">Thanks for the heads-up.</ui-bubble>
</ui-message>
<ui-marker variant="separator">Today</ui-marker>
<ui-message>
<ui-message-avatar><ui-avatar><ui-avatar-fallback>AL</ui-avatar-fallback></ui-avatar></ui-message-avatar>
<ui-bubble variant="muted">Release is tagged.</ui-bubble>
</ui-message>
<ui-message align="end">
<ui-message-avatar><ui-avatar><ui-avatar-fallback>ME</ui-avatar-fallback></ui-avatar></ui-message-avatar>
<ui-bubble align="end">Shipping it.</ui-bubble>
</ui-message>
</div>
</ui-message-scroller>
</div>
</body>
</html>API
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
threshold |
number | 48 |
Distance from the bottom, in pixels, within which the scroller stays pinned to new content. Minimum 0. |
height |
number | Height in spacing units, so height="60" is calc(var(--spacing) * 60). Without it, size the scroller from its layout. |
Parts
| Part | Slot | Renders |
|---|---|---|
ui-message-scroller |
(root) | The scrolling log and the "Jump to latest" button. |
The default slot takes the messages and any other content. Any change inside (added nodes, text changes) scrolls to the bottom while pinned.
CSS parts
| Part | Description |
|---|---|
viewport |
The scrolling element (role="log", aria-live="polite"). |
CSS states and custom properties
| Name | Description |
|---|---|
:state(attr-height) |
Matches while height is set. The height comes from var(--attr-height). |
:state(state-scroll-pinned) |
Matches while the view is within threshold of the bottom. The "Jump to latest" button is hidden in this state. |
Accessibility
- The viewport has
role="log"andaria-live="polite", so screen readers announce new messages. - The "Jump to latest" button is a regular
ui-button, reachable with Tab while it is shown.