Componentmedia0.78 KB gzipZero dependencies
ChatBubble
Single message bubble with tail, status ticks, and hover press feedback.
Preview
Live Preview
ChatBubble
Hey there! How's it going?
Hey there! How's it going?
I'm doing well, thanks!
I'm doing well, thanks!
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add chat-bubbleOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { ChatBubble } from "@/components/ui/chat-bubble";
<ChatBubble direction="outgoing">Message body<ChatStatus sent={true} delivered={true} read={false} /></ChatBubble>Usage guidance
Use ChatBubble when a ticket stub surface needs Single message bubble with tail, status ticks, and hover press feedback. Keep props minimal and prefer composition with sibling stubs.
Avoid ChatBubble for long form flows or dense data grids where a dedicated layout block fits better.
Source code
chat-bubble.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface ChatStatus {
sent?: boolean;
delivered?: boolean;
read?: boolean;
}
export interface ChatBubbleProps {
direction: "incoming" | "outgoing";
children: React.ReactNode;
status?: ChatStatus;
}
const ChatBubble: React.FC<ChatBubbleProps> = ({ direction, children, status }) => {
const [isPressed, setIsPressed] = React.useState(false);
return (
<div
className={cn(
"flex flex-col gap-1 relative min-w-[40px]",
direction === "outgoing"
? "items-end"
: "items-start"
)}
>
{children}
{status && (
<div
className={cn(
"absolute -bottom-1 -left-1 flex gap-0.5 text-[10px] mono uppercase text-muted-foreground",
direction === "outgoing" ? "-right-1" : "-left-1"
)}
>
{[status.sent && "S", status.delivered && "D", status.read && "R"]
.filter(Boolean)
.join(" ")}
</div>
)}
{/* Tail triangle for incoming, rotated square for outgoing */}
{direction === "incoming" ? (
<div
className={cn(
"absolute -inset-0.5 pointer-events-none",
"mx-auto w-0 border-2 border-transparent border-border/50"
)}
>
<div
className={cn(
"mx-auto w-0 border-2 border-border/60",
"border-t-transparent border-b-2"
)}
/>
</div>
) : (
<div
className={cn(
"absolute -inset-0.5 pointer-events-none rotate-6",
"w-0 h-0 border-2 border-transparent border-border/50"
)}
>
<div
className={cn(
"w-0 h-0 border-2 border-border/60",
"border-t-transparent border-b-2 rotate-6"
)}
/>
</div>
)}
{/* Press feedback */}
<div
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onMouseLeave={() => setIsPressed(false)}
className={cn(
"transition-transform duration-150",
isPressed ? "scale-[0.96]" : ""
)}
>
{children}
</div>
</div>
);
};
export { ChatBubble };Props
| Prop | Type | Description |
|---|---|---|
| direction | "incoming" | "outgoing" | |
| children | React.ReactNode | |
| status? | ChatStatus |