shadimage
odysseyui
uicomponents-ai-message-bubblesource: ok · hit

Message Bubble

A composable chat message bubble with avatar, content, and timestamp sub-components. Role-aware layout for user and assistant messages.

npx shadcn add "https://www.odysseyui.com/r/components-ai-message-bubble.json"
free

Where it goes

chat

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@hugeicons/react@hugeicons/core-free-icons

Registry dependencies

avatar@odysseyui/components-ai-chat-container

Files1 · source cached 10/8/2026, 11:22:21 PM

registry/components/ai/message-bubble/index.tsx
'use client';

import { cn } from '@/lib/utils';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { HugeiconsIcon } from '@hugeicons/react';
import { BotIcon, UserIcon } from '@hugeicons/core-free-icons';
import { createContext, useContext } from 'react';

type Role = 'user' | 'assistant';

type MessageBubbleContextValue = { isUser: boolean };

const MessageBubbleContext = createContext<MessageBubbleContextValue | null>(
  null,
);

function useMessageBubble() {
  const ctx = useContext(MessageBubbleContext);
  if (!ctx)
    throw new Error('useMessageBubble must be used inside <MessageBubble />');
  return ctx;
}

export type MessageBubbleProps = React.ComponentProps<'div'> & { role: Role };

export function MessageBubble({
  role,
  children,
  className,
  ...props
}: MessageBubbleProps) {
  const isUser = role === 'user';
  return (
    <MessageBubbleContext.Provider value={{ isUser }}>
      <div
        className={cn(
          'flex w-full items-end gap-2',
          isUser ? 'flex-row-reverse' : 'flex-row',
          className,
        )}
        {...props}
      >
        {children}
      </div>
    </MessageBubbleContext.Provider>
  );
}

export type MessageBubbleAvatarProps = {
  src?: string;
  fallback?: string;
  className?: string;
};

export function MessageBubbleAvatar({
  src,
  fallback,
  className,
}: MessageBubbleAvatarProps) {
  const { isUser } = useMessageBubble();
  return (
    <Avatar className={cn('size-7 shrink-0', className)}>
      {isUser ? (
        <>
          {src && <AvatarImage src={src} />}
          <AvatarFallback className="bg-primary/10 text-primary">
            {fallback ?? (
              <HugeiconsIcon
                icon={UserIcon}
                className="size-4"
                strokeWidth={2}
              />
            )}
          </AvatarFallback>
        </>
      ) : (
        <>
          {src && <AvatarImage src={src} />}
          <AvatarFallback className="bg-primary/10 text-primary">
            {fallback ?? (
              <HugeiconsIcon
                icon={BotIcon}
                className="size-4"
                strokeWidth={2}
              />
            )}
          </AvatarFallback>
        </>
      )}
    </Avatar>
  );
}

export type MessageBubbleContentProps = React.ComponentProps<'div'>;

export function MessageBubbleContent({
  children,
  className,
  ...props
}: MessageBubbleContentProps) {
  const { isUser } = useMessageBubble();
  return (
    <div
      className={cn(
        'max-w-[75%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed',
        isUser
          ? 'bg-primary text-primary-foreground rounded-br-sm'
          : 'bg-muted text-foreground rounded-bl-sm',
        className,
      )}
      {...props}
    >
      {children}
    </div>
  );
}

export type MessageBubbleTimestampProps = React.ComponentProps<'p'>;

export function MessageBubbleTimestamp({
  children,
  className,
  ...props
}: MessageBubbleTimestampProps) {
  const { isUser } = useMessageBubble();
  return (
    <p
      className={cn(
        'mt-1 text-right text-[10px]',
        isUser ? 'text-primary-foreground/60' : 'text-muted-foreground',
        className,
      )}
      {...props}
    >
      {children}
    </p>
  );
}

export { useMessageBubble };

More from odysseyui

Browse all components in this library →