Chat messages with sender alignment, seven treatments and an optional reactions row.
<tp-bubble>
Conversation
Default density
Compact density
Restart from the initial state
Show the copyable source
Hi Maya, the March invoice shows two seats we removed last week.Can you check whether the credit was applied?Good catch. The credit posts on the next billing date, April 1.
I've added a note to your account so it shows on the receipt.
👍
Hi Maya, the March invoice shows two seats we removed last week.Can you check whether the credit was applied?Good catch. The credit posts on the next billing date, April 1.
I've added a note to your account so it shows on the receipt.
👍
<div style="display:grid;gap:var(--tp-space-6);max-inline-size:32rem;margin-inline:auto">
<tp-bubble-group>
<tp-bubble>Hi Maya, the March invoice shows two seats we removed last week.</tp-bubble>
<tp-bubble>Can you check whether the credit was applied?</tp-bubble>
</tp-bubble-group>
<tp-bubble-group>
<tp-bubble align="end" variant="tinted">Good catch. The credit posts on the next billing date, April 1.</tp-bubble>
<tp-bubble align="end" variant="tinted">
I've added a note to your account so it shows on the receipt.
<span slot="reactions" role="img" aria-label="Reaction: thumbs up">👍</span>
</tp-bubble>
</tp-bubble-group>
</div>
Usage
tp-bubble renders one message; tp-bubble-group stacks consecutive messages from the same sender. Your application owns delivery, sender names and reaction counts; the Bubble only presents them.
Grouping adds no conversation state or announcements.
Treatments
Variants
Default density
Compact density
Restart from the initial state
Show the copyable source
DefaultSecondarySubduedTintedOutlineGhostMessage failed to send. Tap to retry.
DefaultSecondarySubduedTintedOutlineGhostMessage failed to send. Tap to retry.
<div style="display:grid;gap:var(--tp-space-3);max-inline-size:32rem;margin-inline:auto">
<tp-bubble variant="default">Default</tp-bubble>
<tp-bubble>Secondary</tp-bubble>
<tp-bubble variant="subdued">Subdued</tp-bubble>
<tp-bubble variant="tinted">Tinted</tp-bubble>
<tp-bubble variant="outline">Outline</tp-bubble>
<tp-bubble variant="ghost">Ghost</tp-bubble>
<tp-bubble variant="destructive">Message failed to send. Tap to retry.</tp-bubble>
</div>
secondary is the default. Use one treatment per sender so the conversation reads at a glance, ghost for unframed assistant text that should take the full row, and destructive for delivery failures.
Sender alignment
align is logical: end sits on the inline end and follows writing direction. Group consecutive messages so spacing reflects who is speaking, not each message.
Alignment
Default density
Compact density
Restart from the initial state
Show the copyable source
Deploy finished for web-prod.Error rate is back under 0.1%.Thanks. I'll close the incident.
Deploy finished for web-prod.Error rate is back under 0.1%.Thanks. I'll close the incident.
<div style="display:grid;gap:var(--tp-space-6);max-inline-size:32rem;margin-inline:auto">
<tp-bubble-group>
<tp-bubble variant="subdued">Deploy finished for web-prod.</tp-bubble>
<tp-bubble variant="subdued">Error rate is back under 0.1%.</tp-bubble>
</tp-bubble-group>
<tp-bubble-group>
<tp-bubble align="end" variant="default">Thanks. I'll close the incident.</tp-bubble>
</tp-bubble-group>
</div>
Reactions
The reactions slot accepts plain counts or real controls. reaction-side picks the top or bottom edge and reactions-align the inline edge; the two are independent. Give every reaction an accessible name.
Empty reactions have no visible surface.
Reactions
Default density
Compact density
Restart from the initial state
Show the copyable source
The new onboarding flow is live for 10% of signups.
🎉 🔥
Nice. Let's review conversion on Friday.
👀
Can someone own the follow-up survey?
Like · 3
The new onboarding flow is live for 10% of signups.
🎉 🔥
Nice. Let's review conversion on Friday.
👀
Can someone own the follow-up survey?
Like · 3
<div style="display:grid;gap:var(--tp-space-10);max-inline-size:32rem;margin-inline:auto;padding-block:var(--tp-space-4)">
<tp-bubble variant="default">
The new onboarding flow is live for 10% of signups.
<span slot="reactions" role="img" aria-label="Reactions: party popper and fire">🎉 🔥</span>
</tp-bubble>
<tp-bubble align="end" variant="tinted" reaction-side="block-start" reactions-align="start">
Nice. Let's review conversion on Friday.
<span slot="reactions" role="img" aria-label="Reaction: eyes">👀</span>
</tp-bubble>
<tp-bubble>
Can someone own the follow-up survey?
<tp-button slot="reactions" size="xs" variant="outline" aria-label="Like this message; 3 likes">Like · 3</tp-button>
</tp-bubble>
</div>
Rich content
The default slot takes any content: paragraphs and links keep their own semantics. The message container is a named group (label, default “Message”); label="" removes the role.
Rich content
Default density
Compact density
Restart from the initial state
Show the copyable source
Release notes for 2.4 are drafted.
Highlights: SSO for all plans, faster exports, and the new audit log. Read the draft
Release notes for 2.4 are drafted.
Highlights: SSO for all plans, faster exports, and the new audit log. Read the draft
<div style="display:grid;gap:var(--tp-space-3);max-inline-size:32rem;margin-inline:auto">
<tp-bubble variant="default" label="Message from Jordan">
<p style="margin:0">Release notes for 2.4 are drafted.</p>
<p style="margin:0">Highlights: SSO for all plans, faster exports, and the new audit log. <a href="#rich-content">Read the draft</a></p>
</tp-bubble>
</div>
Long messages
Expandable message
Default density
Compact density
Restart from the initial state
Show the copyable source
Here's the summary from this morning's incident review.
Show more
Checkout errors started at 09:12 when the payments provider rotated its signing keys. Our webhook verifier cached the old key for up to an hour.
We shortened the cache to five minutes and added an alert on signature failures. No orders were lost; 41 customers saw a retry prompt.
Here's the summary from this morning's incident review.
Show more
Checkout errors started at 09:12 when the payments provider rotated its signing keys. Our webhook verifier cached the old key for up to an hour.
We shortened the cache to five minutes and added an alert on signature failures. No orders were lost; 41 customers saw a retry prompt.
<div style="display:grid;gap:var(--tp-space-3);max-inline-size:32rem;margin-inline:auto">
<tp-bubble variant="subdued" label="Message from Priya">
<p style="margin:0">Here's the summary from this morning's incident review.</p>
<tp-collapsible id="bubble-expand">
<span slot="label">Show more</span>
<p style="margin:0">Checkout errors started at 09:12 when the payments provider rotated its signing keys. Our webhook verifier cached the old key for up to an hour.</p>
<p style="margin-block:var(--tp-space-2) 0">We shortened the cache to five minutes and added an alert on signature failures. No orders were lost; 41 customers saw a retry prompt.</p>
</tp-collapsible>
</tp-bubble>
</div>
<script type="module">
await customElements.whenDefined('tp-collapsible');
const disclosure = document.querySelector('#bubble-expand');
disclosure.partContracts = {
'collapsible-label': { content: ({ open }) => (open ? 'Show less' : 'Show more') },
};
disclosure.partPresentation = {
'collapsible-trigger': { styleHook: { 'padding-inline': '0', 'inline-size': 'fit-content' } },
'collapsible-content-body': { styleHook: { 'padding-inline': '0' } },
};
</script>
Put a Collapsible inside the bubble to keep long replies short in the thread. The Collapsible owns expansion and its native button; partContracts swaps the label as it opens, and partPresentation trims the trigger to fit the bubble’s padding.
Buttons and links
Quick replies
Default density
Compact density
Restart from the initial state
Show the copyable source
Hi Sam, your order #10482 is out for delivery. Anything I can help with?Track your packageChange the delivery addressLeave it with a neighborTalk to a person
Hi Sam, your order #10482 is out for delivery. Anything I can help with?Track your packageChange the delivery addressLeave it with a neighborTalk to a person
<div id="bubble-replies" style="display:grid;gap:var(--tp-space-3);max-inline-size:32rem;margin-inline:auto">
<tp-bubble variant="default">Hi Sam, your order #10482 is out for delivery. Anything I can help with?</tp-bubble>
<tp-bubble variant="outline" data-link>Track your package</tp-bubble>
<tp-bubble-group data-suggestions>
<tp-bubble variant="outline" align="end" data-reply>Change the delivery address</tp-bubble>
<tp-bubble variant="outline" align="end" data-reply>Leave it with a neighbor</tp-bubble>
<tp-bubble variant="outline" align="end" data-reply>Talk to a person</tp-bubble>
</tp-bubble-group>
</div>
<script type="module">
import { html } from 'lit';
await customElements.whenDefined('tp-bubble');
const thread = document.querySelector('#bubble-replies');
thread.querySelector('[data-link]').partContracts = {
'bubble-content': {
renderDelegate: ({ bind, content }) => html`<a href="#buttons-and-links" ${bind}>${content}</a>`,
},
};
for (const reply of thread.querySelectorAll('[data-reply]')) {
reply.partContracts = {
'bubble-content': {
renderDelegate: ({ bind, content }) => html`<button type="button" ${bind}>${content}</button>`,
},
};
reply.addEventListener('click', () => {
const sent = document.createElement('tp-bubble');
sent.align = 'end';
sent.variant = 'tinted';
sent.textContent = reply.textContent;
thread.querySelector('[data-suggestions]').replaceWith(sent);
});
}
</script>
A whole bubble can be a link or a button. partContracts renders the bubble-content part as a native a or button, which keeps the bubble’s look and gives it real link or button behavior. Here the suggested replies send themselves when picked.
partContracts['bubble-content'] also supports content and render delegation for a native interactive content host; delegates must retain the supplied binding and give interactive hosts a name. A render delegate must apply its supplied bind directive to the semantic host and render the supplied content; this preserves component state and slot behavior.
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionalignalign'start' | 'end''start'The sender's side, in writing direction: end for the current user's own messages.variantvariant'default' | 'secondary' | 'subdued' | 'tinted' | 'outline' | 'ghost' | 'destructive''secondary'The message's treatment: one per sender, ghost for unframed full-width text, destructive for a failed delivery.reaction-sidereactionSide'block-start' | 'block-end''block-end'The edge, top or bottom, that the reactions sit on.reactions-alignreactionsAlign'start' | 'end''end'The inline edge the reactions line up with, independent of the message's alignment.labellabelstring'Message'The name of the message's group for assistive technology; empty removes the group role.
Slots
SlotDescriptiondefaultThe message content: text, paragraphs, links or a Collapsible for a long reply.reactionsReaction counts or reaction controls, each with an accessible name.
CSS parts
PartDescription::part(bubble-content)The framed content and the treatment of the variant.::part(bubble-reactions)The row of reactions on the edge of the message.::part(bubble-root)The message container: its alignment and its named group.
<tp-bubble-group>
Slots
SlotDescriptiondefaultThe sender's Bubbles, in order.