Chat Component
A customizable chat window component built with Shadcn UI and React.
Prerequisites
The chat component is compatible with any environment that supports React and has shadcn/ui configured.
This guide assumes you are already familiar with both of these technologies. If you are not, please review their documentation first.
Quick Start
- Install shadcn/ui: If you haven't already, initialize shadcn/ui in your project by following the official installation guide.
- Install Dependencies: Ensure you have the necessary primitives installed.
pnpm dlx shadcn@latest add textarea - Copy the Chat Component: Copy the chat component files from the repository into your project's components directory (e.g.,
@/components/chat). - Import and Use: Import the component into your page and use it as needed.
The root container component that establishes the chat layout structure with container queries and flex column layout for header, messages, and toolbar sections.
1<Chat>
2 <ChatHeader>
3 {/* Header Content */}
4 </ChatHeader>
5
6 <ChatMessages>
7 {/* Messages Content */}
8 </ChatMessages>
9
10 <ChatToolbar>
11 {/* Toolbar Content */}
12 </ChatToolbar>
13</Chat>;
14 A sticky header component with three sections (Start, Main, End) for displaying chat participant info, status, search, and action buttons.
1<ChatHeader className="border-b">
2 <ChatHeaderStart>
3 <Avatar className="rounded-full size-6">
4 <AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
5 <AvatarFallback>ER</AvatarFallback>
6 </Avatar>
7 <span className="font-medium">Evil Rabbit</span>
8 </ChatHeaderStart>
9 <ChatHeaderMain>
10 <span className="text-sm font-semibold">AKA</span>
11 <span className="text-sm font-medium">Chocolate Bunny</span>
12 </ChatHeaderMain>
13 <ChatHeaderEnd>
14 <InputGroup className="@2xl/chat:flex hidden">
15 <InputGroupInput placeholder="Search..." />
16 <InputGroupAddon>
17 <SearchIcon />
18 </InputGroupAddon>
19 </InputGroup>
20 <Button variant="ghost" className="size-8 @2xl/chat:inline-flex hidden">
21 <PhoneIcon />
22 </Button>
23 <Button variant="ghost" className="size-8 @2xl/chat:inline-flex hidden">
24 <VideoIcon />
25 </Button>
26 <Button variant="ghost" className="size-8">
27 <MoreHorizontalIcon />
28 </Button>
29 </ChatHeaderEnd>
30</ChatHeader>
31 A scrollable flex container with reverse column direction that displays chat messages from bottom to top, automatically handling overflow.
1<ChatMessages>
2 {MESSAGES.map((msg, i, msgs) => {
3 // If date changed, show date item
4 if (
5 new Date(msg.timestamp).toDateString() !==
6 new Date(msgs[i + 1]?.timestamp).toDateString()
7 ) {
8 return (
9 <Fragment key={msg.id}>
10 <PrimaryMessage
11 avatarSrc={msg.sender.avatarUrl}
12 avatarAlt={msg.sender.username}
13 avatarFallback={msg.sender.name.slice(0, 2)}
14 senderName={msg.sender.name}
15 content={msg.content}
16 timestamp={msg.timestamp}
17 />
18 <DateItem timestamp={msg.timestamp} className="my-4" />
19 </Fragment>
20 );
21 }
22
23 // If next item is same user, show additional
24 if (msg.sender.id === msgs[i + 1]?.sender.id) {
25 return (
26 <AdditionalMessage
27 key={msg.id}
28 content={msg.content}
29 timestamp={msg.timestamp}
30 />
31 );
32 }
33 // Else, show primary
34 else {
35 return (
36 <PrimaryMessage
37 className="mt-4"
38 key={msg.id}
39 avatarSrc={msg.sender.avatarUrl}
40 avatarAlt={msg.sender.username}
41 avatarFallback={msg.sender.name.slice(0, 2)}
42 senderName={msg.sender.name}
43 content={msg.content}
44 timestamp={msg.timestamp}
45 />
46 );
47 }
48 })}
49</ChatMessages>
50 A flexible message row component with Addon (for avatar/timestamp) and Body sections, supporting any message or event in the chat.
Primary Message
1import { ReactNode } from "react";
2import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
3import { cn } from "@/lib/utils";
4import {
5 ChatEvent,
6 ChatEventAddon,
7 ChatEventBody,
8 ChatEventContent,
9 ChatEventDescription,
10 ChatEventTitle,
11} from "@/components/ui/chat-event";
12
13export function PrimaryMessage({
14 avatarSrc,
15 avatarAlt,
16 avatarFallback,
17 senderName,
18 content,
19 timestamp,
20 className,
21}: {
22 avatarSrc?: string;
23 avatarAlt?: string;
24 avatarFallback?: string;
25 senderName: string;
26 content: ReactNode;
27 timestamp: number;
28 className?: string;
29}) {
30 return (
31 <ChatEvent className={cn("hover:bg-accent", className)}>
32 <ChatEventAddon>
33 <Avatar className="rounded-full size-8 @md/chat:size-10 mx-auto">
34 <AvatarImage src={avatarSrc} alt={avatarAlt} />
35 <AvatarFallback>{avatarFallback}</AvatarFallback>
36 </Avatar>
37 </ChatEventAddon>
38 <ChatEventBody>
39 <div className="flex items-baseline gap-2">
40 <ChatEventTitle>{senderName}</ChatEventTitle>
41 <ChatEventDescription>
42 {new Intl.DateTimeFormat("en-US", {
43 dateStyle: "medium",
44 timeStyle: "short",
45 }).format(timestamp)}
46 </ChatEventDescription>
47 </div>
48 <ChatEventContent>{content}</ChatEventContent>
49 </ChatEventBody>
50 </ChatEvent>
51 );
52}
53 Additional Message
1import { ReactNode } from "react";
2import {
3 ChatEvent,
4 ChatEventAddon,
5 ChatEventBody,
6 ChatEventContent,
7 ChatEventDescription,
8} from "@/components/ui/chat-event";
9
10export function AdditionalMessage({
11 content,
12 timestamp,
13}: {
14 content: ReactNode;
15 timestamp: number;
16}) {
17 return (
18 <ChatEvent className="hover:bg-accent group">
19 <ChatEventAddon>
20 <ChatEventDescription className="text-right text-[8px] @md/chat:text-[10px] group-hover:visible invisible">
21 {new Intl.DateTimeFormat("en-US", {
22 timeStyle: "short",
23 }).format(timestamp)}
24 </ChatEventDescription>
25 </ChatEventAddon>
26 <ChatEventBody>
27 <ChatEventContent>{content}</ChatEventContent>
28 </ChatEventBody>
29 </ChatEvent>
30 );
31}
32Date Item
1import { Separator } from "@/components/ui/separator";
2import { cn } from "@/lib/utils";
3import { ChatEvent } from "@/components/ui/chat-event";
4
5export function DateItem({
6 timestamp,
7 className,
8}: {
9 timestamp: number;
10 className?: string;
11}) {
12 return (
13 <ChatEvent className={cn("items-center gap-1", className)}>
14 <Separator className="flex-1" />
15 <span className="text-muted-foreground text-xs font-semibold min-w-max">
16 {new Intl.DateTimeFormat("en-US", {
17 dateStyle: "long",
18 }).format(timestamp)}
19 </span>
20 <Separator className="flex-1" />
21 </ChatEvent>
22 );
23}
24A sticky bottom input area with a three-column grid layout (AddonStart, Textarea, AddonEnd) for message composition with optional action buttons on both sides.
1<ChatToolbar>
2 <ChatToolbarAddonStart>
3 <Button variant="ghost" className="size-8 @md/chat:size-9">
4 <PlusIcon className="size-5 @md/chat:size-6 stroke-[1.7px]" />
5 </Button>
6 </ChatToolbarAddonStart>
7 <ChatToolbarTextarea />
8 <ChatToolbarAddonEnd>
9 <Button variant="ghost" className="size-8 @md/chat:size-9">
10 <GiftIcon className="size-4 @md/chat:size-5 stroke-[1.7px]" />
11 </Button>
12 <Button variant="ghost" className="size-8 @md/chat:size-9">
13 <CalendarDaysIcon className="size-4 @md/chat:size-5 stroke-[1.7px]" />
14 </Button>
15 <Button variant="ghost" className="size-8 @md/chat:size-9">
16 <SquareChevronRightIcon className="size-4 @md/chat:size-5 stroke-[1.7px]" />
17 </Button>
18 </ChatToolbarAddonEnd>
19</ChatToolbar>
20