elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Chat / MessageList

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

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
<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 bottom

Scroll 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 up

Spring 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}`} />
Reference MessageList in the manual Every part, attribute, slot, key and data attribute.
Prev MessageNext PromptInput
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026