A flexible and feature-rich React bubble component built with TypeScript and TailwindCSS.
The Bubble component provides chat-style message bubbles with support for footers, copy-to-clipboard functionality, and customizable positioning. Perfect for building chat interfaces, notifications, or callout sections.
- 🎯 Chat Bubbles: Left and right-aligned message bubbles with optional tails
- 📋 Copy Functionality: Built-in copy-to-clipboard with custom copy handling
- 📊 Footer Support: Structured footer with key-value pairs or raw JSX
- ♿ Accessible: Keyboard navigation and screen reader support
- 🎨 Customizable: Multiple styling options and theme support
- 🌲 Tree-shakeable: Lightweight and optimized for bundle size
- 🔧 TypeScript: Fully typed with comprehensive prop definitions
npm install @versini/ui-bubble
Note: This component requires TailwindCSS and the
@versini/ui-styles
plugin for proper styling. See the root README for complete setup instructions.
import { Bubble } from "@versini/ui-bubble";
function App() {
return (
<div className="space-y-4">
<Bubble kind="left" tail>
Hello! This is a left-aligned message.
</Bubble>
<Bubble kind="right" tail>
And this is a right-aligned reply.
</Bubble>
</div>
);
}
import { Bubble } from "@versini/ui-bubble";
function App() {
return (
<Bubble
kind="right"
tail
footer={{
Sent: "12:00 PM",
Delivered: "12:01 PM",
Read: "12:02 PM"
}}
>
Message with delivery status footer.
</Bubble>
);
}
import { Bubble } from "@versini/ui-bubble";
function App() {
return (
<div className="space-y-4">
{/* Simple copy - copies the bubble content */}
<Bubble kind="left" copyToClipboard>
Click the copy icon to copy this message.
</Bubble>
{/* Custom copy text */}
<Bubble kind="left" copyToClipboard="Custom text to copy">
This will copy custom text instead of the bubble content.
</Bubble>
{/* Custom copy function */}
<Bubble
kind="left"
copyToClipboard={(text) => {
navigator.clipboard.writeText(`Copied: ${text}`);
}}
>
This uses a custom copy function.
</Bubble>
</div>
);
}
import { Bubble } from "@versini/ui-bubble";
function ChatExample() {
const messages = [
{ id: 1, text: "Hey, how are you?", kind: "left", time: "10:30 AM" },
{
id: 2,
text: "I'm good, thanks! How about you?",
kind: "right",
time: "10:32 AM"
},
{
id: 3,
text: "Doing great! Want to grab lunch?",
kind: "left",
time: "10:35 AM"
}
];
return (
<div className="max-w-md mx-auto space-y-3 p-4 bg-gray-50 rounded-lg">
{messages.map((message) => (
<Bubble
key={message.id}
kind={message.kind}
tail
footer={{ Time: message.time }}
copyToClipboard
>
{message.text}
</Bubble>
))}
</div>
);
}
import { Bubble } from "@versini/ui-bubble";
function AdvancedFooterExample() {
return (
<div className="space-y-4">
{/* Structured footer with empty row */}
<Bubble
kind="right"
tail
footer={{
"Message ID": "msg-123",
Sent: "2:30 PM",
Status: Bubble.FOOTER_EMPTY, // Empty row that maintains height
Delivered: "2:31 PM",
Read: "2:35 PM"
}}
>
Message with detailed tracking information.
</Bubble>
{/* Raw JSX footer */}
<Bubble
kind="left"
rawFooter={
<div className="flex justify-between items-center text-xs">
<span className="text-green-600">✓ Verified</span>
<span>3:45 PM</span>
</div>
}
>
Message with custom JSX footer.
</Bubble>
</div>
);
}
import { Bubble } from "@versini/ui-bubble";
function CustomWidthExample() {
return (
<div className="space-y-4">
{/* Default responsive width */}
<Bubble kind="left" tail>
This bubble has default responsive width behavior.
</Bubble>
{/* Custom width with container queries */}
<div style={{ containerType: "inline-size" }} className="w-96">
<Bubble kind="left" tail noMaxWidth className="max-w-[95cqw]">
This bubble uses container query width (95% of container width).
</Bubble>
</div>
{/* Fixed width */}
<Bubble kind="right" tail noMaxWidth className="w-64">
This bubble has a fixed width of 256px.
</Bubble>
</div>
);
}
import { Bubble } from "@versini/ui-bubble";
function CopyFunctionalityExample() {
const handleCustomCopy = (text: any) => {
// Custom copy logic
const copyText = `Shared message: "${text}"`;
navigator.clipboard.writeText(copyText);
console.log("Copied with custom format:", copyText);
};
return (
<div className="space-y-4">
{/* Boolean - copies bubble content */}
<Bubble kind="left" copyToClipboard={true}>
Basic copy functionality
</Bubble>
{/* String - copies specific text */}
<Bubble kind="left" copyToClipboard="contact@example.com">
Click to copy: contact@example.com
</Bubble>
{/* Function - custom copy behavior */}
<Bubble kind="left" copyToClipboard={handleCustomCopy}>
Custom copy behavior with formatting
</Bubble>
{/* With custom copy button styling */}
<Bubble
kind="right"
copyToClipboard
copyToClipboardMode="dark"
copyToClipboardFocusMode="light"
>
Copy button with custom theme
</Bubble>
</div>
);
}
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode |
- | The text to render in the bubble |
kind | "left" | "right" |
"left" |
The type of Bubble (changes color and chevron location) |
tail | boolean |
false |
Whether or not the Bubble should have a tail |
copyToClipboard | boolean | string | ((text: any) => void) |
- | Copy functionality configuration |
footer | { [key: string]: string | number | undefined | null } |
- | Object depicting the extra rows for the Bubble footer |
rawFooter | React.ReactNode |
- | Same as "footer" but accepts raw JSX |
noMaxWidth | boolean |
false |
Whether to disable default responsive max-width |
className | string |
- | CSS class(es) to add to the main component wrapper |
contentClassName | string |
- | CSS class(es) to add to the content wrapper |
copyToClipboardMode | "dark" | "light" | "system" | "alt-system" |
"system" |
The mode of Copy Button |
copyToClipboardFocusMode | "dark" | "light" | "system" | "alt-system" |
"system" |
The focus mode for the Copy Button |
-
Bubble.FOOTER_EMPTY
- Use in footer to create an empty row that maintains height