Back

iOS Message Bubbles

Beautiful iOS-style message bubbles with authentic iMessage appearance and tail effects.

Category
CardReact
CSS
shadcnCustom CSS

Manual

Create a file and paste the following code into it.

message-bubble.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"use client";

import "./message-bubble.css";

import { cn } from "@/lib/cn";

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>
	);
}

Update the import paths to match your project setup.

Similar screens