How do I add a page?
MessageList
A conversation that stays pinned to the newest message unless the reader scrolls up.
Sticks to the bottom
New and streaming messages scroll into view. Scroll up and it stays put; the button brings you back.
atBottom: true · changes: 0
Run elements create page <name>. It writes the route, the template and a stylesheet.
And a test?
elements create test <path> scaffolds one next to the page. elements test runs them all.
Does it hot reload?
Yes: every save reloads the browser that shows the app.
Where do shared templates go?
In app/shared/templates/<name>/, imported extensionless.
Code
<div class="chat-box">
<MessageList atBottom={state.atBottom} busy={state.streaming}>
<Message e:for={m of state.messages} e:key={(m) => m.id} from={m.role} grouped={m.grouped}>
<MessageContent><MessageResponse markdown={m.text} streaming={...} /></MessageContent>
</Message>
<MessageListDownload slot="overlay" messages={state.messages} />
</MessageList>
</div>Usage
import { MessageList, MessageListEmpty, MessageListDownload } from "@elements/ui/message-list";
<MessageList busy={reply.streaming}>
<MessageListEmpty e:if={messages.length === 0} />
<Message e:for={m of messages} e:key={(m) => m.id} from={m.role}>
<MessageContent><MessageResponse markdown={m.text} /></MessageContent>
</Message>
<MessageListDownload slot="overlay" messages={messages} />
</MessageList>Examples
Empty state
MessageListEmpty centers in the list until the first message.
Start a conversation
Ask anything about your project
No messages yet
Start a conversation to see messages here
Pick a starter prompt below to begin.
Code
<MessageList>
<MessageListEmpty e:if={messages.length === 0}
title="Start a conversation"
description="Ask anything about your project">
<Icon slot="icon" name="message-square" />
</MessageListEmpty>
</MessageList>
<!-- Default slot content replaces the title and description. -->
<MessageListEmpty><p>Pick a starter prompt below to begin.</p></MessageListEmpty>Without following
stickToBottom off and initial none: the list starts at the top and never moves on its own.
How do I add a page?
Run `elements create page <name>`. It writes the route, the template and a stylesheet.
And a test?
`elements create test <path>` scaffolds one next to the page. `elements test` runs them all.
Does it hot reload?
Yes: every save reloads the browser that shows the app.
Where do shared templates go?
In `app/shared/templates/<name>/`, imported extensionless.
Code
<MessageList stickToBottom={false} initial="none">...</MessageList>Custom scroll button
scrollButtonVariant, scrollButtonClass, scrollButtonAttrs and a scroll-icon slot restyle the button; scrollMessageListToBottom resolves true once the bottom is reached.
resolved: none
How do I add a page?
Run `elements create page <name>`. It writes the route, the template and a stylesheet.
And a test?
`elements create test <path>` scaffolds one next to the page. `elements test` runs them all.
Does it hot reload?
Yes: every save reloads the browser that shows the app.
Where do shared templates go?
In `app/shared/templates/<name>/`, imported extensionless.
Code
<MessageList initial="none"
scrollButtonVariant="secondary"
scrollButtonClass="message-list-demo-scroll"
scrollButtonLabel="Jump to newest">
...
<Icon slot="scroll-icon" name="chevron-down" />
</MessageList>
let reached = await scrollMessageListToBottom(listEl); // true at the bottomScroll control
scrollMessageListToBottom takes the animation, wait, ignoreEscapes, preserveScrollPosition and duration options; stopMessageListScroll stops following.
none
How do I add a page?
Run `elements create page <name>`. It writes the route, the template and a stylesheet.
And a test?
`elements create test <path>` scaffolds one next to the page. `elements test` runs them all.
Does it hot reload?
Yes: every save reloads the browser that shows the app.
Where do shared templates go?
In `app/shared/templates/<name>/`, imported extensionless.
Code
<MessageList initial="instant" resize="instant">...</MessageList> <!-- no smooth scroll -->
await scrollMessageListToBottom(listEl, { preserveScrollPosition: true }); // false if scrolled up
await scrollMessageListToBottom(listEl, { wait: 300, animation: "instant" });
await scrollMessageListToBottom(listEl, { ignoreEscapes: true }); // scrolling up does not stop it
await scrollMessageListToBottom(listEl, { duration: 500 }); // follows 500ms more, then resolves
stopMessageListScroll(listEl); // stop following, like scrolling upSpring scrolling
resize takes a spring (damping, stiffness, mass) instead of the native smooth scroll, easing into a bottom that keeps moving; contentClass and contentAttrs reach the content wrapper.
How do I add a page?
Run `elements create page <name>`. It writes the route, the template and a stylesheet.
And a test?
`elements create test <path>` scaffolds one next to the page. `elements test` runs them all.
Does it hot reload?
Yes: every save reloads the browser that shows the app.
New messages ease in on a spring.
Code
<!-- use-stick-to-bottom's spring: damping 0.7, stiffness 0.05, mass 1.25 -->
<MessageList resize={{ damping: 0.7, stiffness: 0.05, mass: 1.25 }}
contentClass="chat-content"
contentAttrs={{ "aria-describedby": "chat-help" }}>...</MessageList>Stop at the newest message
targetScrollTop moves the bottom: here it is the top of the latest reply, so a long answer reads from its start.
How do I add a page?
Run elements create page <name>. It writes the route, the template and a stylesheet.
And a test?
elements create test <path> scaffolds one next to the page. elements test runs them all.
Code
<MessageList targetScrollTop={(target, { scrollElement, contentElement }) => {
let last = contentElement.lastElementChild;
return last ? Math.min(target, last.getBoundingClientRect().top
- scrollElement.getBoundingClientRect().top + scrollElement.scrollTop - 12) : target;
}}>...</MessageList>Download with a formatter
formatMessage shapes each line of the saved Markdown.
How do I add a page?
Run `elements create page <name>`. It writes the route, the template and a stylesheet.
And a test?
`elements create test <path>` scaffolds one next to the page. `elements test` runs them all.
Code
<MessageListDownload slot="overlay" messages={messages} filename="support-chat.md"
formatMessage={(m, i) => `${i + 1}. [${m.role}] ${m.text}`} />