Message Bubble
A focused conversational surface with visual tones, independent alignment, and a speech-bubble tail that pops in on arrival.
Can you summarize the latest deploy logs?
Sure, pulling the last run now.
Build passed. Two warnings, no errors.
import { MessageBubble } from "@/components/motion/message-bubble";
export function MessageBubblePreview() {
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<MessageBubble align="start">
Can you summarize the latest deploy logs?
</MessageBubble>
<MessageBubble align="end" tone="accent">
Sure, pulling the last run now.
</MessageBubble>
<MessageBubble align="end" tone="accent">
Build passed. Two warnings, no errors.
</MessageBubble>
</div>
);
}
"use client";
// easeui.dev/components/agents/message-bubble
import { motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export type MessageBubbleTone = "neutral" | "accent";
export type MessageBubbleAlign = "start" | "end";
export interface MessageBubbleProps {
/** Which side the bubble sits on, matching who sent it. Default "start". */
align?: MessageBubbleAlign;
/** Visual treatment. Default "neutral". */
tone?: MessageBubbleTone;
children: ReactNode;
className?: string;
}
const TONE_CLASS: Record<MessageBubbleTone, string> = {
neutral: "bg-card text-foreground shadow-[0_0_0_1px_var(--border)]",
accent: "bg-accent text-accent-foreground",
};
const EASE = [0.23, 1, 0.32, 1] as const;
/** A chat message surface with a speech-bubble tail that pops in from the side it belongs to. */
export function MessageBubble({
align = "start",
tone = "neutral",
children,
className,
}: MessageBubbleProps) {
const reduce = useReducedMotion();
const fromX = align === "end" ? 10 : -10;
return (
<motion.div
initial={reduce ? { opacity: 0 } : { opacity: 0, x: fromX, scale: 0.97 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
transition={{ duration: reduce ? 0.1 : 0.2, ease: EASE }}
className={cn(
"max-w-[85%] text-pretty rounded-2xl px-3.5 py-2.5 text-sm leading-6",
align === "end" ? "ml-auto rounded-br-sm" : "mr-auto rounded-bl-sm",
TONE_CLASS[tone],
className,
)}
>
{children}
</motion.div>
);
}
Installation
$ bunx --bun shadcn add @easeui/message-bubble
- 1
Set up the theme tokens
Do this once per project. Follow the theme setup or skip it if you already ran shadcn init.
- 2
Install the dependencies
terminal npm install clsx motion tailwind-merge - 3
Add the source files
components/motion/message-bubble.tsx "use client"; // easeui.dev/components/agents/message-bubble import { motion, useReducedMotion } from "motion/react"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; export type MessageBubbleTone = "neutral" | "accent"; export type MessageBubbleAlign = "start" | "end"; export interface MessageBubbleProps { /** Which side the bubble sits on, matching who sent it. Default "start". */ align?: MessageBubbleAlign; /** Visual treatment. Default "neutral". */ tone?: MessageBubbleTone; children: ReactNode; className?: string; } const TONE_CLASS: Record<MessageBubbleTone, string> = { neutral: "bg-card text-foreground shadow-[0_0_0_1px_var(--border)]", accent: "bg-accent text-accent-foreground", }; const EASE = [0.23, 1, 0.32, 1] as const; /** A chat message surface with a speech-bubble tail that pops in from the side it belongs to. */ export function MessageBubble({ align = "start", tone = "neutral", children, className, }: MessageBubbleProps) { const reduce = useReducedMotion(); const fromX = align === "end" ? 10 : -10; return ( <motion.div initial={reduce ? { opacity: 0 } : { opacity: 0, x: fromX, scale: 0.97 }} animate={{ opacity: 1, x: 0, scale: 1 }} transition={{ duration: reduce ? 0.1 : 0.2, ease: EASE }} className={cn( "max-w-[85%] text-pretty rounded-2xl px-3.5 py-2.5 text-sm leading-6", align === "end" ? "ml-auto rounded-br-sm" : "mr-auto rounded-bl-sm", TONE_CLASS[tone], className, )} > {children} </motion.div> ); }lib/utils.ts import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| align? | "start" | "end" | start | Which side the bubble sits on, matching who sent it. Default "start". |
| tone? | "neutral" | "accent" | neutral | Visual treatment. Default "neutral". |
| className? | string | - | - |
Updated