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 / Message

Message

One chat message with avatar, author, time, streaming Markdown, attachments, actions and response branches.

Conversation

A user bubble at the end, an assistant reply at the start with avatar, time and actions.

Ada A
Ada2:02 PM
Can you explain how pages render?
Claude C
Claude2:02 PM
Every page renders on the server first, then attaches in the browser, so the first response is the finished HTML.
retries: 0 · liked: false · disliked: false
Code
<Message from="user" author="Ada" avatar={ada} time="2026-10-29T14:02:00Z" timeLabel="2:02 PM">
  <MessageContent>Can you explain how pages render?</MessageContent>
</Message>
<Message from="assistant" author="Claude" showAvatar time="2026-10-29T14:02:05Z" timeLabel="2:02 PM">
  <MessageContent>Every page renders on the server first, then attaches in the browser.</MessageContent>
  <MessageActions>
    <MessageAction label="Retry" tooltip="Regenerate" onclick={() => retry()}><Icon name="refresh-cw" /></MessageAction>
    <MessageAction label="Copy" tooltip="Copy" onclick={() => copy()}><Icon name="copy" /></MessageAction>
    <MessageAction label="Good response" toggle pressed={state.liked}><Icon name="thumbs-up" /></MessageAction>
    <MessageAction label="Bad response" toggle pressed={state.disliked}><Icon name="thumbs-down" /></MessageAction>
  </MessageActions>
</Message>

Usage

import { Message, MessageContent, MessageResponse, MessageActions, MessageAction } from "@elements/ui/message";

<Message from="user" author="Ada" time={m.createdAt}>
  <MessageContent>{m.text}</MessageContent>
</Message>

<Message from="assistant" author="Claude" showAvatar>
  <MessageContent>
    <MessageResponse markdown={reply.text} streaming={reply.streaming} />
  </MessageContent>
  <MessageActions>
    <MessageAction label="Retry" tooltip="Regenerate" onclick={() => retry(reply)}><Icon name="refresh-cw" /></MessageAction>
    <MessageAction label="Copy" onclick={() => copy(reply.text)}><Icon name="copy" /></MessageAction>
  </MessageActions>
</Message>

Examples

Markdown response

Headings, emphasis, lists, task lists, code with copy, tables, quotes and links.

Claude C
Claude

Setting up a page

Elements renders every page on the server first, then attaches in the browser. You need three things:

  1. A route in app/routes.ts
  2. A template.ehtml with an <html> template
  3. Optionally, a style.css
  • Route declared
  • Template written
ts
export default function route(req: Request, res: Response) {
  return new page({ title: "Hello" });
}
CommandWhat it does
elements buildBuilds and tests
elements startRuns the server

Every save hot reloads the page.

Read more in the manual. No config needed.

code copies: 0
Code
<Message from="assistant">
  <MessageContent>
    <MessageResponse markdown={reply.text} />
  </MessageContent>
</Message>

References and footnotes

Reference links and images, GFM footnotes with back links, www. autolinks, lazy quote lines and multi-backtick code spans.

Claude C
Claude

Elements renders on the server first1, then hot reloads on save2. See the manual and www.elements.dev.

Quotes keep lazy lines: a line without its marker stays in the quote.

Code spans take longer fences: a ` b. Single and double tildes strike.

  1. Every page is HTML before any script runs. ↩︎

  2. elements start watches the project. ↩︎

Code
<!-- reply.text ends with lines like [man]: https://elements.dev and [^1]: A note. -->
<MessageResponse markdown={reply.text} />

Streaming

Unfinished syntax renders closed while text streams in; a link shows as a link before its url arrives; a caret follows the last block.

Claude C
Claude

Here is a quick summary of the release:

  • Faster builds
animation: none
Code
<MessageResponse markdown={reply.text} streaming={reply.streaming} />

<!-- An unfinished link shows as plain text instead of a link with no url yet. -->
<MessageResponse markdown={reply.text} streaming={reply.streaming} linkMode="text-only" />

Branches

Several versions of one reply. Previous and next wrap; the page says where you are.

Ada A
Ada
What is the capital of France?
Claude C
Claude

The capital of France is Paris.

Claude C
Claude

Paris. It has been the capital since the 10th century, apart from a few short interruptions.

Claude C
Claude

France's capital is Paris, on the Seine, home to about two million people.

1 of 3
branch: 0 · changes: 0
Claude C
Claude
Only one version of this reply.
1 of 1
Code
<!-- count is optional: with it the server render already shows "1 of 3". -->
<MessageBranch branch={state.branch} count={versions.length} onBranchChange={(i) => log(i)}>
  <MessageBranchContent>
    <Message e:for={v of versions} e:key={(v) => v.id} from="assistant">
      <MessageContent><MessageResponse markdown={v.text} /></MessageContent>
    </Message>
  </MessageBranchContent>
  <MessageToolbar>
    <MessageBranchSelector>
      <MessageBranchPrevious />
      <MessageBranchPage branch={state.branch} count={versions.length} />
      <MessageBranchNext />
    </MessageBranchSelector>
    <MessageActions>...</MessageActions>
  </MessageToolbar>
</MessageBranch>

With attachments

Attachments above the bubble, in the grid variant.

Ada A
Ada
q3-revenue.svg q3-revenue.svg
mountains.svg mountains.svg
brief.pdf
What does this chart show?
Code
<Message from="user">
  <Attachments variant="grid">
    <Attachment e:for={f of m.files} e:key={(f) => f.id} data={f} />
  </Attachments>
  <MessageContent>What does this chart show?</MessageContent>
</Message>

System and grouped

System notices are centered; grouped follow-ups drop the header and keep the avatar column.

Ada joined the conversation
Grace G
Grace2:04 PM
Morning! Standup in ten.
Grace G
I will share the board.
Code
<Message from="system"><MessageContent>Ada joined the conversation</MessageContent></Message>
<Message from="user" author="Ada" avatar={ada} timeLabel="2:04 PM"><MessageContent>First</MessageContent></Message>
<Message from="user" author="Ada" avatar={ada} grouped><MessageContent>Second, grouped</MessageContent></Message>

Variant and align

Override the defaults: a flat user message at the start, an assistant bubble.

Ada A
Ada
Flat, at the start, like a chat room.
Claude C
Claude
A bubble from the assistant.
Code
<Message from="user" variant="flat" align="start" author="Ada">...</Message>
<Message from="assistant" variant="bubble" author="Claude" showAvatar>...</Message>

Loading

MessageLoading holds the place of a reply that has not started.

Claude C
Claude
Code
<Message from="assistant" author="Claude" showAvatar>
  <MessageContent><MessageLoading label="Claude is thinking" /></MessageContent>
</Message>

Code, table and image controls

Code is highlighted with copy and download; tables copy as Markdown, CSV or TSV and download as CSV or Markdown; images download. All are disabled while streaming.

Claude C
Claude

Highlighted, with copy and download:

python
def greet(name: str) -> str:
    # Say hello
    return f"Hello, {name}!" * 2
RegionQ3Q4
North120140
South, east98131

Revenue chart

table copies: none
Code
<MessageResponse markdown={reply.text}
                 codeBlockMaxHeight={240}
                 onTableCopy={(text, format) => log(format)} />
<!-- Turn controls off: copyCode, downloadCode, tableControls, imageDownload, highlight -->

Response options

Line numbers (noLineNumbers per block), a code theme (codeColors), raw languages with no highlighting or controls, download names, the CSV separator (auto follows the locale), urlTransform, autolink protocols, and which table controls show.

Claude C
Claude

Numbered code, saved as snippet.ts:

ts
1
2
3let a = 1;
let b = 2;
let c = a + b;
ts
let unnumbered = true;
+---+---+
| a | b |
+---+---+
ItemPrice
Tea; green3,50
Cake4

Chart from the report. Mail ada@elements.dev or see https://elements.dev.

copy errors: 0
Claude C
Claude
ItemPrice
Tea3
Cake4
Claude C
Claude
OnlyDownload
ab
cd
ef
gh
ij
Claude C
Claude
NoControls
ab
Code
<MessageResponse markdown={text}
                 lineNumbers
                 codeColors={{ keyword: "var(--accent)", number: "var(--info-ink)", background: "var(--bg-muted)" }}
                 codeBlockRawLanguages={["ascii"]}
                 codeFilename="snippet"
                 tableFilename="report"
                 csvSeparator=";"
                 urlTransform={(url, key) => key === "src" ? proxy(url) : url}
                 disableAutolinkProtocols={["mailto"]}
                 onLinkCheck={async (url) => (await checkUrl(url)).isSafe}
                 onCodeCopyError={(err) => toast(err.message)} />

<!-- Only some table controls: -->
<MessageResponse markdown={text} tableCopy={false} tableFullscreen={false} tableMaxHeight={96} />
<MessageResponse markdown={text} tableControls={false} />  <!-- no table bar at all -->

Link safety

A link asks before it opens, showing the full address with Copy link and Open link. onLinkCheck lets trusted links through.

Claude C
Claude

Open the manual (safelisted, opens at once) or an outside page (asks first). Mail ada@elements.dev or ada@elements.dev.

Claude C
Claude

With linkSafety off, this link opens directly.

checked: 0
Code
<!-- Every link asks first; onLinkCheck returns true to open at once. -->
<MessageResponse markdown={reply.text}
                 onLinkCheck={(url) => url.startsWith("https://elements.dev/")} />
<MessageResponse markdown={reply.text} linkSafety={false} />

Direction and caret

dir=auto sets each block's direction from its own text; caret=circle swaps the block caret for a dot.

Claude C
Claude

مرحبا، هذه فقرة باللغة العربية.

This paragraph is English, so it reads left to right.

Claude C
Claude

Still writing this reply

Code
<MessageResponse markdown={reply.text} dir="auto" />
<MessageResponse markdown={reply.text} streaming caret="circle" />

Safe links and images

Unsafe HTML stays text, javascript: is blocked, and prefixes limit links and images.

Claude

Links: manual, relative, elsewhere and script.

<script>alert("not run")</script> bold inert

chart

Claude

Unfinished **bold and `code

Code
<MessageResponse markdown={text}
                 allowedLinkPrefixes={["https://elements.dev"]}
                 allowedImagePrefixes={[]}
                 defaultOrigin="https://elements.dev" />

HTML in replies

Safe tags render (kbd, sub, sup, br in a table cell, details); script, event handlers and javascript: urls never do. skipHtml leaves HTML out.

Claude

Press Ctrl + C. Water is H2O; area is r2.

StepNotes
Installone
two
More detail

The Markdown inside renders too.

<script>alert(1)</script> blocked a raw link

Claude

Press Ctrl and x go.

Claude

**not Markdown** and a styled note

Indented HTML

Code
<!-- Safe tags render; everything else stays text. -->
<MessageResponse markdown={reply.text} />
<MessageResponse markdown={reply.text} skipHtml />
<MessageResponse markdown={reply.text}
                 allowedTags={{ "cite-ref": ["id", "data-source"] }}
                 literalTagContent={["cite-ref"]}
                 normalizeHtmlIndentation />

Element filters

allowedElements, disallowedElements and allowElement choose what renders; unwrapDisallowed keeps a left-out element's text.

Claude

A word, an emphasis, a link and an image .

Claude

A bold word, an emphasis, a link and an image chart.

Claude

A bold word, an emphasis, a link and an image chart.

Claude

A bold word, an emphasis, a link and an image chart.

Code
<MessageResponse markdown={reply.text} disallowedElements={["strong", "img"]} />
<MessageResponse markdown={reply.text} allowedElements={["p", "em"]} unwrapDisallowed />
<MessageResponse markdown={reply.text} allowElement={(el) => el.tagName !== "a"} unwrapDisallowed />

Repair rules, labels and icons

Each stream repair rule can be turned off and handlers added; translations relabel every control and the link dialog; icons swap the control icons.

Claude

Unfinished **bold and code ...

Claude

One chunk for the whole reply.

Claude
js
let x = 1;
ab
12

See the manual.

Code
<MessageResponse markdown={reply.text}
                 repair={{ bold: false, handlers: [{ name: "ellipsis", handle: (t) => t + " ..." }] }} />
<MessageResponse markdown={reply.text} parseMarkdownIntoBlocksFn={(md) => [md]} />
<MessageResponse markdown={reply.text}
                 translations={{ copyCode: "Copiar código", copied: "Copiado" }}
                 icons={{ copy: "clipboard", download: "arrow-down" }} />

Animated streaming

animated fades each new word in as it arrives (blur and slide-up are the other animations); words already shown stay still.

Claude C
Claude
Code
<MessageResponse markdown={reply.text} streaming={reply.streaming} animated />
<MessageResponse markdown={reply.text} streaming={reply.streaming} animated animation="blur" animationSep="char" />
Reference Message in the manual Every part, attribute, slot, key and data attribute.
Prev AttachmentNext MessageList
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026