AIMessageBubble
The AIMessageBubble styles and structures messages for chat-based AI interfaces. It aligns messages according to sender roles, supports user/assistant avatars, handles copy-to-clipboard functionality, provides hover actions, and scales with smooth entrance animations.
- Preview
- Code
Session started. AI assistant initialized.
You11:01 AM
Can you show me how to style a button in Tailwind?
Assistant11:01 AM
Certainly! Here is a standard button class:
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
Click Me
</button>
import { AIMessageBubble } from '@ignix-ui/ai-message-bubble';
import { ThumbsUp, ThumbsDown } from 'lucide-react';
function ChatFeed() {
return (
<div className="flex flex-col space-y-4">
<AIMessageBubble
role="assistant"
variant="default"
shape="bubble"
senderName="Assistant"
timestamp="10:30 AM"
avatar="https://api.dicebear.com/7.x/bottts/svg?seed=assistant"
showCopy
content="Hello! How can I assist you today?"
actions={
<>
<button className="p-1 text-neutral-400 hover:text-neutral-600"><ThumbsUp size={14} /></button>
<button className="p-1 text-neutral-400 hover:text-neutral-600"><ThumbsDown size={14} /></button>
</>
}
/>
<AIMessageBubble
role="user"
variant="default"
shape="bubble"
senderName="You"
timestamp="10:31 AM"
avatar="https://api.dicebear.com/7.x/adventurer/svg?seed=user"
showCopy
content="Explain quantum computing in one sentence."
/>
</div>
);
}
Installation
- CLI
- Manual
ignix add component ai-message-bubble
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { motion } from "framer-motion";
import { Copy, Check } from "lucide-react";
import { cn } from "../../../utils/cn";
import { Avatar } from "../avatar";
const bubbleVariants = cva(
"relative px-4 py-3 text-sm leading-6 transition-colors shadow-sm",
{
variants: {
role: {
user: "",
assistant: "",
system: "text-center border border-dashed",
},
variant: {
default: "",
minimal: "shadow-none px-0 py-0 border-none bg-transparent dark:bg-transparent",
glass: "backdrop-blur-md",
},
},
compoundVariants: [
{
role: "user",
variant: "default",
className: "bg-neutral-950 border border-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900 dark:border-neutral-200",
},
{
role: "assistant",
variant: "default",
className: "bg-neutral-100 border border-neutral-200/50 text-neutral-900 dark:bg-neutral-800/80 dark:border-neutral-700/50 dark:text-neutral-100",
},
{
role: "system",
variant: "default",
className: "bg-neutral-50 border-neutral-200 text-neutral-500 dark:bg-neutral-900/30 dark:border-neutral-800 dark:text-neutral-400",
},
{
role: "user",
variant: "minimal",
className: "text-neutral-900 dark:text-neutral-100",
},
{
role: "assistant",
variant: "minimal",
className: "text-neutral-900 dark:text-neutral-100",
},
{
role: "system",
variant: "minimal",
className: "text-neutral-500 dark:text-neutral-400",
},
{
role: "assistant",
variant: "glass",
className:
"bg-white/10 border border-white/20 text-white backdrop-blur-xl",
},
{
role: "user",
variant: "glass",
className:
"bg-white/10 border border-white/20 text-white backdrop-blur-xl",
},
{
role: "system",
variant: "glass",
className:
"bg-white/5 border border-white/10 text-white/70 backdrop-blur-xl",
},
],
defaultVariants: {
role: "assistant",
variant: "default",
},
}
);
const bubbleShapeVariants = cva("", {
variants: {
role: {
user: "",
assistant: "",
system: "",
},
shape: {
bubble: "",
card: "",
pill: "",
flat: "",
},
},
compoundVariants: [
// Bubble
{
role: "user",
shape: "bubble",
className: "rounded-2xl rounded-tr-none",
},
{
role: "assistant",
shape: "bubble",
className: "rounded-2xl rounded-tl-none",
},
{
role: "system",
shape: "bubble",
className: "rounded-lg",
},
// Card
{
role: "user",
shape: "card",
className: "rounded-lg",
},
{
role: "assistant",
shape: "card",
className: "rounded-lg",
},
{
role: "system",
shape: "card",
className: "rounded-md",
},
{
role: "assistant",
shape: "pill",
className:
"rounded-[36px] rounded-tl-2xl px-7 py-4 max-w-full break-words leading-7",
},
{
role: "user",
shape: "pill",
className:
"rounded-[36px] rounded-tr-2xl px-7 py-4 max-w-full break-words leading-7",
},
{
role: "system",
shape: "pill",
className:
"rounded-[28px] px-6 py-3 leading-6",
},
// Flat
{
role: "user",
shape: "flat",
className: "rounded-none px-3 py-2",
},
{
role: "assistant",
shape: "flat",
className: "rounded-none px-3 py-2",
},
{
role: "system",
shape: "flat",
className: "rounded-none px-3 py-2",
},
],
defaultVariants: {
shape: "bubble",
},
});
export interface AIMessageBubbleProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "content" | "role">,
VariantProps<typeof bubbleVariants>,
VariantProps<typeof bubbleShapeVariants> {
content: React.ReactNode;
avatar?: React.ReactNode;
senderName?: string;
timestamp?: string;
actions?: React.ReactNode;
showCopy?: boolean;
animateEntry?: boolean;
}
const AIMessageBubble = React.forwardRef<HTMLDivElement, AIMessageBubbleProps>(
(
{
className,
role = "assistant",
variant = "default",
shape = "bubble",
content,
avatar,
senderName,
timestamp,
actions,
showCopy = false,
animateEntry = true,
...props
},
ref
) => {
const [copied, setCopied] = React.useState(false);
const handleCopy = async () => {
if (copied || typeof content !== "string") return;
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
const containerVariants = {
hidden: { opacity: 0, y: 12, scale: 0.98 },
visible: { opacity: 1, y: 0, scale: 1 },
};
const isUser = role === "user";
const isSystem = role === "system";
const bubbleContent = (
<div
ref={ref}
className={cn(
"flex w-full items-start gap-3 p-4 group select-text",
isUser ? "flex-row-reverse" : isSystem ? "justify-center" : "flex-row",
className
)}
{...props}
>
{/* Avatar rendering */}
{!isSystem && avatar && (
<div className="flex-shrink-0 select-none">
{typeof avatar === "string" ? (
<Avatar
src={avatar}
alt={senderName || role || undefined}
size="sm"
shape="circle"
/>
) : (
avatar
)}
</div>
)}
{/* Message wrapper */}
<div
className={cn(
"flex flex-col max-w-[85%] sm:max-w-[75%]",
isUser ? "items-end" : isSystem ? "items-center w-full" : "items-start"
)}
>
{/* Header metadata */}
{!isSystem && (senderName || timestamp) && (
<div className="flex items-center gap-2 mb-1 text-xs text-neutral-500 dark:text-neutral-400 select-none">
{senderName && <span className="font-semibold">{senderName}</span>}
{timestamp && <span>{timestamp}</span>}
</div>
)}
{/* Core bubble element */}
<div
className={cn(
bubbleVariants({ role, variant }),
bubbleShapeVariants({ role, shape })
)}
> {typeof content === "string" ? (
<div className="whitespace-pre-wrap">{content}</div>
) : (
content
)}
</div>
{/* Action bar */}
{!isSystem && (showCopy || actions) && (
<div className="flex items-center gap-2 mt-1.5 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-200 select-none">
{showCopy && typeof content === "string" && (
<button
type="button"
onClick={handleCopy}
className="p-1 rounded text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all cursor-pointer"
title="Copy message"
>
{copied ? (
<Check size={14} className="text-emerald-500" />
) : (
<Copy size={14} />
)}
</button>
)}
{actions}
</div>
)}
</div>
</div>
);
if (animateEntry) {
return (
<motion.div
initial="hidden"
animate="visible"
variants={containerVariants}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
>
{bubbleContent}
</motion.div>
);
}
return bubbleContent;
}
);
AIMessageBubble.displayName = "AIMessageBubble";
export { AIMessageBubble, bubbleVariants };
Usage
Import the component:
import { AIMessageBubble } from '@mindfiredigital/ignix-ui';
Basic Example
<AIMessageBubble
role="assistant"
shape="bubble"
content="Hello! How can I help you?"
/>
With Avatar and Timestamp
<AIMessageBubble
role="assistant"
shape="bubble"
senderName="Assistant"
timestamp="12:35 PM"
avatar="/path-to-avatar.png"
content="I can help you build responsive web apps."
/>
Glassmorphic Variant
Perfect for dark themes, hero sections, and image backgrounds:
<AIMessageBubble
role="assistant"
shape="bubble"
variant="glass"
content="I am rendering inside a blur-background glass bubble."
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
content | React.ReactNode | - | The message text or rich layout to display (required) |
role | 'user' | 'assistant' | 'system' | 'assistant' | The role of the sender, determining alignment and visual themes |
variant | 'default' | 'minimal' | 'glass' | 'default' | Bubble containers visual theme |
avatar | React.ReactNode | undefined | Custom avatar (image URL or custom React Node) |
senderName | string | undefined | Sender's name displayed above the message bubble |
timestamp | string | undefined | Message timestamp displayed next to the sender's name |
actions | React.ReactNode | undefined | Custom action buttons rendered inside the hover action bar |
showCopy | boolean | false | Shows a built-in copy-to-clipboard button |
animateEntry | boolean | true | Slide-up and fade-in entry animation when component mounts |
shape | 'bubble' | 'card' | 'pill' | 'flat' | bubble | Controls the visual shape of the message bubble. |