MessageBubble
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
A versatile message bubble component for building chat interfaces and messaging UIs. MessageBubble supports different sender types, delivery statuses, avatars, color variants, and custom styling through CSS Shadow Parts.
Examples
Live Preview
Interactive Chat Demo
Type a message and click Send to add it to the conversation. This demonstrates real-world integration with VueInput and VueButton.
Basic Message Bubbles
Messages can be sent from "me" or "them" with author, time, and optional footer.
Delivery Status
Use the footer prop to display delivery status indicators.
Color Variants
Different semantic variants for various message types.
Success
Warning
Danger
Info
Neutral
Monochrome
With Avatar URLs
Display user avatars using the avatarUrl prop.
Custom Avatar Component
Use the avatar slot to provide custom avatar components with more control.
Long Content
Message bubbles adapt to longer content with proper text wrapping.
View Vue Code
<template>
<section>
<!-- Interactive Chat Demo -->
<div class="mbe4">
<h2>Interactive Chat Demo</h2>
<p>Type a message and click Send to add it to the conversation. This demonstrates real-world integration with VueInput and VueButton.</p>
</div>
<div class="chat-demo-container mbe6">
<!-- Messages Container -->
<div class="messages-container">
<TransitionGroup name="message-fade">
<VueMessageBubble
v-for="message in messages"
:key="message.id"
:from="message.from"
:message="message.text"
:author="message.author"
:time="message.time"
:avatar-url="message.avatarUrl"
:footer="message.footer"
class="message-item"
/>
</TransitionGroup>
<div
v-if="messages.length === 0"
class="empty-state"
>
No messages yet. Start the conversation!
</div>
</div>
<!-- Input Area -->
<div class="input-area">
<VueInput
v-model:value="newMessage"
placeholder="Type your message..."
rounded
@keyup.enter="sendMessage"
>
<template #addon-left>
<MessageCircle
:size="18"
color="var(--ag-primary)"
/>
</template>
</VueInput>
<VueButton
variant="primary"
shape="rounded"
@click="sendMessage"
:disabled="!newMessage.trim()"
class="send-button"
>
<Send :size="18" />
<span class="mis1">Send</span>
</VueButton>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<VueButton
size="sm"
shape="rounded"
:bordered="true"
@click="addBotResponse"
class="mie2"
>
<Bot :size="16" />
<span class="mis1">Add Bot Reply</span>
</VueButton>
<VueButton
size="sm"
shape="rounded"
variant="danger"
:bordered="true"
@click="clearMessages"
>
<Trash2 :size="16" />
<span class="mis1">Clear All</span>
</VueButton>
</div>
</div>
<!-- Basic Example -->
<div class="mbe4">
<h2>Basic Message Bubbles</h2>
<p>Messages can be sent from "me" or "them" with author, time, and optional footer.</p>
</div>
<div class="message-container mbe6">
<VueMessageBubble
from="them"
message="Hey, are we still on for lunch?"
author="Sarah"
time="11:30 AM"
/>
<VueMessageBubble
from="me"
message="Yes! Where should we go?"
author="Me"
time="11:32 AM"
/>
<VueMessageBubble
from="them"
message="How about that new Italian place?"
author="Sarah"
time="11:33 AM"
/>
<VueMessageBubble
from="me"
message="Sounds perfect. See you there at 12:30."
author="Me"
time="11:35 AM"
/>
</div>
<!-- Delivery Status -->
<div class="mbe4">
<h2>Delivery Status</h2>
<p>Use the footer prop to display delivery status indicators.</p>
</div>
<div class="message-container mbe6">
<VueMessageBubble
from="me"
message="Hey, did you get my last message?"
author="Me"
time="2:30 PM"
footer="Delivered"
/>
<VueMessageBubble
from="me"
message="Just checking in!"
author="Me"
time="2:31 PM"
footer="Seen"
/>
<VueMessageBubble
from="them"
message="Yes, I saw it! Thanks for sending."
author="Sarah"
time="2:32 PM"
footer="Read"
/>
<VueMessageBubble
from="me"
message="Great! Let's catch up later."
author="Me"
time="2:33 PM"
footer="Sending..."
/>
</div>
<!-- Color Variants -->
<div class="mbe4">
<h2>Color Variants</h2>
<p>Different semantic variants for various message types.</p>
</div>
<div class="message-container mbe6">
<div class="variant-section">
<h3>Success</h3>
<VueMessageBubble
from="them"
message="Payment completed successfully!"
author="System"
time="10:02 AM"
variant="success"
/>
<VueMessageBubble
from="me"
message="Great, thanks for confirming"
author="Me"
time="10:03 AM"
variant="success"
/>
</div>
<div class="variant-section">
<h3>Warning</h3>
<VueMessageBubble
from="them"
message="Your session will expire in 5 minutes"
author="System"
time="10:04 AM"
variant="warning"
/>
<VueMessageBubble
from="me"
message="I'll save my work now"
author="Me"
time="10:05 AM"
variant="warning"
/>
</div>
<div class="variant-section">
<h3>Danger</h3>
<VueMessageBubble
from="them"
message="Error: Unable to process your request"
author="System"
time="10:06 AM"
variant="danger"
/>
<VueMessageBubble
from="me"
message="Let me try again"
author="Me"
time="10:07 AM"
variant="danger"
/>
</div>
<div class="variant-section">
<h3>Info</h3>
<VueMessageBubble
from="them"
message="New features available in version 2.0"
author="System"
time="10:08 AM"
variant="info"
/>
<VueMessageBubble
from="me"
message="Sounds interesting!"
author="Me"
time="10:09 AM"
variant="info"
/>
</div>
<div class="variant-section">
<h3>Neutral</h3>
<VueMessageBubble
from="them"
message="This is a neutral informational message"
author="System"
time="10:10 AM"
variant="neutral"
/>
</div>
<div class="variant-section">
<h3>Monochrome</h3>
<VueMessageBubble
from="them"
message="Simple monochrome styling"
author="System"
time="10:12 AM"
variant="monochrome"
/>
</div>
</div>
<!-- With Avatars -->
<div class="mbe4">
<h2>With Avatar URLs</h2>
<p>Display user avatars using the avatarUrl prop.</p>
</div>
<div class="message-container mbe6">
<VueMessageBubble
from="them"
message="I have an avatar!"
author="Jane Smith"
time="10:05 AM"
avatar-url="https://i.pravatar.cc/150?img=1"
/>
<VueMessageBubble
from="me"
message="Me too!"
author="Me"
time="10:06 AM"
avatar-url="https://i.pravatar.cc/150?img=2"
/>
</div>
<!-- Custom Avatar Slot -->
<div class="mbe4">
<h2>Custom Avatar Component</h2>
<p>Use the avatar slot to provide custom avatar components with more control.</p>
</div>
<div class="message-container mbe6">
<VueMessageBubble
from="them"
message="I'm using a custom Avatar component with text initials!"
author="Jane Smith"
time="10:00 AM"
>
<template #avatar>
<VueAvatar
text="JS"
variant="info"
size="sm"
/>
</template>
</VueMessageBubble>
<VueMessageBubble
from="me"
message="And I'm using an Avatar with an image and different styling!"
author="Me"
time="10:01 AM"
>
<template #avatar>
<VueAvatar
img-src="https://i.pravatar.cc/150?img=10"
img-alt="User avatar"
variant="success"
size="sm"
/>
</template>
</VueMessageBubble>
<VueMessageBubble
from="them"
message="Avatar components give you more control over styling and variants!"
author="Alex Chen"
time="10:02 AM"
>
<template #avatar>
<VueAvatar
text="AC"
variant="warning"
size="sm"
shape="square"
/>
</template>
</VueMessageBubble>
</div>
<!-- Long Content -->
<div class="mbe4">
<h2>Long Content</h2>
<p>Message bubbles adapt to longer content with proper text wrapping.</p>
</div>
<div class="message-container mbe6">
<VueMessageBubble
from="them"
message="This is a much longer message that demonstrates how the message bubble handles extended content. It should wrap properly and maintain good readability across multiple lines of text."
author="Alex"
time="10:20 AM"
avatar-url="https://i.pravatar.cc/150?img=4"
/>
<VueMessageBubble
from="me"
message="I can write long messages too! Here's a detailed response that spans multiple lines and shows how the bubble adapts to different content lengths while maintaining its visual hierarchy."
author="Me"
time="10:21 AM"
footer="Delivered"
avatar-url="https://i.pravatar.cc/150?img=8"
/>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { VueMessageBubble } from "agnosticui-core/message-bubble/vue";
import { VueAvatar } from "agnosticui-core/avatar/vue";
import VueInput from "agnosticui-core/input/vue";
import VueButton from "agnosticui-core/button/vue";
import { MessageCircle, Send, Bot, Trash2 } from "lucide-vue-next";
export default defineComponent({
name: "MessageBubbleExamples",
components: {
VueMessageBubble,
VueAvatar,
VueInput,
VueButton,
MessageCircle,
Send,
Bot,
Trash2,
},
data() {
return {
newMessage: "",
messages: [] as Array<{
id: number;
from: "me" | "them";
text: string;
author: string;
time: string;
avatarUrl: string;
footer: string;
}>,
messageIdCounter: 1,
botResponses: [
"That's interesting! Tell me more.",
"I understand what you mean.",
"Thanks for sharing that!",
"Great point!",
"I appreciate your message.",
"That makes sense to me.",
"Interesting perspective!",
],
};
},
methods: {
sendMessage() {
if (!this.newMessage.trim()) return;
const now = new Date();
const time = now.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
this.messages.push({
id: this.messageIdCounter++,
from: "me",
text: this.newMessage,
author: "Me",
time: time,
avatarUrl: "https://i.pravatar.cc/150?img=8",
footer: "Sent",
});
// Clear input
this.newMessage = "";
// Auto-scroll to bottom
this.$nextTick(() => {
const container = this.$el.querySelector(".messages-container");
if (container) {
container.scrollTop = container.scrollHeight;
}
});
},
addBotResponse() {
const now = new Date();
const time = now.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
const randomResponse =
this.botResponses[Math.floor(Math.random() * this.botResponses.length)];
this.messages.push({
id: this.messageIdCounter++,
from: "them",
text: randomResponse,
author: "AI Assistant",
time: time,
avatarUrl: "https://i.pravatar.cc/150?img=5",
footer: "Read",
});
// Auto-scroll to bottom
this.$nextTick(() => {
const container = this.$el.querySelector(".messages-container");
if (container) {
container.scrollTop = container.scrollHeight;
}
});
},
clearMessages() {
if (confirm("Are you sure you want to clear all messages?")) {
this.messages = [];
this.messageIdCounter = 1;
}
},
},
});
</script>
<style scoped>
/* Chat Demo Styles */
.chat-demo-container {
max-width: 700px;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
background-color: var(--vp-c-bg);
}
.messages-container {
min-height: 400px;
max-height: 500px;
overflow-y: auto;
padding: 20px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
margin-bottom: 16px;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 360px;
color: var(--vp-c-text-3);
font-style: italic;
}
.message-fade-enter-active {
animation: message-fade-in var(--ag-motion-medium, 0.2s) ease-out;
}
.message-fade-leave-active {
animation: message-fade-out var(--ag-motion-fast, 0.15s) ease-in;
}
.message-fade-move {
transition: transform var(--ag-motion-medium, 0.2s) ease;
}
@keyframes message-fade-in {
from {
opacity: 0;
transform: translateY(12px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes message-fade-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
.input-area {
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: flex-end;
}
.input-area > :first-child {
flex: 1;
}
.send-button {
flex-shrink: 0;
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* Scrollbar styling */
.messages-container::-webkit-scrollbar {
width: 8px;
}
.messages-container::-webkit-scrollbar-track {
background: var(--vp-c-bg);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb {
background: var(--vp-c-divider);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: var(--vp-c-text-3);
}
/* Standard Example Styles */
.message-container {
max-width: 600px;
border: 1px solid var(--vp-c-divider);
padding: 20px;
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
}
.variant-section {
margin-bottom: 24px;
}
.variant-section:last-child {
margin-bottom: 0;
}
.variant-section h4 {
margin: 0 0 12px 0;
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-2);
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/message-bubble';
import 'agnosticui-core/avatar';
import 'agnosticui-core/input';
import 'agnosticui-core/button';
import 'agnosticui-core/icon';
export class MessageBubbleLitExamples extends LitElement {
static properties = {
newMessage: { type: String },
messages: { type: Array },
messageIdCounter: { type: Number }
};
constructor() {
super();
this.newMessage = '';
this.messages = [];
this.messageIdCounter = 1;
this.botResponses = [
"That's interesting! Tell me more.",
"I understand what you mean.",
"Thanks for sharing that!",
"Great point!",
"I appreciate your message.",
"That makes sense to me.",
"Interesting perspective!",
];
}
// Render in light DOM to access global utility classes
createRenderRoot() {
return this;
}
sendMessage() {
if (!this.newMessage.trim()) return;
const now = new Date();
const time = now.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
this.messages = [...this.messages, {
id: this.messageIdCounter++,
from: "me",
text: this.newMessage,
author: "Me",
time: time,
avatarUrl: "https://i.pravatar.cc/150?img=8",
footer: "Sent",
}];
this.newMessage = '';
// Auto-scroll to bottom
setTimeout(() => {
const container = this.querySelector('.messages-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
}
addBotResponse() {
const now = new Date();
const time = now.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
const randomResponse =
this.botResponses[Math.floor(Math.random() * this.botResponses.length)];
this.messages = [...this.messages, {
id: this.messageIdCounter++,
from: "them",
text: randomResponse,
author: "AI Assistant",
time: time,
avatarUrl: "https://i.pravatar.cc/150?img=5",
footer: "Read",
}];
// Auto-scroll to bottom
setTimeout(() => {
const container = this.querySelector('.messages-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
}
clearMessages() {
if (confirm("Are you sure you want to clear all messages?")) {
this.messages = [];
this.messageIdCounter = 1;
}
}
handleKeyUp(e) {
if (e.key === 'Enter') {
this.sendMessage();
}
}
handleInput(e) {
this.newMessage = e.target.value;
}
render() {
return html`
<section>
<!-- Interactive Chat Demo -->
<div class="mbe4">
<h2>Interactive Chat Demo</h2>
<p>Type a message and click Send to add it to the conversation. This demonstrates real-world integration with ag-input and ag-button.</p>
</div>
<div class="chat-demo-container mbe6">
<!-- Messages Container -->
<div class="messages-container">
${this.messages.map(message => html`
<ag-message-bubble
from=${message.from}
message=${message.text}
author=${message.author}
time=${message.time}
avatar-url=${message.avatarUrl}
footer=${message.footer}
class="message-item"
></ag-message-bubble>
`)}
${this.messages.length === 0 ? html`
<div class="empty-state">
No messages yet. Start the conversation!
</div>
` : ''}
</div>
<!-- Input Area -->
<div class="input-area">
<ag-input
.value=${this.newMessage}
@input=${this.handleInput}
@keyup=${this.handleKeyUp}
placeholder="Type your message..."
rounded
>
<ag-icon slot="addon-left" size="18">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--ag-primary)" stroke-width="2">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
</ag-icon>
</ag-input>
<ag-button
variant="primary"
shape="rounded"
@click=${this.sendMessage}
?disabled=${!this.newMessage.trim()}
class="send-button"
>
<ag-icon size="18" no-fill>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</ag-icon>
<span class="mis1">Send</span>
</ag-button>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<ag-button
size="sm"
shape="rounded"
bordered
@click=${this.addBotResponse}
class="mie2"
>
<ag-icon size="16" no-fill>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</ag-icon>
<span class="mis1">Add Bot Reply</span>
</ag-button>
<ag-button
size="sm"
shape="rounded"
variant="danger"
bordered
@click=${this.clearMessages}
>
<ag-icon size="16" no-fill>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</ag-icon>
<span class="mis1">Clear All</span>
</ag-button>
</div>
</div>
<!-- Basic Example -->
<div class="mbe4">
<h2>Basic Message Bubbles</h2>
<p>Messages can be sent from "me" or "them" with author, time, and optional footer.</p>
</div>
<div class="message-container mbe6">
<ag-message-bubble
from="them"
message="Hey, are we still on for lunch?"
author="Sarah"
time="11:30 AM"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Yes! Where should we go?"
author="Me"
time="11:32 AM"
></ag-message-bubble>
<ag-message-bubble
from="them"
message="How about that new Italian place?"
author="Sarah"
time="11:33 AM"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Sounds perfect. See you there at 12:30."
author="Me"
time="11:35 AM"
></ag-message-bubble>
</div>
<!-- Delivery Status -->
<div class="mbe4">
<h2>Delivery Status</h2>
<p>Use the footer prop to display delivery status indicators.</p>
</div>
<div class="message-container mbe6">
<ag-message-bubble
from="me"
message="Hey, did you get my last message?"
author="Me"
time="2:30 PM"
footer="Delivered"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Just checking in!"
author="Me"
time="2:31 PM"
footer="Seen"
></ag-message-bubble>
<ag-message-bubble
from="them"
message="Yes, I saw it! Thanks for sending."
author="Sarah"
time="2:32 PM"
footer="Read"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Great! Let's catch up later."
author="Me"
time="2:33 PM"
footer="Sending..."
></ag-message-bubble>
</div>
<!-- Color Variants -->
<div class="mbe4">
<h2>Color Variants</h2>
<p>Different semantic variants for various message types.</p>
</div>
<div class="message-container mbe6">
<div class="variant-section">
<h3>Success</h3>
<ag-message-bubble
from="them"
message="Payment completed successfully!"
author="System"
time="10:02 AM"
variant="success"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Great, thanks for confirming"
author="Me"
time="10:03 AM"
variant="success"
></ag-message-bubble>
</div>
<div class="variant-section">
<h3>Warning</h3>
<ag-message-bubble
from="them"
message="Your session will expire in 5 minutes"
author="System"
time="10:04 AM"
variant="warning"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="I'll save my work now"
author="Me"
time="10:05 AM"
variant="warning"
></ag-message-bubble>
</div>
<div class="variant-section">
<h3>Danger</h3>
<ag-message-bubble
from="them"
message="Error: Unable to process your request"
author="System"
time="10:06 AM"
variant="danger"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Let me try again"
author="Me"
time="10:07 AM"
variant="danger"
></ag-message-bubble>
</div>
<div class="variant-section">
<h3>Info</h3>
<ag-message-bubble
from="them"
message="New features available in version 2.0"
author="System"
time="10:08 AM"
variant="info"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Sounds interesting!"
author="Me"
time="10:09 AM"
variant="info"
></ag-message-bubble>
</div>
<div class="variant-section">
<h3>Neutral</h3>
<ag-message-bubble
from="them"
message="This is a neutral informational message"
author="System"
time="10:10 AM"
variant="neutral"
></ag-message-bubble>
</div>
<div class="variant-section">
<h3>Monochrome</h3>
<ag-message-bubble
from="them"
message="Simple monochrome styling"
author="System"
time="10:12 AM"
variant="monochrome"
></ag-message-bubble>
</div>
</div>
<!-- With Avatars -->
<div class="mbe4">
<h2>With Avatar URLs</h2>
<p>Display user avatars using the avatarUrl prop.</p>
</div>
<div class="message-container mbe6">
<ag-message-bubble
from="them"
message="I have an avatar!"
author="Jane Smith"
time="10:05 AM"
avatar-url="https://i.pravatar.cc/150?img=1"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Me too!"
author="Me"
time="10:06 AM"
avatar-url="https://i.pravatar.cc/150?img=2"
></ag-message-bubble>
</div>
<!-- Custom Avatar Slot -->
<div class="mbe4">
<h2>Custom Avatar Component</h2>
<p>Use the avatar slot to provide custom avatar components with more control.</p>
</div>
<div class="message-container mbe6">
<ag-message-bubble
from="them"
message="I'm using a custom Avatar component with text initials!"
author="Jane Smith"
time="10:00 AM"
>
<ag-avatar slot="avatar" text="JS" variant="info" size="sm"></ag-avatar>
</ag-message-bubble>
<ag-message-bubble
from="me"
message="And I'm using an Avatar with an image and different styling!"
author="Me"
time="10:01 AM"
>
<ag-avatar
slot="avatar"
img-src="https://i.pravatar.cc/150?img=10"
img-alt="User avatar"
variant="success"
size="sm"
></ag-avatar>
</ag-message-bubble>
<ag-message-bubble
from="them"
message="Avatar components give you more control over styling and variants!"
author="Alex Chen"
time="10:02 AM"
>
<ag-avatar
slot="avatar"
text="AC"
variant="warning"
size="sm"
shape="square"
></ag-avatar>
</ag-message-bubble>
</div>
<!-- Long Content -->
<div class="mbe4">
<h2>Long Content</h2>
<p>Message bubbles adapt to longer content with proper text wrapping.</p>
</div>
<div class="message-container mbe6">
<ag-message-bubble
from="them"
message="This is a much longer message that demonstrates how the message bubble handles extended content. It should wrap properly and maintain good readability across multiple lines of text."
author="Alex"
time="10:20 AM"
avatar-url="https://i.pravatar.cc/150?img=4"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="I can write long messages too! Here's a detailed response that spans multiple lines and shows how the bubble adapts to different content lengths while maintaining its visual hierarchy."
author="Me"
time="10:21 AM"
footer="Delivered"
avatar-url="https://i.pravatar.cc/150?img=8"
></ag-message-bubble>
</div>
</section>
<style>
/* Chat Demo Styles */
.chat-demo-container {
max-width: 700px;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
background-color: var(--vp-c-bg);
}
.messages-container {
min-height: 400px;
max-height: 500px;
overflow-y: auto;
padding: 20px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
margin-bottom: 16px;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 360px;
color: var(--vp-c-text-3);
font-style: italic;
}
.input-area {
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: flex-end;
}
.input-area > :first-child {
flex: 1;
}
.send-button {
flex-shrink: 0;
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* Scrollbar styling */
.messages-container::-webkit-scrollbar {
width: 8px;
}
.messages-container::-webkit-scrollbar-track {
background: var(--vp-c-bg);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb {
background: var(--vp-c-divider);
border-radius: 4px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: var(--vp-c-text-3);
}
/* Standard Example Styles */
.message-container {
max-width: 600px;
border: 1px solid var(--vp-c-divider);
padding: 20px;
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
}
.variant-section {
margin-bottom: 24px;
}
.variant-section:last-child {
margin-bottom: 0;
}
.variant-section h4 {
margin: 0 0 12px 0;
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-2);
}
</style>
`;
}
}
// Register the custom element
customElements.define('message-bubble-lit-examples', MessageBubbleLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { useState } from "react";
import { ReactMessageBubble } from "agnosticui-core/message-bubble/react";
import { ReactAvatar } from "agnosticui-core/avatar/react";
import { ReactInput } from "agnosticui-core/input/react";
import { ReactButton } from "agnosticui-core/button/react";
import { ReactIcon } from "agnosticui-core/icon/react";
export default function MessageBubbleReactExamples() {
const [newMessage, setNewMessage] = useState("");
const [messages, setMessages] = useState([]);
const [messageIdCounter, setMessageIdCounter] = useState(1);
const botResponses = [
"That's interesting! Tell me more.",
"I understand what you mean.",
"Thanks for sharing that!",
"Great point!",
"I appreciate your message.",
"That makes sense to me.",
"Interesting perspective!",
];
const sendMessage = () => {
if (!newMessage.trim()) return;
const now = new Date();
const time = now.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
setMessages([
...messages,
{
id: messageIdCounter,
from: "me",
text: newMessage,
author: "Me",
time: time,
avatarUrl: "https://i.pravatar.cc/150?img=8",
footer: "Sent",
},
]);
setMessageIdCounter(messageIdCounter + 1);
setNewMessage("");
// Auto-scroll to bottom
setTimeout(() => {
const container = document.querySelector(".messages-container");
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
};
const addBotResponse = () => {
const now = new Date();
const time = now.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
const randomResponse =
botResponses[Math.floor(Math.random() * botResponses.length)];
setMessages([
...messages,
{
id: messageIdCounter,
from: "them",
text: randomResponse,
author: "AI Assistant",
time: time,
avatarUrl: "https://i.pravatar.cc/150?img=5",
footer: "Read",
},
]);
setMessageIdCounter(messageIdCounter + 1);
// Auto-scroll to bottom
setTimeout(() => {
const container = document.querySelector(".messages-container");
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
};
const clearMessages = () => {
if (window.confirm("Are you sure you want to clear all messages?")) {
setMessages([]);
setMessageIdCounter(1);
}
};
const handleKeyUp = (e) => {
if (e.key === "Enter") {
sendMessage();
}
};
return (
<section>
{/* Interactive Chat Demo */}
<div className="mbe4">
<h2>Interactive Chat Demo</h2>
<p>
Type a message and click Send to add it to the conversation. This
demonstrates real-world integration with ReactInput and ReactButton.
</p>
</div>
<div className="chat-demo-container mbe6">
{/* Messages Container */}
<div className="messages-container">
{messages.map((message) => (
<ReactMessageBubble
key={message.id}
from={message.from}
message={message.text}
author={message.author}
time={message.time}
avatarUrl={message.avatarUrl}
footer={message.footer}
className="message-item"
/>
))}
{messages.length === 0 && (
<div className="empty-state">
No messages yet. Start the conversation!
</div>
)}
</div>
{/* Input Area */}
<div className="input-area">
<ReactInput
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyUp={handleKeyUp}
placeholder="Type your message..."
rounded
>
<ReactIcon slot="addon-left" size="18">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="var(--ag-primary)"
strokeWidth="2"
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
</ReactIcon>
</ReactInput>
<ReactButton
variant="primary"
shape="rounded"
onClick={sendMessage}
disabled={!newMessage.trim()}
className="send-button"
>
<ReactIcon size="18" noFill>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</ReactIcon>
<span className="mis1">Send</span>
</ReactButton>
</div>
{/* Quick Actions */}
<div className="quick-actions">
<ReactButton
size="sm"
shape="rounded"
bordered
onClick={addBotResponse}
className="mie2"
>
<ReactIcon size="16" noFill>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</ReactIcon>
<span className="mis1">Add Bot Reply</span>
</ReactButton>
<ReactButton
size="sm"
shape="rounded"
variant="danger"
bordered
onClick={clearMessages}
>
<ReactIcon size="16" noFill>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</ReactIcon>
<span className="mis1">Clear All</span>
</ReactButton>
</div>
</div>
{/* Basic Example */}
<div className="mbe4">
<h2>Basic Message Bubbles</h2>
<p>
Messages can be sent from "me" or "them" with author, time, and
optional footer.
</p>
</div>
<div className="message-container mbe6">
<ReactMessageBubble
from="them"
message="Hey, are we still on for lunch?"
author="Sarah"
time="11:30 AM"
/>
<ReactMessageBubble
from="me"
message="Yes! Where should we go?"
author="Me"
time="11:32 AM"
/>
<ReactMessageBubble
from="them"
message="How about that new Italian place?"
author="Sarah"
time="11:33 AM"
/>
<ReactMessageBubble
from="me"
message="Sounds perfect. See you there at 12:30."
author="Me"
time="11:35 AM"
/>
</div>
{/* Delivery Status */}
<div className="mbe4">
<h2>Delivery Status</h2>
<p>Use the footer prop to display delivery status indicators.</p>
</div>
<div className="message-container mbe6">
<ReactMessageBubble
from="me"
message="Hey, did you get my last message?"
author="Me"
time="2:30 PM"
footer="Delivered"
/>
<ReactMessageBubble
from="me"
message="Just checking in!"
author="Me"
time="2:31 PM"
footer="Seen"
/>
<ReactMessageBubble
from="them"
message="Yes, I saw it! Thanks for sending."
author="Sarah"
time="2:32 PM"
footer="Read"
/>
<ReactMessageBubble
from="me"
message="Great! Let's catch up later."
author="Me"
time="2:33 PM"
footer="Sending..."
/>
</div>
{/* Color Variants */}
<div className="mbe4">
<h2>Color Variants</h2>
<p>Different semantic variants for various message types.</p>
</div>
<div className="message-container mbe6">
<div className="variant-section">
<h3>Success</h3>
<ReactMessageBubble
from="them"
message="Payment completed successfully!"
author="System"
time="10:02 AM"
variant="success"
/>
<ReactMessageBubble
from="me"
message="Great, thanks for confirming"
author="Me"
time="10:03 AM"
variant="success"
/>
</div>
<div className="variant-section">
<h3>Warning</h3>
<ReactMessageBubble
from="them"
message="Your session will expire in 5 minutes"
author="System"
time="10:04 AM"
variant="warning"
/>
<ReactMessageBubble
from="me"
message="I'll save my work now"
author="Me"
time="10:05 AM"
variant="warning"
/>
</div>
<div className="variant-section">
<h3>Danger</h3>
<ReactMessageBubble
from="them"
message="Error: Unable to process your request"
author="System"
time="10:06 AM"
variant="danger"
/>
<ReactMessageBubble
from="me"
message="Let me try again"
author="Me"
time="10:07 AM"
variant="danger"
/>
</div>
<div className="variant-section">
<h3>Info</h3>
<ReactMessageBubble
from="them"
message="New features available in version 2.0"
author="System"
time="10:08 AM"
variant="info"
/>
<ReactMessageBubble
from="me"
message="Sounds interesting!"
author="Me"
time="10:09 AM"
variant="info"
/>
</div>
<div className="variant-section">
<h3>Neutral</h3>
<ReactMessageBubble
from="them"
message="This is a neutral informational message"
author="System"
time="10:10 AM"
variant="neutral"
/>
</div>
<div className="variant-section">
<h3>Monochrome</h3>
<ReactMessageBubble
from="them"
message="Simple monochrome styling"
author="System"
time="10:12 AM"
variant="monochrome"
/>
</div>
</div>
{/* With Avatars */}
<div className="mbe4">
<h2>With Avatar URLs</h2>
<p>Display user avatars using the avatarUrl prop.</p>
</div>
<div className="message-container mbe6">
<ReactMessageBubble
from="them"
message="I have an avatar!"
author="Jane Smith"
time="10:05 AM"
avatarUrl="https://i.pravatar.cc/150?img=1"
/>
<ReactMessageBubble
from="me"
message="Me too!"
author="Me"
time="10:06 AM"
avatarUrl="https://i.pravatar.cc/150?img=2"
/>
</div>
{/* Custom Avatar Slot */}
<div className="mbe4">
<h2>Custom Avatar Component</h2>
<p>
Use the avatar slot to provide custom avatar components with more
control.
</p>
</div>
<div className="message-container mbe6">
<ReactMessageBubble
from="them"
message="I'm using a custom Avatar component with text initials!"
author="Jane Smith"
time="10:00 AM"
>
<ReactAvatar slot="avatar" text="JS" variant="info" size="sm" />
</ReactMessageBubble>
<ReactMessageBubble
from="me"
message="And I'm using an Avatar with an image and different styling!"
author="Me"
time="10:01 AM"
>
<ReactAvatar
slot="avatar"
imgSrc="https://i.pravatar.cc/150?img=10"
imgAlt="User avatar"
variant="success"
size="sm"
/>
</ReactMessageBubble>
<ReactMessageBubble
from="them"
message="Avatar components give you more control over styling and variants!"
author="Alex Chen"
time="10:02 AM"
>
<ReactAvatar
slot="avatar"
text="AC"
variant="warning"
size="sm"
shape="square"
/>
</ReactMessageBubble>
</div>
{/* Long Content */}
<div className="mbe4">
<h2>Long Content</h2>
<p>
Message bubbles adapt to longer content with proper text wrapping.
</p>
</div>
<div className="message-container mbe6">
<ReactMessageBubble
from="them"
message="This is a much longer message that demonstrates how the message bubble handles extended content. It should wrap properly and maintain good readability across multiple lines of text."
author="Alex"
time="10:20 AM"
avatarUrl="https://i.pravatar.cc/150?img=4"
/>
<ReactMessageBubble
from="me"
message="I can write long messages too! Here's a detailed response that spans multiple lines and shows how the bubble adapts to different content lengths while maintaining its visual hierarchy."
author="Me"
time="10:21 AM"
footer="Delivered"
avatarUrl="https://i.pravatar.cc/150?img=8"
/>
</div>
</section>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add MessageBubbleThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<div>
<VueMessageBubble
from="them"
message="Hello, how are you?"
author="John Doe"
time="10:00 AM"
/>
<VueMessageBubble
from="me"
message="I'm doing great, thanks!"
author="Me"
time="10:01 AM"
/>
<VueMessageBubble
from="them"
message="Check out this feature!"
author="Jane"
time="10:05 AM"
avatar-url="https://i.pravatar.cc/150?img=1"
/>
<VueMessageBubble
from="me"
message="Message with status"
author="Me"
time="10:10 AM"
footer="Delivered ✓"
/>
<VueMessageBubble
from="them"
message="Payment successful!"
author="System"
time="10:15 AM"
variant="success"
/>
<VueMessageBubble
from="them"
message="Using custom avatar component"
author="Alex"
time="10:20 AM"
>
<template #avatar>
<VueAvatar text="AC" variant="info" size="sm" />
</template>
</VueMessageBubble>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { VueMessageBubble } from 'agnosticui-core/message-bubble/vue';
import { VueAvatar } from 'agnosticui-core/avatar/vue';
export default defineComponent({
components: { VueMessageBubble, VueAvatar }
});
</script>React
import { ReactMessageBubble } from 'agnosticui-core/message-bubble/react';
import { ReactAvatar } from 'agnosticui-core/avatar/react';
export default function Example() {
return (
<div>
<ReactMessageBubble
from="them"
message="Hello, how are you?"
author="John Doe"
time="10:00 AM"
/>
<ReactMessageBubble
from="me"
message="I'm doing great, thanks!"
author="Me"
time="10:01 AM"
/>
<ReactMessageBubble
from="them"
message="Check out this feature!"
author="Jane"
time="10:05 AM"
avatarUrl="https://i.pravatar.cc/150?img=1"
/>
<ReactMessageBubble
from="me"
message="Message with status"
author="Me"
time="10:10 AM"
footer="Delivered ✓"
/>
<ReactMessageBubble
from="them"
message="Payment successful!"
author="System"
time="10:15 AM"
variant="success"
/>
</div>
);
}Lit (Web Components)
<script type="module">
import 'agnosticui-core/message-bubble';
</script>
<ag-message-bubble
from="them"
message="Hello, how are you?"
author="John Doe"
time="10:00 AM"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="I'm doing great, thanks!"
author="Me"
time="10:01 AM"
></ag-message-bubble>
<ag-message-bubble
from="them"
message="Check out this feature!"
author="Jane"
time="10:05 AM"
avatar-url="https://i.pravatar.cc/150?img=1"
></ag-message-bubble>
<ag-message-bubble
from="me"
message="Message with status"
author="Me"
time="10:10 AM"
footer="Delivered ✓"
></ag-message-bubble>
<ag-message-bubble
from="them"
message="Payment successful!"
author="System"
time="10:15 AM"
variant="success"
></ag-message-bubble>Props
| Prop | Type | Default | Description |
|---|---|---|---|
from | 'me' | 'them' | 'them' | Indicates the sender of the message. Affects bubble alignment and styling. |
message | string | '' | The text content of the message. |
time | string | '' | Timestamp to display with the message. |
author | string | '' | Name of the message sender. |
avatarUrl | string | '' | URL for the sender's avatar image. |
footer | string | '' | Footer text, typically used for delivery status (e.g., "Delivered", "Seen"). |
variant | 'default' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'monochrome' | 'default' | Color variant for semantic styling. |
Slots
| Slot | Description |
|---|---|
default | Main content slot for the message text (alternative to message prop). |
header | Optional header content above the message. |
footer | Optional footer content below the message (alternative to footer prop). |
avatar | Custom avatar content, allowing use of Avatar components or custom elements. |
CSS Custom Properties
The MessageBubble component can be styled using CSS custom properties (variables):
ag-message-bubble {
--ag-message-bubble-bg: #f0f0f0;
--ag-message-bubble-color: #333;
--ag-message-bubble-padding: 12px 16px;
--ag-message-bubble-border-radius: 18px;
--ag-message-bubble-max-width: 70%;
}CSS Shadow Parts
For advanced customization, MessageBubble exposes CSS Shadow Parts that allow you to style internal elements:
Vue Example
<template>
<div>
<VueMessageBubble
class="custom-bubble"
from="them"
message="This message bubble is custom styled using CSS Shadow Parts!"
author="CSS Wizard"
time="10:00 AM"
/>
<VueMessageBubble
from="me"
message="This is a normal message bubble from me."
author="Me"
time="10:05 AM"
/>
</div>
</template>
<style>
.custom-bubble::part(ag-message-bubble) {
background-color: #4a90e2;
color: white;
border-radius: 20px;
font-family: 'Comic Sans MS', cursive, sans-serif;
padding: 16px 20px;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
}
</style>Available Shadow Parts
::part(ag-message-bubble) {
}
::part(ag-message-bubble-avatar) {
}
::part(ag-message-bubble-content) {
}
::part(ag-message-bubble-author) {
}
::part(ag-message-bubble-time) {
}
::part(ag-message-bubble-footer) {
}Color Variants
MessageBubble includes semantic color variants for different message types:
- default - Standard message styling
- success - For positive notifications (green)
- warning - For cautionary messages (yellow/orange)
- danger - For errors or critical alerts (red)
- info - For informational messages (blue)
- neutral - For neutral system messages (gray)
- monochrome - Minimal, monochrome styling
Accessibility
The MessageBubble component is built with accessibility in mind:
- Uses semantic HTML structure for proper screen reader support
- Supports keyboard navigation when interactive elements are included
- Maintains appropriate color contrast ratios across all variants
- Author and timestamp information is properly associated with messages
- Avatar images include alt text support when using the
avatarUrlprop
Best Practices
- Always provide meaningful
authorinformation for screen reader users - Use
timeprop to give context about when messages were sent - Ensure custom avatar content includes appropriate alt text
- Use semantic
variantprops to convey message importance visually and contextually - For delivery status, use clear text in the
footerprop (e.g., "Delivered", "Read")
Examples
Complete Chat Interface
Full Example
<template>
<div class="chat-container">
<VueMessageBubble
from="them"
message="Hey! How's the project going?"
author="Sarah Johnson"
time="2:30 PM"
avatar-url="https://i.pravatar.cc/150?img=5"
/>
<VueMessageBubble
from="me"
message="Great! Just finished the API integration."
author="Me"
time="2:32 PM"
footer="Delivered ✓"
avatar-url="https://i.pravatar.cc/150?img=8"
/>
<VueMessageBubble
from="them"
message="That's awesome! Can you send me the documentation?"
author="Sarah Johnson"
time="2:33 PM"
avatar-url="https://i.pravatar.cc/150?img=5"
/>
<VueMessageBubble
from="me"
message="Sure, I'll email it right now."
author="Me"
time="2:35 PM"
footer="Seen ✓✓"
avatar-url="https://i.pravatar.cc/150?img=8"
/>
<VueMessageBubble
from="them"
message="Perfect! Thanks so much! 🎉"
author="Sarah Johnson"
time="2:36 PM"
avatar-url="https://i.pravatar.cc/150?img=5"
/>
</div>
</template>
<style>
.chat-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
</style>System Notifications
Example
<template>
<div class="notifications">
<VueMessageBubble
from="them"
message="Your file has been uploaded successfully!"
author="System"
time="3:00 PM"
variant="success"
/>
<VueMessageBubble
from="them"
message="Warning: Your storage is almost full (90% used)"
author="System"
time="3:05 PM"
variant="warning"
/>
<VueMessageBubble
from="them"
message="Error: Failed to sync with cloud storage"
author="System"
time="3:10 PM"
variant="danger"
/>
<VueMessageBubble
from="them"
message="New feature: Dark mode is now available!"
author="System"
time="3:15 PM"
variant="info"
/>
</div>
</template>