Componentmedia0.78 KB gzipZero dependencies

ChatBubble

Single message bubble with tail, status ticks, and hover press feedback.

Preview

Live Preview

ChatBubble

Hey there! How's it going?
Hey there! How's it going?
I'm doing well, thanks!
I'm doing well, thanks!

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add chat-bubble

Or install the full npm package:

npm install bigbullui
import { ChatBubble } from "@/components/ui/chat-bubble";

<ChatBubble direction="outgoing">Message body<ChatStatus sent={true} delivered={true} read={false} /></ChatBubble>

Usage guidance

Use ChatBubble when a ticket stub surface needs Single message bubble with tail, status ticks, and hover press feedback. Keep props minimal and prefer composition with sibling stubs.

Avoid ChatBubble for long form flows or dense data grids where a dedicated layout block fits better.

Source code

chat-bubble.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface ChatStatus {
  sent?: boolean;
  delivered?: boolean;
  read?: boolean;
}

export interface ChatBubbleProps {
  direction: "incoming" | "outgoing";
  children: React.ReactNode;
  status?: ChatStatus;
}

const ChatBubble: React.FC<ChatBubbleProps> = ({ direction, children, status }) => {
  const [isPressed, setIsPressed] = React.useState(false);

  return (
    <div
      className={cn(
        "flex flex-col gap-1 relative min-w-[40px]",
        direction === "outgoing"
          ? "items-end"
          : "items-start"
      )}
    >
      {children}
      {status && (
        <div
          className={cn(
            "absolute -bottom-1 -left-1 flex gap-0.5 text-[10px] mono uppercase text-muted-foreground",
            direction === "outgoing" ? "-right-1" : "-left-1"
          )}
        >
          {[status.sent && "S", status.delivered && "D", status.read && "R"]
            .filter(Boolean)
            .join(" ")}
        </div>
      )}

      {/* Tail triangle for incoming, rotated square for outgoing */}
      {direction === "incoming" ? (
        <div
          className={cn(
            "absolute -inset-0.5 pointer-events-none",
            "mx-auto w-0 border-2 border-transparent border-border/50"
          )}
        >
          <div
            className={cn(
              "mx-auto w-0 border-2 border-border/60",
              "border-t-transparent border-b-2"
            )}
          />
        </div>
      ) : (
        <div
          className={cn(
            "absolute -inset-0.5 pointer-events-none rotate-6",
            "w-0 h-0 border-2 border-transparent border-border/50"
          )}
        >
          <div
            className={cn(
              "w-0 h-0 border-2 border-border/60",
              "border-t-transparent border-b-2 rotate-6"
            )}
          />
        </div>
      )}

      {/* Press feedback */}
      <div
        onMouseDown={() => setIsPressed(true)}
        onMouseUp={() => setIsPressed(false)}
        onMouseLeave={() => setIsPressed(false)}
        className={cn(
          "transition-transform duration-150",
          isPressed ? "scale-[0.96]" : ""
        )}
      >
        {children}
      </div>
    </div>
  );
};

export { ChatBubble };

Props

PropTypeDescription
direction"incoming" | "outgoing"
childrenReact.ReactNode
status?ChatStatus