Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Persistent threaded-discussion container. Decorates author-rendered
<comment-thread> is the container primitive for a persistent, threaded discussion. Authors render comments as <article data-comment> children with metadata attributes; the component decorates each one with an author header, relative timestamp, action toolbar (Reply / Edit / Delete), and threaded indentation via data-parent.
The component is presentational with respect to persistence: action clicks emit events, and authors call addComment / updateComment / removeComment after the server confirms.
| Use this | When |
|---|---|
<comment-thread> | Threaded, persistent discussion attached to an item. Comments have authors + timestamps + replies + reactions + edit/delete. |
<comment-wc> | Single inline action button used by <selection-menu> for adding a comment to selected text. Different role — we keep both. |
<comment-box> | The reply / new-comment form. <comment-thread> consumes it as the reply-form template. |
<chat-thread> | Real-time chat (sender-grouped, ephemeral). Different interaction model from threaded comments. |
Render comments as direct <article data-comment> children. Put the rendered body in <div data-comment-body>. Provide a <template data-reply-form> that wraps a <comment-box> — the component clones it under whichever comment is being replied to.
<comment-thread aria-label="Comments on issue 482"> <article data-comment id="c1" data-author="Ada Lovelace" data-time="2026-05-14T10:00:00Z" data-mine> <div data-comment-body>Looks great — ship it.</div> <reaction-bar> <button data-reaction="thumbsup" data-count="3" data-mine>👍</button> <template data-palette> <button data-reaction="thumbsup">👍</button> <button data-reaction="heart">❤️</button> <button data-reaction="rocket">🚀</button> </template> </reaction-bar> </article> <article data-comment id="c2" data-author="Bob" data-time="2026-05-14T11:00:00Z" data-parent="c1"> <div data-comment-body>Agreed!</div> </article> <template data-reply-form> <comment-box submit-label="Reply" data-show-cancel></comment-box> </template></comment-thread>
| Attribute | Description |
|---|---|
id | Stable comment id (used in events + parent refs). |
data-author | Display name. |
data-author-href | Optional link to the author's profile. |
data-author-avatar | Optional avatar URL (renders <user-avatar>). |
data-time | ISO-8601 timestamp; rendered as relative ("2 hours ago") with absolute on hover. |
data-mine | Current user authored this — enables Edit / Delete actions and accent styling. |
data-parent | Id of the parent comment for nested replies (drives indent depth). |
data-edited | Optional ISO timestamp; renders an "(edited)" badge with the absolute time on hover. |
Hook the events; call back via the imperative API.
const thread = document.querySelector('comment-thread'); thread.addEventListener('comment-thread:reply', async (e) => { const { parentId, value } = e.detail; const saved = await api.postComment({ parentId, body: value }); thread.addComment({ id: saved.id, author: saved.author, time: saved.time, body: saved.bodyHtml, parentId, mine: true, });}); thread.addEventListener('comment-thread:edit-request', async (e) => { const { commentId, value } = e.detail; // Render your own inline editor, then on save: const next = await showEditor(value); const saved = await api.updateComment(commentId, next); thread.updateComment(commentId, { body: saved.bodyHtml, edited: saved.editedAt });}); thread.addEventListener('comment-thread:delete-request', async (e) => { if (!confirm('Delete this comment?')) return; await api.deleteComment(e.detail.commentId); thread.removeComment(e.detail.commentId);});
Per-comment reactions bubble up as reaction-bar:toggle events — the thread doesn't intercept them. Wire them once at the thread level.
Set data-disabled on the thread to hide all action toolbars (no Reply / Edit / Delete). Reactions still render but stay clickable unless the inner <reaction-bar> elements also carry data-disabled.
<comment-thread data-disabled aria-label="Archived discussion"> <article data-comment id="c1" data-author="Ada" data-time="2025-01-01T00:00:00Z"> <div data-comment-body>Original launch announcement.</div> </article></comment-thread>
role="region" with the host's aria-label ("Comments" by default).<article> role; nested replies get aria-level matching their depth.<time datetime> with the absolute timestamp in title; component refreshes the relative text every 60s.<comment-box> handles its own keyboard.)| Attribute | Type | Default | Description |
|---|---|---|---|
aria-label | string | Comments | Region label. |
data-disabled | boolean | false | Read-only mode — hides the action toolbar on every comment. |
| Event | Bubbles | Detail |
|---|---|---|
comment-thread:reply | yes | { parentId, value } |
comment-thread:edit-request | yes | { commentId, value } |
comment-thread:delete-request | yes | { commentId } |
Reactions bubble naturally via reaction-bar:toggle from each per-comment <reaction-bar>.
| Method | Description |
|---|---|
addComment(data) | Insert a new comment. data: { id, author, time, body, parentId?, mine?, authorHref?, authorAvatar?, edited? }. |
updateComment(id, patch) | Patch a comment. patch: { body?, author?, edited? }. |
removeComment(id) | Remove a comment from the thread. |
<comment-box> — the reply form (composed via the <template data-reply-form>).<comment-wc> — single inline action used by <selection-menu>. Different role; both ship.<reaction-bar> — per-comment emoji reactions.<markdown-viewer> — for rendering the comment body server-side or client-side.