Componentmedia1.19 KB gzipZero dependencies

ChatWindow

Full chat panel with message list, input bar, and typing indicator slot.

Preview

Live Preview

ChatWindow

Hey! How's it going?
AM · 2:15 PM
All good here, just working on the bigbullui components. How about you?
SB · 2:16 PM
Love the new wave 6 components! Especially the chat window and bubbles.
AM · 2:18 PM
Thanks! Working on the typing indicator and reaction bar next.
SB · 2:19 PM

Installation

Tokens guide →

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

npx bigbullui add chat-window

Or install the full npm package:

npm install bigbullui
import { ChatWindow } from "@/components/ui/chat-window";

<ChatWindow messages={messages} onSend={onSend} typing={typing} />

Usage guidance

Use ChatWindow when a ticket stub surface needs Full chat panel with message list, input bar, and typing indicator slot. Keep props minimal and prefer composition with sibling stubs.

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

Source code

chat-window.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 ChatMessage {
  id: string;
  author: string;
  initials: string;
  body: string;
  time?: string;
  direction: "incoming" | "outgoing";
}

export interface ChatWindowProps {
  messages?: ChatMessage[];
  onSend?: (body: string) => void;
  typing?: boolean;
  autoFocus?: boolean;
}

type LocalBubbleProps = { direction: "incoming" | "outgoing"; className?: string; children: React.ReactNode };

function LocalBubble({ direction, className, children }: LocalBubbleProps) {
  return (
    <div
      className={cn(
        "relative rounded-lg px-3 py-2 text-sm shadow-xs animate-[fade-in-up_0.25s_ease-out_both] motion-reduce:animate-none",
        direction === "outgoing" ? "rounded-br-sm" : "rounded-bl-sm",
        className
      )}
    >
      {children}
    </div>
  );
}

function LocalTyping() {
  return (
    <div className="flex gap-1 px-3 py-2" aria-label="Someone is typing">
      {[0, 1, 2].map((i) => (
        <span
          key={i}
          className="size-1.5 rounded-full bg-muted-foreground animate-[loadingDotsBounce_1.2s_ease-in-out_infinite] motion-reduce:animate-none"
          style={{ animationDelay: `${i * 160}ms` }}
        />
      ))}
    </div>
  );
}

const ChatWindow: React.FC<ChatWindowProps> = ({ messages = [], onSend, typing, autoFocus = false }) => {
  const [input, setInput] = React.useState("");

  const handleSend = () => {
    const trimmed = input.trim();
    if (trimmed && onSend) {
      onSend(trimmed);
      setInput("");
    }
  };

  return (
    <div className="space-y-1 max-w-lg">
      <div className="space-y-2 p-4 h-48 overflow-y-auto">
        {messages.map((msg) => (
          <div
            key={msg.id}
            className={cn(
              "flex",
              msg.direction === "outgoing"
                ? "justify-end"
                : "justify-start"
            )}
          >
            <LocalBubble
              className={cn(
                "max-w-[80%]",
                msg.direction === "outgoing"
                  ? "bg-accent text-accent-foreground"
                  : "bg-card text-foreground"
              )}
              direction={msg.direction}
            >
              <>{msg.body}</>
              <div className="text-[10px] mono uppercase text-muted-foreground">
                {msg.initials} · {msg.time || ""}
              </div>
            </LocalBubble>
          </div>
        ))}

        {typing && (
          <div className="flex justify-start">
            <LocalTyping />
          </div>
        )}
      </div>

      <div className="p-4 border-t border-border">
        <div className="flex items-gap-2">
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Message..."
            className="flex-1 rounded-border border-input px-3 py-2 text-sm"
            autoFocus={autoFocus}
          />
          <button
            onClick={handleSend}
            className="bg-accent text-accent-foreground px-3 rounded-md hover:bg-accent/90"
            disabled={!input.trim()}
          >
            Send
          </button>
        </div>
      </div>
    </div>
  );
};

export { ChatWindow };

Props

PropTypeDescription
messages?ChatMessage[]
onSend?(body: string) => void
typing?boolean
autoFocus?boolean