MessageBubble
One chat message, yours on the right or theirs on the left, with the time and sent, delivered, read or failed ticks.
Demo
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/message_bubble.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/message_bubble.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/message_bubble.qwe
view MessageBubbleDemo() = <div.demo-stack style="gap: 4px"> <MessageBubble tone="system" time="Today">Ana Silva joined the chat</MessageBubble> <MessageBubble id="message-bubble-demo-theirs" name="Ana Silva" avatar="https://i.pravatar.cc/80?img=47" time="09:41" datetime="2026-10-28T09:41:00Z" last={false}>Morning! Any luck with that test?</MessageBubble> <MessageBubble name="Ana Silva" avatar="https://i.pravatar.cc/80?img=47" time="09:41" first={false}>I can pair on it after lunchif that helps.</MessageBubble> <MessageBubble id="message-bubble-demo-mine" mine={true} time="09:42" datetime="2026-10-28T09:42:00Z" status="read">Fixed it: a path with a space in it.</MessageBubble> <MessageBubble mine={true} time="09:42" status="delivered">Shipping now 🚀</MessageBubble> <MessageBubble name="Ben Okafor" time="09:43">Nice one.</MessageBubble> <MessageBubble mine={true} time="09:44" status="sent">Thanks!</MessageBubble> <MessageBubble mine={true} time="09:44" status="sending">And the release notes are up.</MessageBubble> <MessageBubble id="message-bubble-demo-failed" mine={true} time="09:45" status="failed">This one did not go.</MessageBubble> </div>Source
ui/message_bubble.qwe
// MessageBubble: one message in a chat: yours on the right in the accent colour,// theirs on the left, with the time under it and, on yours, a tick for sent, two for// delivered, two in colour for read. (ChatThread lays out a whole conversation.)//// <MessageBubble name="Ana Silva" time="09:41">Morning! Any luck with that test?</MessageBubble>// <MessageBubble mine={true} time="09:42" status="read">Fixed it: a path with a space.</MessageBubble>// <MessageBubble mine={true} status="failed" time="09:43">This one did not go.</MessageBubble>//// children: the message (line breaks kept). mine: yours, on the right. name: whose it is,// shown above theirs and told to screen readers; avatar: their picture (else initials)// beside it. time: when, as it should read; datetime: the same in ISO form.// status, on yours: "sending" (a clock), "sent" (one tick), "delivered" (two),// "read" (two, in colour) or "failed" (a red mark); each is said to screen readers.// tone: "neutral" (default) or "system" (a centred grey note, "Ana joined").// first, last: false drops the rounded corner on the side of the group it joins,// for messages in a row from one person.import ui/theme.qwe use ui_clslet message_bubble_css = css`.ui-message-bubble { display: flex; align-items: flex-end; gap: 8px; min-width: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-message-bubble-mine { flex-direction: row-reverse; }.ui-message-bubble-avatar { display: grid; place-items: center; flex: none; width: 30px; height: 30px; border-radius: 50%; overflow: hidden; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 12px; font-weight: 650; user-select: none;}.ui-message-bubble-avatar img { width: 100%; height: 100%; object-fit: cover; }.ui-message-bubble-body { display: grid; justify-items: start; min-width: 0; max-width: min(75%, 560px); }.ui-message-bubble-mine .ui-message-bubble-body { justify-items: end; }.ui-message-bubble-name { margin: 0 0 2px 12px; color: var(--ui-muted); font-size: 12px; font-weight: 600; }.ui-message-bubble-text { padding: 8px 12px; border-radius: 16px; background: var(--ui-subtle); color: var(--ui-fg); white-space: pre-wrap; overflow-wrap: anywhere; }.ui-message-bubble-mine .ui-message-bubble-text { background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-message-bubble-theirs.ui-message-bubble-joins-up .ui-message-bubble-text { border-top-left-radius: 5px; }.ui-message-bubble-theirs.ui-message-bubble-joins-down .ui-message-bubble-text { border-bottom-left-radius: 5px; }.ui-message-bubble-mine.ui-message-bubble-joins-up .ui-message-bubble-text { border-top-right-radius: 5px; }.ui-message-bubble-mine.ui-message-bubble-joins-down .ui-message-bubble-text { border-bottom-right-radius: 5px; }.ui-message-bubble-meta { display: inline-flex; align-items: center; gap: 4px; margin: 3px 12px 0; color: var(--ui-muted); font-size: 11px; }.ui-message-bubble-ticks { width: 16px; height: 12px; }.ui-message-bubble-read .ui-message-bubble-ticks { color: var(--ui-accent); }.ui-message-bubble-failed .ui-message-bubble-meta { color: var(--ui-danger); }.ui-message-bubble-system { justify-content: center; }.ui-message-bubble-system .ui-message-bubble-body { justify-items: center; max-width: 100%; }.ui-message-bubble-system .ui-message-bubble-text { padding: 3px 10px; border-radius: 999px; background: transparent; color: var(--ui-muted); font-size: 12px; }`// Up to two initials of a name: "Ana Silva" is "AS".fn message_bubble_initials(name: str) -> str = upper(join(split(trim(name), " ") | filter((w) => w != "") | map((w) => slice(w, 0, 1)) | slice(0, 2), ""))view MessageBubble(mine: bool = false, name: str? = null, avatar: str? = null, time: str? = null, datetime: str? = null, status: str? = null, tone: str = "neutral", first: bool = true, last: bool = true, id: str? = null, class: str? = null, children) { style global message_bubble_css let system = tone == "system" <div class={ui_cls(["ui-message-bubble", system ? "ui-message-bubble-system" : (mine ? "ui-message-bubble-mine" : "ui-message-bubble-theirs"), !first && "ui-message-bubble-joins-up", !last && "ui-message-bubble-joins-down", status != null && `ui-message-bubble-${status}`, class])} id={id}> {!mine && !system && name != null && <span class={ui_cls(["ui-message-bubble-avatar", !last && "ui-sr"])} aria-hidden="true"> {avatar != null ? <img src={avatar} alt="" loading="lazy"/> : message_bubble_initials(name)} </span>} <div.ui-message-bubble-body> {!mine && !system && name != null && (first ? <div.ui-message-bubble-name>{name}</div> : <span.ui-sr>{name}:</span>)} {mine && <span.ui-sr>You:</span>} <div.ui-message-bubble-text>{children}</div> {(time != null || (mine && status != null)) && <span.ui-message-bubble-meta> {time != null && <time datetime={datetime}>{time}</time>} {mine && status != null && match status { "sending" => <svg.ui-message-bubble-ticks viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Sending"><circle cx="8" cy="6" r="5"/><path d="M8 3.5V6l1.8 1.2"/></svg> "sent" => <svg.ui-message-bubble-ticks viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Sent"><path d="M3 6.5l3 3 6-7"/></svg> "delivered" => <svg.ui-message-bubble-ticks viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Delivered"><path d="M1 6.5l3 3 6-7"/><path d="M6 6.5l3 3 6-7"/></svg> "read" => <svg.ui-message-bubble-ticks viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Read"><path d="M1 6.5l3 3 6-7"/><path d="M6 6.5l3 3 6-7"/></svg> "failed" => <svg.ui-message-bubble-ticks viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="Not sent"><circle cx="8" cy="6" r="5"/><path d="M8 3.5v3M8 8.5v.01"/></svg> _ => null }} {status == "failed" && <span>Not sent</span>} </span>} </div> </div>}Related
ChatInputA chat message box that grows as you type, sends on Enter and breaks the line on Shift+Enter; a plain form in a page.6 views · 6 downloadsTypingIndicator"Ana is typing…" with pulsing dots, naming one, two or more people, and nothing at all when nobody is.6 views · 6 downloadsChatWidgetA chat button in the corner that opens a small chat panel, with replies from your server and a count for unseen ones.4 views · 7 downloadsChatThreadA conversation: grouped messages with avatars, days, a typing indicator, and a box where Enter sends.4 views · 4 downloadsCommentOne comment: avatar, name, role, time, text, Like and Reply, with its replies nested inside it.6 views · 8 downloadsCommentThreadNested comments with Like and Reply, a reply box that opens under the comment answered, and branches that fold.5 views · 5 downloads
Thread
Thread 0 posts
On MessageBubble: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.