Chat
tier 2 — classesA conversation: messages in bubbles, mine on the end and theirs on the start, with the day, the author, the time, what happened to the message and how people reacted to it — the composer under it, and the launcher that keeps a chat one tap away on every page. Every piece of state is the app's; the two things that need the platform use it rather than a script.
role="log" with aria-live="polite" on the .chat: a
message that arrives is announced once, after whatever the reader is doing, and the
history is one region to navigate. Give it an aria-label naming the
conversation. Every icon — a status check, a reaction, an action — carries a
name; the emoji in a reaction is not one.
A conversation
.chat-message is avatar and .chat-body; --mine puts
it on the end with no avatar, --continued is the same author's next message
and drops the .chat-meta above it, so a run reads as one turn. The
.chat-bubble is on the neutral surface for theirs and the brand tint for mine,
both in the page's own text colour. .chat-day and .chat-unread are
lines across the thread; .chat-status is what happened to my message —
one check sent, two delivered, --read in the brand — and
.chat-bubble--failed with .chat-status--failed and a retry is one
that did not go. .chat-typing is the three dots, with the words for a screen
reader beside them, visually hidden.
<div class="card" style="max-width:560px">
<div class="card-body">
<div class="chat" role="log" aria-live="polite" aria-label="Chat with Alex Fischer">
<div class="chat-day">Yesterday</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-12T17:52">17:52</time></div>
<div class="chat-bubble">Can you reroute ORD-4209 to EU-West? Hamburg has no capacity until Thursday.</div>
</div>
</div>
<div class="chat-message chat-message--continued">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-12T17:52">17:52</time></div>
<div class="chat-bubble">The customer is fine with Friday.</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">Done — it leaves Rotterdam at 12:20 tomorrow.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-12T18:03">18:03</time><span class="chat-edited">edited</span><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
</div>
<div class="chat-day">Today</div>
<div class="chat-unread">New messages</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:41">09:41</time></div>
<div class="chat-bubble">Thanks. Can you also confirm the label was reprinted?</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble chat-bubble--failed">Yes, both labels — the old one is void.</div>
<div class="chat-meta">
<i class="ri-error-warning-line chat-status chat-status--failed" aria-label="Not sent" role="img"></i>
<span>Not sent</span>
<button class="btn-bare btn-bare--link" type="button">Retry</button>
</div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-bubble"><span class="chat-typing" aria-hidden="true"><i></i><i></i><i></i></span><span class="visually-hidden">Alex Fischer is typing</span></div>
</div>
</div>
</div>
</div>
</div>
A group
The same markup with more people: every message carries its avatar and
.chat-author, a .chat-system line tells what the app did, a
.chat-mention names someone, a .chat-quote at the top of a bubble
is the message it answers, and .chat-seen after the last message is who has
read up to here. Colour the avatars with .avatar--viz-1 to
--viz-6, so four people are four colours and none of them is a semantic one.
Dispatch — EU-Central
4 members<div class="card" style="max-width:600px">
<div class="card-head"><h3>Dispatch — EU-Central</h3><span class="badge">4 members</span></div>
<div class="card-body">
<div class="chat" role="log" aria-live="polite" aria-label="Dispatch — EU-Central">
<div class="chat-system">Priya Nair added Tomás Ferreira</div>
<div class="chat-message">
<span class="avatar avatar-sm avatar--viz-2">PN</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Priya Nair</span><time class="chat-time" datetime="2026-09-13T08:12">08:12</time></div>
<div class="chat-bubble">Carrier capacity in Hamburg is exhausted until Thursday. <a class="chat-mention" href="#">@Alex</a>, can your team take the EU-West reroutes?</div>
<div class="chat-reactions">
<button class="chat-reaction" type="button" aria-pressed="true" aria-label="Thumbs up, 3, including you">👍 3</button>
<button class="chat-reaction" type="button" aria-pressed="false" aria-label="Eyes, 1">👀 1</button>
<button class="chat-reaction" type="button" aria-label="Add a reaction"><i class="ri-emotion-line"></i></button>
</div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T08:15">08:15</time></div>
<div class="chat-bubble">
<span class="chat-quote"><strong>Priya Nair</strong>Carrier capacity in Hamburg is exhausted until Thursday…</span>
Yes — send them over, we have two slots on Friday's 12:20.
</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">I will reroute ORD-4209, 4213 and 4250 now and post the labels here.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T08:16">08:16</time><i class="ri-check-double-line chat-status" aria-label="Delivered" role="img"></i></div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm avatar--viz-4">TF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Tomás Ferreira</span><time class="chat-time" datetime="2026-09-13T08:20">08:20</time></div>
<div class="chat-bubble">4213 has a changed address — hold that one until the customer confirms.</div>
<div class="chat-reactions">
<button class="chat-reaction" type="button" aria-pressed="false" aria-label="Check, 2">✅ 2</button>
<button class="chat-reaction" type="button" aria-label="Add a reaction"><i class="ri-emotion-line"></i></button>
</div>
</div>
</div>
<div class="chat-seen">
Seen by
<span class="avatar-group">
<span class="avatar avatar-sm avatar--viz-2">PN</span>
<span class="avatar avatar-sm">AF</span>
</span>
</div>
</div>
</div>
</div>
Reactions
Live — open the picker. .chat-reactions under a bubble
is a pill per emoji with its count, aria-pressed="true" on the ones that are
mine and an aria-label that says the emoji's name and the count — a
screen reader does not read “👍 3” as anything. The last pill adds one: it opens
a .popover holding a .chat-emoji-grid of
.chat-emoji buttons, each named. Which emoji the picker offers is the app's
list; the platform's own picker is a keyboard away and needs no help.
<div class="sedna-col sedna-gap-3" style="max-width:560px">
<div class="chat-reactions">
<button class="chat-reaction" type="button" aria-pressed="true" aria-label="Thumbs up, 3, including you">👍 3</button>
<button class="chat-reaction" type="button" aria-pressed="false" aria-label="Party, 2">🎉 2</button>
<button class="chat-reaction" type="button" aria-pressed="false" aria-label="Eyes, 1">👀 1</button>
<button class="chat-reaction" type="button" aria-label="Add a reaction" popovertarget="ex-emoji"><i class="ri-emotion-line"></i></button>
</div>
<div class="popover" id="ex-emoji" popover aria-label="Pick a reaction">
<div class="chat-emoji-grid">
<button class="chat-emoji" type="button" aria-label="Thumbs up">👍</button>
<button class="chat-emoji" type="button" aria-label="Heart">❤️</button>
<button class="chat-emoji" type="button" aria-label="Laughing">😂</button>
<button class="chat-emoji" type="button" aria-label="Surprised">😮</button>
<button class="chat-emoji" type="button" aria-label="Sad">😢</button>
<button class="chat-emoji" type="button" aria-label="Party">🎉</button>
<button class="chat-emoji" type="button" aria-label="Eyes">👀</button>
<button class="chat-emoji" type="button" aria-label="Check">✅</button>
<button class="chat-emoji" type="button" aria-label="Fire">🔥</button>
<button class="chat-emoji" type="button" aria-label="Thinking">🤔</button>
<button class="chat-emoji" type="button" aria-label="Clap">👏</button>
<button class="chat-emoji" type="button" aria-label="Pray">🙏</button>
<button class="chat-emoji" type="button" aria-label="Rocket">🚀</button>
<button class="chat-emoji" type="button" aria-label="Hundred">💯</button>
<button class="chat-emoji" type="button" aria-label="Warning">⚠️</button>
<button class="chat-emoji" type="button" aria-label="Plus one">➕</button>
</div>
</div>
</div>
Actions on a message
Live — hover a message, or tab into it. .chat-actions
is a row of ghost icon buttons in the gap above the bubble's far corner, shown on hover
and whenever anything in the message has focus, so the keyboard reaches it. React and
reply are buttons; more is a .menu with
data-menu-toggle, and the destructive item is
.menu-item--danger. On a phone, where there is no hover, a long press on the
bubble is the app's way in — it opens the same menu.
<div class="card" style="max-width:560px">
<div class="card-body">
<div class="chat" role="log" aria-label="Chat with Alex Fischer">
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:41">09:41</time></div>
<div class="chat-bubble">Thanks. Can you also confirm the label was reprinted?</div>
</div>
<div class="chat-actions" role="toolbar" aria-label="Message actions">
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="React"><i class="ri-emotion-line"></i></button>
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Reply"><i class="ri-reply-line"></i></button>
<span class="menu-anchor">
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="More" aria-expanded="false" data-menu-toggle><i class="ri-more-2-fill"></i></button>
<div class="menu" hidden>
<button class="menu-item" type="button"><i class="ri-file-copy-line"></i> Copy text</button>
<button class="menu-item" type="button"><i class="ri-pushpin-line"></i> Pin in this chat</button>
<button class="menu-item" type="button"><i class="ri-flag-line"></i> Report</button>
</div>
</span>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">Yes, both labels — the old one is void.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T09:44">09:44</time><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
<div class="chat-actions" role="toolbar" aria-label="Message actions">
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="React"><i class="ri-emotion-line"></i></button>
<span class="menu-anchor">
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="More" aria-expanded="false" data-menu-toggle><i class="ri-more-2-fill"></i></button>
<div class="menu menu--start" hidden>
<button class="menu-item" type="button"><i class="ri-pencil-line"></i> Edit</button>
<button class="menu-item" type="button"><i class="ri-file-copy-line"></i> Copy text</button>
<button class="menu-item menu-item--danger" type="button"><i class="ri-delete-bin-line"></i> Delete for everyone</button>
</div>
</span>
</div>
</div>
</div>
</div>
</div>
The composer
.chat-composer is what is being answered, what is attached, the field and the
send. .chat-composer-reply quotes the message being replied to with a way to
stop; .chat-composer-files is the pending attachments as
chips, each removable; the .chat-composer-row holds the
.form-input textarea between attach, emoji and send. Enter sends and
Shift+Enter breaks a line — the app's handler, because the library does not know
which the app wants. The field grows with the text from its rows to about
six lines and scrolls after that (field-sizing: content; a browser without
it keeps the rows). .chat-closed takes the composer's place
when nothing can be sent any more.
<div class="card" style="max-width:560px">
<div class="card-body">
<form class="chat-composer">
<div class="chat-composer-reply">
<i class="ri-reply-line"></i>
<span><strong>Alex Fischer</strong> Can you also confirm the label was reprinted?</span>
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Stop replying"><i class="ri-close-line"></i></button>
</div>
<div class="chat-composer-files">
<span class="chip"><i class="ri-file-pdf-2-line"></i> label-ORD-4209.pdf <button class="chip-dismiss" type="button" aria-label="Remove label-ORD-4209.pdf"><i class="ri-close-line"></i></button></span>
</div>
<div class="chat-composer-row">
<button class="btn btn-ghost btn-icon" type="button" aria-label="Attach a file"><i class="ri-attachment-2"></i></button>
<textarea class="form-input" rows="1" placeholder="Message Alex Fischer" aria-label="Message">Both labels are attached — the old one is void.</textarea>
<button class="btn btn-ghost btn-icon" type="button" aria-label="Insert an emoji"><i class="ri-emotion-line"></i></button>
<button class="btn btn-primary btn-icon" type="submit" aria-label="Send"><i class="ri-send-plane-2-line"></i></button>
</div>
</form>
</div>
</div>
Quick replies, and a closed conversation
.chat-quick-replies under a message is a row of chips
that are buttons — the answers a bot or an assistant offers, each sending itself as
the reply; the app removes the row once one is taken. .chat-closed stands
where the composer was when the conversation cannot continue, with the way to a new one.
<div class="card" style="max-width:560px">
<div class="card-body">
<div class="chat" role="log" aria-live="polite" aria-label="Support">
<div class="chat-message">
<span class="avatar avatar-sm avatar--muted"><i class="ri-robot-2-line"></i></span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Support</span><time class="chat-time" datetime="2026-09-12T15:30">15:30</time></div>
<div class="chat-bubble">Your reservation rsv-20419 was approved. Is there anything else?</div>
<div class="chat-quick-replies">
<button class="chip" type="button">Change the dates</button>
<button class="chip" type="button">Add a colleague</button>
<button class="chip" type="button">No, thanks</button>
</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">No, thanks</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-12T15:31">15:31</time><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
</div>
<div class="chat-system">The conversation was closed</div>
</div>
</div>
<div class="chat-closed">
<i class="ri-lock-line"></i>
This conversation was closed on 12 September.
<a href="#">Start a new one</a>
</div>
</div>
Files and pictures
A file in a bubble is the .file-item every other file
list uses — an <a> with the .file-icon, the name and
the size, so it downloads like any link, on the bubble's own surface — and
.chat-bubble--media is a bubble that is a picture or a <video>,
filled to its corners with the caption in the next bubble. Give the image its width and
height, or the thread jumps as it loads.
<div class="card" style="max-width:560px">
<div class="card-body">
<div class="chat" role="log" aria-label="Chat with Alex Fischer">
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">
<a class="file-item" href="#" download>
<span class="file-icon file-icon--sm file-icon--pdf"><i class="ri-file-pdf-2-line"></i></span>
<span class="file-item-name">label-ORD-4209.pdf</span>
<span class="file-item-size">84 KB</span>
</a>
</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T09:46">09:46</time><i class="ri-check-line chat-status" aria-label="Sent" role="img"></i></div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:52">09:52</time></div>
<div class="chat-bubble chat-bubble--media">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=480&h=300&fit=crop&q=70&auto=format"
width="480" height="300" alt="A warehouse floor stacked with yellow crates, staff working in the aisle" loading="lazy" />
</div>
<div class="chat-bubble">Reprinted and back on the floor. Photo by Adrian Sulyok on Unsplash.</div>
</div>
</div>
</div>
</div>
</div>
Pictures, and looking at one large
Live — open a picture. Several pictures in one message are a
.gallery--tight inside a media bubble, two across, each a
.media-open button around a square
.media-frame that opens the .lightbox —
a modal dialog with the picture as large as the screen allows, the sender and time in its
meta line, and the way to the next one. A file travels beside them as a
.file-item. In the composer, a picture waiting to be sent is a
.file-thumb with its remove button over the corner, next to the
chips for the other files.
<div class="card" style="max-width:560px">
<div class="card-body">
<div class="chat" role="log" aria-label="Chat with Alex Fischer">
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:52">09:52</time></div>
<div class="chat-bubble chat-bubble--media">
<div class="gallery gallery--tight">
<button class="media-open" type="button" aria-label="Open the photo of the racking bay"
onclick="document.getElementById('lb-chat-1').showModal()">
<span class="media-frame media-frame--square media-frame--cover">
<img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy" alt="A warehouse aisle of tall pallet racking" />
</span>
</button>
<button class="media-open" type="button" aria-label="Open the photo of the crates"
onclick="document.getElementById('lb-chat-2').showModal()">
<span class="media-frame media-frame--square media-frame--cover">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy" alt="A warehouse floor stacked with yellow crates" />
</span>
</button>
</div>
</div>
<div class="chat-bubble">
Reprinted and on the belt — both bays. Photos by CHUTTERSNAP and Adrian Sulyok on Unsplash.
<a class="file-item sedna-mt-2" href="#" download>
<span class="file-icon file-icon--sm file-icon--pdf"><i class="ri-file-pdf-2-line"></i></span>
<span class="file-item-name">pod-ord-4209.pdf</span>
<span class="file-item-size">84 KB</span>
</a>
</div>
</div>
</div>
</div>
<form class="chat-composer">
<div class="chat-composer-files">
<span class="file-thumb">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=96&h=96&fit=crop&q=70&auto=format" width="48" height="48" alt="The crates on the floor, attached" />
<button class="chip-dismiss" type="button" aria-label="Remove the photo"><i class="ri-close-line"></i></button>
</span>
<span class="chip"><i class="ri-file-excel-2-line"></i> stock-check.xlsx <button class="chip-dismiss" type="button" aria-label="Remove stock-check.xlsx"><i class="ri-close-line"></i></button></span>
</div>
<div class="chat-composer-row">
<button class="btn btn-ghost btn-icon" type="button" aria-label="Attach a file"><i class="ri-attachment-2"></i></button>
<textarea class="form-input" rows="1" placeholder="Message Alex Fischer" aria-label="Message">Stock check from this morning, and the belt right now.</textarea>
<button class="btn btn-primary btn-icon" type="submit" aria-label="Send"><i class="ri-send-plane-2-line"></i></button>
</div>
</form>
</div>
</div>
<dialog class="lightbox" id="lb-chat-1" aria-labelledby="lb-chat-1-title">
<div class="lightbox-head">
<strong class="lightbox-title" id="lb-chat-1-title">racking-bay-4209.jpg</strong>
<span class="lightbox-meta">1 of 2 · Alex Fischer, 09:52</span>
<div class="lightbox-actions">
<form method="dialog"><button class="btn btn-ghost btn-icon" aria-label="Close"><i class="ri-close-line"></i></button></form>
</div>
</div>
<div class="lightbox-stage">
<img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=1600&q=80&auto=format" alt="A warehouse aisle of tall pallet racking" />
</div>
<button class="lightbox-prev" type="button" aria-label="Previous" disabled><i class="ri-arrow-left-s-line"></i></button>
<button class="lightbox-next" type="button" aria-label="Next"
onclick="document.getElementById('lb-chat-1').close(); document.getElementById('lb-chat-2').showModal()"><i class="ri-arrow-right-s-line"></i></button>
<p class="lightbox-caption">Photo by CHUTTERSNAP on Unsplash.</p>
</dialog>
<dialog class="lightbox" id="lb-chat-2" aria-labelledby="lb-chat-2-title">
<div class="lightbox-head">
<strong class="lightbox-title" id="lb-chat-2-title">crates-4209.jpg</strong>
<span class="lightbox-meta">2 of 2 · Alex Fischer, 09:52</span>
<div class="lightbox-actions">
<form method="dialog"><button class="btn btn-ghost btn-icon" aria-label="Close"><i class="ri-close-line"></i></button></form>
</div>
</div>
<div class="lightbox-stage">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=1600&q=80&auto=format" alt="A warehouse floor stacked with yellow crates" />
</div>
<button class="lightbox-prev" type="button" aria-label="Previous"
onclick="document.getElementById('lb-chat-2').close(); document.getElementById('lb-chat-1').showModal()"><i class="ri-arrow-left-s-line"></i></button>
<button class="lightbox-next" type="button" aria-label="Next" disabled><i class="ri-arrow-right-s-line"></i></button>
<p class="lightbox-caption">Photo by Adrian Sulyok on Unsplash.</p>
</dialog>
What a bubble holds
.chat-forwarded above the text says where a message came from;
.chat-bubble--deleted keeps a taken-back message's place and says so. A
<pre>, a <code> and a list fit the bubble and take
its ink, and so does a .markdown-body inside it
— which is how an assistant's answer, rendered from Markdown, reads as a message
rather than a document.
<div class="card" style="max-width:600px">
<div class="card-body">
<div class="chat" role="log" aria-label="Chat with Alex Fischer">
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T10:02">10:02</time></div>
<div class="chat-bubble">
<span class="chat-forwarded"><i class="ri-share-forward-line"></i> Forwarded from Dispatch — EU-Central</span>
Cut-off for Friday's 12:20 is Thursday 16:00 — anything later goes Monday.
</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble chat-bubble--deleted">This message was deleted.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T10:04">10:04</time></div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">
The reroute call is <code>POST /orders/4213/reroute</code> with the hub in the body:
<pre><code>{ "hub": "EU-West", "reason": "capacity" }</code></pre>
</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T10:06">10:06</time><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm avatar--muted"><i class="ri-robot-2-line"></i></span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Assistant</span><time class="chat-time" datetime="2026-09-13T10:07">10:07</time></div>
<div class="chat-bubble">
<div class="markdown-body">
<p>Three orders are affected by the Hamburg capacity hold:</p>
<ul>
<li><strong>ORD-4209</strong> — rerouted to EU-West, label reprinted</li>
<li><strong>ORD-4213</strong> — held, address change pending</li>
<li><strong>ORD-4250</strong> — rerouted to EU-West</li>
</ul>
<p>Nothing else in the queue is due before Thursday.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
A link, a voice message, an upload
A .link-preview unfurls a link the message carries — the picture, the
title, a line and the host in .link-preview-body; the app fetches the
preview. A voice message is an .audio-wave: play, the .audio-wave-bars
with each bar's height in --h and the ones already heard as
.audio-wave-heard, and the .audio-wave-time. The
<audio> element is the app's and hidden; the play button names the
length. A file still uploading is a .file-item with a
.progress under the name and the percentage in the
size column.
<div class="card" style="max-width:600px">
<div class="card-body">
<div class="chat" role="log" aria-label="Chat with Alex Fischer">
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T10:12">10:12</time></div>
<div class="chat-bubble">
The dock schedule for week 38 is up.
<a class="link-preview" href="#">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=176&h=176&fit=crop&q=70&auto=format" width="88" height="88" alt="" loading="lazy" />
<span class="link-preview-body">
<strong>Dock schedule — week 38</strong>
<span>Inbound and outbound slots for all four docks, with the Thursday cut-off marked.</span>
<small>ops.example.com</small>
</span>
</a>
</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">
<div class="audio-wave">
<button class="btn btn-sm btn-primary btn-icon" type="button" aria-label="Play the voice message, 19 seconds"><i class="ri-play-fill"></i></button>
<span class="audio-wave-bars" aria-hidden="true">
<i class="audio-wave-heard" style="--h:35%"></i><i class="audio-wave-heard" style="--h:60%"></i><i class="audio-wave-heard" style="--h:85%"></i><i class="audio-wave-heard" style="--h:50%"></i><i class="audio-wave-heard" style="--h:30%"></i><i class="audio-wave-heard" style="--h:70%"></i><i class="audio-wave-heard" style="--h:95%"></i><i class="audio-wave-heard" style="--h:55%"></i><i class="audio-wave-heard" style="--h:40%"></i>
<i style="--h:65%"></i><i style="--h:80%"></i><i style="--h:45%"></i><i style="--h:25%"></i><i style="--h:60%"></i><i style="--h:90%"></i><i style="--h:50%"></i><i style="--h:35%"></i><i style="--h:70%"></i><i style="--h:40%"></i><i style="--h:55%"></i><i style="--h:30%"></i><i style="--h:65%"></i><i style="--h:45%"></i><i style="--h:25%"></i>
</span>
<span class="audio-wave-time">0:07 / 0:19</span>
</div>
</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T10:15">10:15</time><i class="ri-check-double-line chat-status" aria-label="Delivered" role="img"></i></div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">
<span class="file-item" role="status" aria-label="Uploading stock-check.xlsx, 64 percent">
<span class="file-icon file-icon--sm file-icon--sheet"><i class="ri-file-excel-2-line"></i></span>
<span class="file-item-main">
<span class="file-item-name">stock-check.xlsx</span>
<span class="progress" aria-hidden="true"><span class="progress-bar" style="width:64%"></span></span>
</span>
<span class="file-item-size">64%</span>
</span>
</div>
<div class="chat-meta"><span>Sending…</span></div>
</div>
</div>
</div>
</div>
</div>
The launcher and the flyover
Live — it is in the corner of this page. .chat-launcher
is the round brand button in the corner, above the home indicator, with the unread count on it as
.chat-launcher-count; it is the page's one floating action where a chat is the
page's job, so it takes the FAB's corner and rung. It opens the
.chat-flyover with popovertarget: a popover="manual"
panel, so it is in the top layer and stays open while the reader works, and the close
button in its .chat-flyover-head is the way out. Inside it the thread and the
composer are the ones above; on a phone it is the whole screen.
<div class="sedna-row sedna-gap-3">
<button class="btn btn-primary" type="button" popovertarget="ex-flyover"><i class="ri-chat-3-line"></i> Open the chat</button>
<span class="text-muted text-sm">The launcher is in the corner of this page, where it lives; this opens the same flyover.</span>
</div>
<button class="chat-launcher" type="button" popovertarget="ex-flyover" aria-label="Open the chat, 2 unread">
<i class="ri-chat-3-line"></i>
<span class="chat-launcher-count" aria-hidden="true">2</span>
</button>
<div class="chat-flyover" id="ex-flyover" popover="manual" role="dialog" aria-label="Chat with Alex Fischer">
<div class="chat-flyover-head">
<span class="avatar avatar-sm">AF</span>
<strong>Alex Fischer</strong>
<span class="health-badge health-badge--live"><span class="health-dot"></span> Online</span>
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Close the chat" popovertarget="ex-flyover" popovertargetaction="hide"><i class="ri-close-line"></i></button>
</div>
<div class="chat" role="log" aria-live="polite" aria-label="Messages">
<div class="chat-day">Today</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:41">09:41</time></div>
<div class="chat-bubble">Can you also confirm the label was reprinted?</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">Yes, both labels — the old one is void.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T09:44">09:44</time><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:45">09:45</time></div>
<div class="chat-bubble">Perfect, thanks!</div>
</div>
</div>
</div>
<form class="chat-composer">
<div class="chat-composer-row">
<button class="btn btn-ghost btn-icon" type="button" aria-label="Attach a file"><i class="ri-attachment-2"></i></button>
<textarea class="form-input" rows="1" placeholder="Message Alex Fischer" aria-label="Message"></textarea>
<button class="btn btn-primary btn-icon" type="submit" aria-label="Send"><i class="ri-send-plane-2-line"></i></button>
</div>
</form>
</div>
A page of it
Live — scroll the thread. .chat-pane is the chat as
a page's whole height: give it a height — 100% inside a
.page--fill, or one of its own — and the
thread takes what the .chat-pinned bar above and the composer below leave,
scrolling on its own with each .chat-day sticking as it passes. The thread
that scrolls carries tabindex="0", so the keyboard can scroll it too.
.chat-jump floats over the thread's bottom edge for the moment the reader
has scrolled up and something arrives; the app shows it and scrolls on the click.
<div class="card" style="max-width:600px; height:460px">
<div class="chat-pane">
<div class="chat-pinned">
<i class="ri-pushpin-line"></i>
<span><strong>Pinned</strong> Cut-off for Friday's 12:20 is Thursday 16:00 — anything later goes Monday.</span>
<button class="btn btn-sm btn-ghost btn-icon" type="button" aria-label="Unpin"><i class="ri-close-line"></i></button>
</div>
<div class="chat" role="log" aria-live="polite" aria-label="Dispatch — EU-Central" tabindex="0">
<div class="chat-day">Yesterday</div>
<div class="chat-message">
<span class="avatar avatar-sm avatar--viz-2">PN</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Priya Nair</span><time class="chat-time" datetime="2026-09-12T16:40">16:40</time></div>
<div class="chat-bubble">Carrier capacity in Hamburg is exhausted until Thursday.</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">Then EU-West takes the reroutes. I will post the labels here.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-12T16:44">16:44</time><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm avatar--viz-4">TF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Tomás Ferreira</span><time class="chat-time" datetime="2026-09-12T17:02">17:02</time></div>
<div class="chat-bubble">4213 has a changed address — hold that one until the customer confirms.</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">Held. 4209 and 4250 are rerouted.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-12T17:05">17:05</time><i class="ri-check-double-line chat-status chat-status--read" aria-label="Read" role="img"></i></div>
</div>
</div>
<div class="chat-day">Today</div>
<div class="chat-message">
<span class="avatar avatar-sm">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T09:41">09:41</time></div>
<div class="chat-bubble">Customer confirmed the new address for 4213 — it can go.</div>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<div class="chat-bubble">Released. Label is on the belt.</div>
<div class="chat-meta"><time class="chat-time" datetime="2026-09-13T09:44">09:44</time><i class="ri-check-double-line chat-status" aria-label="Delivered" role="img"></i></div>
</div>
</div>
<div class="chat-message">
<span class="avatar avatar-sm avatar--viz-2">PN</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Priya Nair</span><time class="chat-time" datetime="2026-09-13T09:52">09:52</time></div>
<div class="chat-bubble">Thanks both. Hamburg is back to normal capacity from Thursday.</div>
</div>
</div>
<div class="chat-message chat-message--continued">
<span class="avatar avatar-sm avatar--viz-2">PN</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Priya Nair</span><time class="chat-time" datetime="2026-09-13T09:52">09:52</time></div>
<div class="chat-bubble">I will unpin the cut-off note then.</div>
</div>
</div>
</div>
<button class="chat-jump" type="button">2 new messages <i class="ri-arrow-down-line"></i></button>
<form class="chat-composer">
<div class="chat-composer-row">
<button class="btn btn-ghost btn-icon" type="button" aria-label="Attach a file"><i class="ri-attachment-2"></i></button>
<textarea class="form-input" rows="1" placeholder="Message Dispatch — EU-Central" aria-label="Message"></textarea>
<button class="btn btn-primary btn-icon" type="submit" aria-label="Send"><i class="ri-send-plane-2-line"></i></button>
</div>
</form>
</div>
</div>
Flat, like a channel
.chat--flat on the thread is a channel rather than a conversation: no
bubbles, every message full width on the start with the author beside the time, the row
lit on hover. Mine is not on the end here — in a channel of forty people, whose a
message is is the name. .chat-replies under a message is the thread it
started: who answered, how many, and when.
# dispatch-eu-central
41 members<div class="card" style="max-width:640px">
<div class="card-head"><h3># dispatch-eu-central</h3><span class="badge">41 members</span></div>
<div class="card-body">
<div class="chat chat--flat" role="log" aria-live="polite" aria-label="dispatch-eu-central">
<div class="chat-message">
<span class="avatar avatar--viz-2">PN</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Priya Nair</span><time class="chat-time" datetime="2026-09-13T08:12">08:12</time></div>
<div class="chat-bubble">Carrier capacity in Hamburg is exhausted until Thursday. <a class="chat-mention" href="#">@Alex</a>, can your team take the EU-West reroutes?</div>
<div class="chat-reactions">
<button class="chat-reaction" type="button" aria-pressed="true" aria-label="Thumbs up, 3, including you">👍 3</button>
<button class="chat-reaction" type="button" aria-label="Add a reaction"><i class="ri-emotion-line"></i></button>
</div>
<a class="chat-replies" href="#">
<span class="avatar-group">
<span class="avatar avatar-sm">AF</span>
<span class="avatar avatar-sm avatar--viz-4">TF</span>
</span>
<strong>3 replies</strong><span>last today at 08:20</span>
</a>
</div>
</div>
<div class="chat-message chat-message--mine">
<span class="avatar">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T08:16">08:16</time></div>
<div class="chat-bubble">Rerouting ORD-4209, 4213 and 4250 now — labels follow here.</div>
</div>
</div>
<div class="chat-message chat-message--mine chat-message--continued">
<span class="avatar">AF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Alex Fischer</span><time class="chat-time" datetime="2026-09-13T08:17">08:17</time></div>
<div class="chat-bubble">
<a class="file-item" href="#" download>
<span class="file-icon file-icon--sm file-icon--pdf"><i class="ri-file-pdf-2-line"></i></span>
<span class="file-item-name">labels-ORD-4209-4250.pdf</span>
<span class="file-item-size">212 KB</span>
</a>
</div>
</div>
</div>
<div class="chat-system">Tomás Ferreira pinned a message</div>
<div class="chat-message">
<span class="avatar avatar--viz-4">TF</span>
<div class="chat-body">
<div class="chat-meta"><span class="chat-author">Tomás Ferreira</span><time class="chat-time" datetime="2026-09-13T08:20">08:20</time></div>
<div class="chat-bubble">4213 has a changed address — hold that one until the customer confirms.</div>
</div>
</div>
</div>
</div>
</div>
While it loads
The history arriving is skeletons in the thread's own shapes: a
.skeleton-avatar at the avatar's size, a .skeleton-text for the
meta line and a .skeleton-block at a bubble's height, mine on the end. The
card is aria-busy and the words are there for a screen reader.
<div class="card" style="max-width:560px" aria-busy="true">
<div class="card-body">
<div class="chat" aria-label="Loading the conversation">
<div class="chat-message">
<span class="skeleton skeleton-avatar" style="width:22px; height:22px"></span>
<div class="chat-body">
<span class="skeleton skeleton-text" style="width:120px; margin:0"></span>
<span class="skeleton skeleton-block" style="width:280px; --skeleton-height:44px"></span>
</div>
</div>
<div class="chat-message chat-message--mine">
<div class="chat-body">
<span class="skeleton skeleton-block" style="width:200px; --skeleton-height:44px"></span>
</div>
</div>
<div class="chat-message">
<span class="skeleton skeleton-avatar" style="width:22px; height:22px"></span>
<div class="chat-body">
<span class="skeleton skeleton-text" style="width:120px; margin:0"></span>
<span class="skeleton skeleton-block" style="width:240px; --skeleton-height:64px"></span>
</div>
</div>
<span class="visually-hidden">Loading the conversation</span>
</div>
</div>
</div>
The conversations
The list of chats is a .list: an avatar, the name and the
last line, the time and an unread badge — the row that
navigates is the row that navigates everywhere else. A group is an
.avatar-pair: two of its people stacked corner to corner
inside one avatar's square, so a group's name starts where a person's does and the column
of names stays straight. An .avatar-group there would be as wide as its count.
Name the pair once, on the wrapper.
<div class="card" style="max-width:420px">
<ul class="list">
<li>
<a class="list-row" href="#" aria-current="true">
<span class="avatar">AF</span>
<span class="list-main">
<span class="list-title">Alex Fischer</span>
<span class="list-sub">Perfect, thanks!</span>
</span>
<span class="list-meta"><span>09:45</span></span>
</a>
</li>
<li>
<a class="list-row" href="#">
<span class="avatar-pair" role="img" aria-label="Priya Nair and Tomás Ferreira">
<span class="avatar avatar--viz-2" aria-hidden="true">PN</span>
<span class="avatar avatar--viz-4" aria-hidden="true">TF</span>
</span>
<span class="list-main">
<span class="list-title">Dispatch — EU-Central</span>
<span class="list-sub"><strong>Tomás:</strong> 4213 has a changed address — hold that one…</span>
</span>
<span class="list-meta"><span>08:20</span><span class="badge badge-go">3</span></span>
</a>
</li>
<li>
<a class="list-row" href="#">
<span class="avatar avatar--muted"><i class="ri-robot-2-line"></i></span>
<span class="list-main">
<span class="list-title">Workflow API</span>
<span class="list-sub">Reservation rsv-20419 was approved.</span>
</span>
<span class="list-meta"><span>Yesterday</span></span>
</a>
</li>
</ul>
</div>