Social

MessageBubble

One chat message, yours on the right or theirs on the left, with the time and sent, delivered, read or failed ticks.

#social#chat#no-js

Demo

Preview
Ana Silva joined the chat
Ana Silva
Morning! Any luck with that test?
Ana Silva:
I can pair on it after lunch if that helps.
You:
Fixed it: a path with a space in it.
You:
Shipping now 🚀
Ben Okafor
Nice one.
You:
Thanks!
You:
And the release notes are up.
You:
This one did not go.
Not sent

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.qwe
main.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>}
Thread
68Useful? Like it, or add to its thread below.

Thread 0 posts

On MessageBubble: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.