gaia
uimessage-bubblesource: ok · hit
iOS Message Bubbles
Beautiful iOS-style message bubbles with authentic iMessage appearance, message grouping, and tail effects.
npx shadcn add "https://ui.heygaia.io/r/message-bubble.json"free
Interaction
scroll-reveal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files2 · source cached 10/14/2026, 8:42:06 AM
registry/new-york/ui/message-bubble.tsx
"use client";
import "./message-bubble.css";
import { cn } from "@/lib/utils";
export interface MessageBubbleProps {
message: string;
variant?: "sent" | "received";
grouped?: "first" | "middle" | "last" | "none";
className?: string;
children?: React.ReactNode;
}
export function MessageBubble({
message,
variant = "received",
grouped = "none",
className,
children,
}: MessageBubbleProps) {
const groupedClasses =
grouped === "first"
? "imessage-grouped-first mb-1.5"
: grouped === "last"
? "imessage-grouped-last"
: grouped === "middle"
? "imessage-grouped-middle mb-1.5"
: "";
return (
<div
className={cn(
"imessage-bubble",
variant === "sent" ? "imessage-from-me" : "imessage-from-them",
groupedClasses,
className,
)}
>
{children || <p className="whitespace-pre-wrap">{message}</p>}
</div>
);
}
export interface ChatMessageProps {
timestamp?: string;
messages: string[];
variant?: "sent" | "received";
className?: string;
showTimestamp?: boolean;
}
export function ChatMessage({
timestamp,
messages,
variant = "received",
className,
showTimestamp = true,
}: ChatMessageProps) {
const hasMultipleMessages = messages.length > 1;
const getGroupedType = (index: number, total: number) => {
if (total === 1) return "none";
if (index === 0) return "first";
if (index === total - 1) return "last";
return "middle";
};
return (
<div className={cn("flex w-full flex-col", className)}>
<div className="flex flex-col">
{messages.map((message, index) => (
<MessageBubble
key={index}
message={message}
variant={variant}
grouped={
hasMultipleMessages
? getGroupedType(index, messages.length)
: "none"
}
/>
))}
</div>
{showTimestamp && timestamp && (
<span
className={cn(
"mt-1 px-2 text-xs text-muted-foreground",
variant === "sent" && "text-right",
)}
>
{timestamp}
</span>
)}
</div>
);
}
registry/new-york/ui/message-bubble.css
/* iOS-style message bubbles */
.imessage-bubble {
word-wrap: break-word;
line-height: 24px;
position: relative;
padding: 8px 20px;
max-width: 60vw;
border-radius: 20px;
}
.imessage-bubble::before,
.imessage-bubble::after {
content: "";
position: absolute;
bottom: 0;
height: 18px;
}
/* Grouped messages have tighter corner radii on the stacking side */
/* Received messages (left side) - tight corners on left side */
.imessage-grouped-first.imessage-from-them {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 20px 20px 20px 5px !important;
}
.imessage-grouped-middle.imessage-from-them {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 5px 20px 20px 5px !important;
}
.imessage-grouped-last.imessage-from-them {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 5px 20px 20px 20px !important;
}
.imessage-grouped-last.imessage-from-them {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 5px 20px 20px 20px !important;
}
/* Sent messages (right side) - tight corners on right side */
.imessage-grouped-first.imessage-from-me {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 20px 20px 5px 20px !important;
}
.imessage-grouped-middle.imessage-from-me {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 20px 5px 5px 20px !important;
}
.imessage-grouped-last.imessage-from-me {
/* biome-ignore lint/complexity/noImportantStyles: need to override radius */
border-radius: 20px 5px 20px 20px !important;
}
/* Bot messages (from-them style) */
.imessage-from-them {
background-color: var(--color-zinc-300);
color: black;
align-self: flex-start;
}
.imessage-from-them::before {
left: -7px;
width: 20px;
background-color: var(--color-zinc-300);
border-bottom-right-radius: 16px;
}
.imessage-from-them::after {
left: -26px;
width: 26px;
background-color: var(--color-background);
border-bottom-right-radius: 10px;
}
/* User messages (from-me style) */
.imessage-from-me {
color: black;
background: #00bbff;
align-self: flex-end;
}
.imessage-from-me::before {
right: -7px;
width: 20px;
background-color: #00bbff;
border-bottom-left-radius: 16px 14px;
}
.imessage-from-me::after {
right: -26px;
width: 26px;
background-color: var(--color-background);
border-bottom-left-radius: 10px;
}
/* Grouped iMessage bubbles for NEW_MESSAGE_BREAK */
/* Hide tails for grouped messages except the last one */
.imessage-grouped-first::before,
.imessage-grouped-first::after,
.imessage-grouped-middle::before,
.imessage-grouped-middle::after {
display: none;
}