AI Message Bubble

Render assistant, user, tool, and system messages in React. Markdown-ready chat bubbles for LLM and agent apps. Accessible, install via the shadcn CLI.

Report a bug

When to use this in an AI app

Use AI Message Bubble to render each turn in a conversation. Role variants (user, assistant, tool, system) keep an LLM transcript readable and consistent with the rest of your design system.

Browse all AI agent components

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/ai-message-bubble.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

3 stories available:

Code

import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../lib/utils"; import { Avatar, AvatarFallback } from "../avatar/avatar"; import { Badge } from "../badge"; const bubbleVariants = cva( "rounded-2xl border px-4 py-3 shadow-sm transition-colors", { defaultVariants: { messageRole: "assistant", }, variants: { messageRole: { assistant: "border-border bg-card text-card-foreground", system: "border-border/80 bg-muted/60 text-foreground", tool: "border-border bg-muted/40 text-foreground", user: "border-primary/20 bg-primary/10 text-foreground", }, }, }, ); function AIMessageMeta({ author, isUser, status, timestamp, }: { author?: string; isUser: boolean; status?: string; timestamp?: string; }) { return ( <div className={cn( "flex flex-wrap items-center gap-2 text-xs text-muted-foreground", isUser ? "justify-end" : "justify-start", )} > {author ? ( <span className="font-medium text-foreground">{author}</span> ) : null} {timestamp ? <span>{timestamp}</span> : null} {status ? ( <Badge className="rounded-full px-2 py-0 text-[10px]" variant="secondary" > {status} </Badge> ) : null} </div> ); } export type AIMessageBubbleProps = React.ComponentPropsWithoutRef<"div"> & VariantProps<typeof bubbleVariants> & { /** Optional short label describing the speaker. */ author?: string; /** Bubble body content. */ children: React.ReactNode; /** Optional status badge for the message. */ status?: string; /** Optional timestamp or relative time label. */ timestamp?: string; }; const AIMessageBubble = forwardRef<HTMLDivElement, AIMessageBubbleProps>( ( { author, children, className, messageRole = "assistant", status, timestamp, ...props }, ref, ) => { const resolvedMessageRole = messageRole ?? "assistant"; const isUser = resolvedMessageRole === "user"; const fallbackLabel = (author ?? resolvedMessageRole) .charAt(0) .toUpperCase(); return ( <div className={cn("flex w-full", isUser ? "justify-end" : "justify-start")} > <div className={cn( "flex w-full max-w-3xl gap-3", isUser ? "flex-row-reverse text-right" : "flex-row", )} > <Avatar className="mt-0.5 size-8 border border-border/70"> <AvatarFallback className="bg-muted text-[11px] font-medium uppercase text-muted-foreground"> {fallbackLabel} </AvatarFallback> </Avatar> <div className={cn( "min-w-0 space-y-2", isUser ? "items-end" : "items-start", )} > <AIMessageMeta author={author} isUser={isUser} status={status} timestamp={timestamp} /> <div className={cn( bubbleVariants({ messageRole: resolvedMessageRole }), className, )} ref={ref} {...props} > <div className="text-sm leading-6 whitespace-pre-wrap"> {children} </div> </div> </div> </div> </div> ); }, ); AIMessageBubble.displayName = "AIMessageBubble"; export { AIMessageBubble };

Dependencies

  • @vllnt/ui@^0.2.1
  • class-variance-authority