Chat
Message bubbles and conversation areas for direct and group messaging.
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"
import {
Message,
MessageAvatar,
MessageContent,
MessageFooter,
MessageHeader,
} from "@/components/ui/message"
export default function ChatBubbles() {
return (
<div className="mx-auto flex w-full max-w-lg flex-col gap-5">
<Message>
<MessageAvatar>
<Avatar className="size-8">
<AvatarImage src="/avatars/avatar-04.webp" alt="Maya Chen" />
<AvatarFallback>MC</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<MessageHeader>Maya · 10:24 AM</MessageHeader>
<BubbleGroup>
<Bubble variant="secondary">
<BubbleContent>Hey! Are we still on for the design review?</BubbleContent>
</Bubble>
<Bubble variant="secondary">
<BubbleContent>I added the latest notes to the prototype.</BubbleContent>
</Bubble>
</BubbleGroup>
</MessageContent>
</Message>
<Message align="end">
<MessageContent>
<Bubble variant="default">
<BubbleContent>Yes, I’ll join in five. Looking forward to it.</BubbleContent>
</Bubble>
<MessageFooter>Seen 10:26 AM</MessageFooter>
</MessageContent>
</Message>
</div>
)
}
"use client"
import type { SyntheticEvent } from "react"
import { ArrowUp, Paperclip, Video } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/components/ui/input-group"
import { Marker, MarkerContent } from "@/components/ui/marker"
import { Message, MessageContent, MessageFooter } from "@/components/ui/message"
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@/components/ui/message-scroller"
const messages = [
{ id: "1", side: "start", text: "Could you share the updated launch date?" },
{ id: "2", side: "end", text: "We’re targeting Thursday, after the final QA pass." },
{ id: "3", side: "start", text: "Perfect. I’ll update the release notes today." },
] as const
export default function DirectMessage() {
function handleSubmit(event: SyntheticEvent<HTMLFormElement>) {
event.preventDefault()
}
return (
<section className="mx-auto flex h-[32rem] w-full max-w-md flex-col overflow-hidden rounded-xl border bg-background shadow-sm">
<header className="flex h-14 shrink-0 items-center gap-3 border-b px-4">
<Avatar className="size-8">
<AvatarImage src="/avatars/avatar-02.webp" alt="Noah Williams" />
<AvatarFallback>NW</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">Noah Williams</p>
<p className="text-xs text-muted-foreground">Active now</p>
</div>
<Button variant="ghost" size="icon-sm" aria-label="Start a video call">
<Video />
</Button>
</header>
<MessageScrollerProvider defaultScrollPosition="end">
<MessageScroller className="flex-1">
<MessageScrollerViewport aria-label="Conversation with Noah Williams">
<MessageScrollerContent className="justify-end gap-4 p-4">
<Marker variant="separator">
<MarkerContent>Today</MarkerContent>
</Marker>
{messages.map((message, index) => (
<MessageScrollerItem
key={message.id}
messageId={message.id}
scrollAnchor={index === messages.length - 1}
>
<Message align={message.side}>
<MessageContent>
<Bubble variant={message.side === "end" ? "default" : "secondary"}>
<BubbleContent>{message.text}</BubbleContent>
</Bubble>
{index === messages.length - 1 && (
<MessageFooter>Delivered</MessageFooter>
)}
</MessageContent>
</Message>
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
<form className="border-t p-3" onSubmit={handleSubmit}>
<InputGroup className="h-auto rounded-xl">
<InputGroupTextarea
aria-label="Message Noah"
placeholder="Write a message…"
className="min-h-10 max-h-28"
/>
<InputGroupAddon align="block-end" className="justify-between">
<InputGroupButton size="icon-xs" aria-label="Attach a file">
<Paperclip />
</InputGroupButton>
<InputGroupButton
type="submit"
variant="default"
size="icon-xs"
className="rounded-full"
aria-label="Send message"
>
<ArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</form>
</section>
)
}
import { CheckCheck, FileText, MoreHorizontal } from "lucide-react"
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/components/ui/attachment"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"
import { Message, MessageContent, MessageFooter, MessageHeader } from "@/components/ui/message"
export default function ProjectThread() {
return (
<div className="mx-auto flex w-full max-w-xl flex-col gap-5 rounded-xl border bg-background p-4 sm:p-6">
<Marker variant="separator">
<MarkerContent>2 unread messages</MarkerContent>
</Marker>
<Message>
<MessageContent>
<MessageHeader>Jamie · 2:14 PM</MessageHeader>
<Bubble variant="secondary">
<BubbleContent>
I’ve attached the final copy deck. The revised sections are highlighted.
</BubbleContent>
</Bubble>
<AttachmentGroup>
<Attachment size="sm">
<AttachmentMedia>
<FileText />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Launch copy.pdf</AttachmentTitle>
<AttachmentDescription>2.4 MB · PDF</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="More attachment actions">
<MoreHorizontal />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</AttachmentGroup>
</MessageContent>
</Message>
<Message align="end">
<MessageContent>
<Bubble variant="tinted">
<BubbleContent>Got it. I’ll review this before our check-in.</BubbleContent>
</Bubble>
<MessageFooter className="gap-1">
<CheckCheck aria-hidden="true" /> Seen by Jamie
</MessageFooter>
</MessageContent>
</Message>
<Marker variant="border">
<MarkerIcon>
<CheckCheck />
</MarkerIcon>
<MarkerContent>All caught up</MarkerContent>
</Marker>
</div>
)
}