Skip to content

Message

One message of the conversation — who wrote it, when, and its content as plain text or as a list of segments.

  • <aparte-chat-bubble>
On this page
Open

<aparte-chat-bubble> is one message with one role, user or assistant. The transcript creates one per message in its store; you write the tag yourself only when your code draws the conversation.

A message holds either plain text (the content attribute, rendered as Markdown when a renderer is installed) or a list of segments: text, reasoning, code, tool calls, errors and views of your own. Segments win when both are set.

Open
  1. Avatar
  2. Name and time
  3. Content
  4. Actions
  5. Versions
Open

The avatar slot is empty, and hidden, until you fill it with setAvatarProvider(): initials, a picture, a component. The provider receives the role and the slot, already sized; it can return a cleanup function for a live component.

Each part of a message has its own hook instead of a slot: setBubbleShellRenderer for the whole structure, setAvatarProvider, setAttachmentRenderer, setSiblingNavRenderer, and registerSegmentRenderer for the content. The colours and spacing are CSS variables, listed below.

html
<aparte-chat-bubble streaming=""></aparte-chat-bubble>
js
el.addEventListener('aparte-attachment-preview', (e) => use(e.detail));

Installation and the framework-specific traps: React · Vue · Svelte · Angular.

<aparte-chat-bubble> · class AparteChatBubble

Attributes

AttributeDescription
data-roleThe message role, user or assistant — the one channel for it, and what the CSS keys off. (role is ARIA’s attribute; this element sets it to article itself and no longer reads a message role from it.)
contentPlain text content, for a bubble with no segments.
timestampEpoch milliseconds or a date string; a numeric string is read as milliseconds.
message-idHow streaming and the action bar address this bubble.
streamingHides the action bar and shows the caret while a reply is in flight.
nameThe display name in the header.

Methods

MethodDescription
appendToken(chunk: string): voidAppend a token chunk (for streaming)
setContent(content: string): voidSet content directly
getContent(): stringGet current content
setSegments(segments: AparteSegment[]): voidSet segments for rich content
addSegment(segment: AparteSegment): voidAdd a segment
updateSegment(segmentId: string, updates: Partial<AparteSegment>): voidUpdate a specific segment
appendToSegment(segmentId: string, content: string): voidAppend content to a segment
getSegments(): AparteSegment[]Get all segments
removeSegment(segmentId: string): voidScoped to DIRECT children on purpose. Segments are appended as direct children of the container, but a descendant query returns the first match in document order — and sanitized model markdown renders inside that same container, with data-* attributes deliberately preserved (they are inert). So a decoy data-segment-id planted in an earlier segment’s prose used to win over the real segment element. Parser ids are unguessable UUIDs, but a tool segment is tool-${toolCallId} and the MODEL chooses that id — so this was reachable, and pointing an update at a decoy left a rejected tool rendering as still-running: a spoof against the human-in-the-loop control.
setAttachments(attachments: AparteAttachment[]): voidSet attachments (chips shown above message content, user role only)
setUsage(usage: AparteUsage | null | undefined): voidSet token usage + timing for this message (assistant only). This is the precondition for the info (“i”) action, not the trigger: the button appears only if the app also declared it wants it — aparteGlobalConfig.setBubbleActions({ info: true }) — because the stats popover it opens (aparte-message-info) is the app’s, and core has none. Without usage there is nothing to show, so the button never renders either way.
setSiblings(count: number, index: number): voidUpdate the branch picker UI for tree-based navigation. The viewport calls this after a branch switch or re-render. Prev/Next clicks dispatch aparte-branch-navigate (bubbles: true) so the viewport can handle the actual tree switch.
updateMessage(updates: Partial<AparteMessage>): voidAtomic update for the message
setTranscriptBusy(busy: boolean): void

Events

EventTypeDescription
aparte-attachment-previewCustomEvent<AparteAttachmentPreviewEventDetail>An attached image was clicked, asking the app to open it full-size.
aparte-actionCustomEvent<AparteActionEventDetail>A custom action-bar button was pressed.
aparte-retryCustomEvent<AparteRetryEventDetail>Retry was pressed; the host forks the turn.
aparte-feedbackCustomEvent<AparteFeedbackEventDetail>Thumbs up or down.
aparte-message-infoCustomEvent<AparteMessageInfoEventDetail>The info affordance was pressed.
aparte-editCustomEvent<AparteEditEventDetail>An edit was saved.
aparte-branch-navigateCustomEvent<AparteBranchNavigateEventDetail>The ‹1/2› picker moved between sibling versions.
aparte-link-clickCustomEvent<AparteLinkClickEventDetail>A link in the message body is about to be followed. Cancelable: preventDefault() keeps the browser from navigating, so a host can route the link itself.

CSS variables

VariableDefaultDescription
--aparte-message-gapvar(--aparte-space-6)Gap between the avatar column and the body (the viewport reuses it between messages).
--aparte-message-padding-blockvar(--aparte-space-8)Vertical padding of one message row. (The composite --aparte-message-padding is gone: joined on :root, it froze both parts, and the narrow-container override of them never reached the row.)
--aparte-message-padding-inlinevar(--aparte-space-6)Horizontal padding of one message row.
--aparte-message-max-width800pxWidth of the centred message row.
--aparte-message-content-radiusvar(--aparte-radius-bubble)Radius of the painted content box.
--aparte-message-content-paddingvar(--aparte-space-5) var(--aparte-space-7)Padding of the USER box only; the assistant’s content is plain full-width prose.
--aparte-message-content-bg-user—Background of the user box: a wash of --aparte-primary over --aparte-surface-1, derived in theme.css so a rebrand moves it (declare it to override).
--aparte-message-content-bg-assistanttransparentBackground of the assistant box — transparent on purpose (AI-chat convention, not messaging).
--aparte-message-content-text-uservar(--aparte-text)Text colour inside the user box.
--aparte-message-content-text-assistantvar(--aparte-text)Text colour inside the assistant box.
--aparte-avatar-size32pxSquare size of the avatar slot.
--aparte-avatar-radius-ratio0.28Avatar corner radius, as a fraction of its size (the corner follows the ramp; 0.28 × 32px is the 9px it used to be).
--aparte-avatar-initials-ratio0.34Size of the initial, as a fraction of the avatar’s size.
--aparte-avatar-bg-uservar(--aparte-primary)Avatar background, user role.
--aparte-avatar-text-uservar(--aparte-text-inverse)Avatar text colour, user role.
--aparte-avatar-bg-assistantvar(--aparte-surface-3)Avatar background, assistant role.
--aparte-avatar-text-assistantvar(--aparte-text)Avatar text colour, assistant role.
--aparte-avatar-image-usernonebackground-image for the user avatar — a logo with no AvatarProvider and no JS.
--aparte-avatar-image-assistantnonebackground-image for the assistant avatar.
--aparte-avatar-image-size90%background-size for both avatar images.
--aparte-name-font-sizevar(--aparte-font-size-md)Sender name in the header.
--aparte-name-colorvar(--aparte-text)Sender name colour.
--aparte-timestamp-font-sizevar(--aparte-font-size-sm)Timestamp in the header.
--aparte-timestamp-colorvar(--aparte-text-muted)Timestamp colour.
--aparte-content-font-sizevar(--aparte-font-size-base)Body type size, applied to both the plain-content and the segments container.
--aparte-content-colorvar(--aparte-text)Body text colour.
--aparte-content-line-heightvar(--aparte-line-height-loose)Body line height.
--aparte-attachments-max-height140pxCap on the sent-attachment strip; past it the strip scrolls instead of growing.
--aparte-attachment-image-size72pxTile size in the strip. The strip re-declares the global 72px down to 40px, since these are thumbnails inside a conversation.
--aparte-thumb-radiusvar(--aparte-radius-lg)Attachment tile radius (shared with the composer’s preview tiles).
--aparte-thumb-name-color#ffffffFilename overlaid on a tile.
--aparte-thumb-name-scrim—Gradient behind that filename, so it stays legible over any image.
--aparte-thumb-name-padding14px 5px 4pxPadding of the filename overlay.
--aparte-action-bar-gapvar(--aparte-space-2)Gap between action buttons (and between the footer’s two regions).
--aparte-action-bar-btn-sizevar(--aparte-btn-size-sm)Square size of an action button (the recipe’s small step); also the footer’s reserved height.
--aparte-action-bar-btn-colorvar(--aparte-text-muted)Action icon colour at rest.
--aparte-action-bar-btn-hover-bgvar(--aparte-surface-2)Action button hover background (the branch arrows reuse it).
--aparte-action-bar-btn-hover-colorvar(--aparte-text)Action icon colour on hover.
--aparte-branch-picker-gapvar(--aparte-space-2)Gap between the arrows and the position label.
--aparte-branch-picker-btn-colorvar(--aparte-text-muted)Arrow colour at rest.
--aparte-branch-picker-btn-hover-colorvar(--aparte-text)Arrow colour on hover (a disabled arrow is dimmed instead).
--aparte-branch-picker-label-sizevar(--aparte-font-size-sm)Type size of the position label.
--aparte-branch-picker-label-colorvar(--aparte-text-muted)Colour of the position label.
--aparte-branch-picker-label-min-width32pxReserved label width, so 9 / 9 growing to 10 / 12 does not shift the arrows.
--aparte-waiting-height1.5emMin height of the waiting region, so the first token does not jump the layout.
--aparte-waiting-dot-gapvar(--aparte-space-2)Gap between the three waiting dots.
--aparte-status-dot-size6pxDiameter of a waiting dot (shared with the status indicator).
--aparte-status-colorvar(--aparte-text-muted)Colour of the waiting dots (shared with the status indicator).
--aparte-error-solid#dc2626Ring drawn around the avatar while data-error is set. The error CARD itself belongs to the error segment renderer.
  • Each message is a role="article" named after its author, so a screen reader can move from one message to the next.
  • A message with nothing to show (a tool with no output, a turn stopped before its first token) hides its whole row instead of leaving a name and a time over nothing.