Skip to content
All Taste components

Communication

Chat thread

A conversation with a client: message bubbles, times, an attachment and a reply box.

incoming · outgoing · day divider · attachment · read receipt · composer

PN
Priya Nair
Kaveri Foods · Online
Today
Hi! Could we move the tasting session to Saturday?
10:12
Sure, Saturday at 11:00 works for us.
I have attached the revised menu with the new prices.
PDF
Menu-v3.pdf
420 KB
10:14 · Read
Perfect. See you on Saturday, thank you!
10:20
"use client";
import { useState } from "react";
import { MoreHorizontal, Paperclip, SendHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";

export interface ChatMessage { id: string; from: "me" | "them"; text: string; time: string; read?: boolean }

const sample: ChatMessage[] = [
  { id: "1", from: "them", text: "Hi! Could we move the tasting session to Saturday?", time: "10:12" },
  { id: "2", from: "me", text: "Sure, Saturday at 11:00 works for us.", time: "10:14" },
  { id: "3", from: "me", text: "I have attached the revised menu with the new prices.", time: "10:14", read: true },
  { id: "4", from: "them", text: "Perfect. See you on Saturday, thank you!", time: "10:20" },
];

export function ChatThread({ name = "Priya Nair", subtitle = "Kaveri Foods · Online", messages = sample, onSend }:
  { name?: string; subtitle?: string; messages?: ChatMessage[]; onSend?: (text: string) => void }) {
  const [draft, setDraft] = useState("");
  const send = (e: React.FormEvent) => {
    e.preventDefault();
    if (!draft.trim()) return;
    onSend?.(draft.trim());
    setDraft("");
  };
  const initials = name.split(" ").map((w) => w[0]).join("").slice(0, 2);

  return (
    <section className="flex w-full max-w-[440px] flex-col overflow-hidden rounded-lg border border-border bg-bg">
      <header className="flex items-center gap-2.5 border-b border-border px-4 py-3">
        <span className="relative grid size-9 place-items-center rounded-full bg-primary-soft text-[13px] font-medium text-primary-soft-fg">
          {initials}
          <span className="absolute bottom-0 right-0 size-[9px] rounded-full border-2 border-bg bg-success" aria-hidden />
        </span>
        <div className="min-w-0 flex-1">
          <p className="font-medium">{name}</p>
          <p className="text-[12.5px] text-fg-subtle">{subtitle}</p>
        </div>
        <button aria-label="More options" className="grid size-7 place-items-center rounded text-fg-muted hover:bg-surface-2">
          <MoreHorizontal className="size-4" />
        </button>
      </header>

      <div className="flex flex-col gap-1 bg-surface p-4" role="log" aria-label={"Conversation with " + name}>
        <span className="mb-2 self-center rounded-full border border-border bg-bg px-2.5 py-0.5 text-[11.5px] text-fg-subtle">Today</span>
        {messages.map((m, i) => {
          const mine = m.from === "me";
          const lastOfRun = messages[i + 1]?.from !== m.from;
          return (
            <div key={m.id} className={cn("flex flex-col", mine ? "items-end" : "items-start")}>
              <p className={cn(
                "max-w-[80%] rounded-lg px-3 py-2 text-[13.5px] leading-[1.45] [overflow-wrap:anywhere]",
                mine ? "rounded-br-sm bg-primary text-primary-fg" : "rounded-bl-sm border border-border bg-bg",
              )}>{m.text}</p>
              {lastOfRun && (
                <span className="mx-1 mb-2 mt-1 text-[11px] tabular-nums text-fg-subtle">{m.time}{mine && m.read ? " · Read" : ""}</span>
              )}
            </div>
          );
        })}
      </div>

      <form onSubmit={send} className="flex items-center gap-2 border-t border-border p-3">
        <button type="button" aria-label="Attach a file" className="grid size-9 shrink-0 place-items-center rounded text-fg-muted hover:bg-surface-2">
          <Paperclip className="size-4" />
        </button>
        <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Write a reply…" aria-label="Message"
          className="h-9 min-w-0 flex-1 rounded border border-border-strong bg-bg px-3 text-sm outline-none placeholder:text-fg-subtle focus:border-primary focus:ring-[3px] focus:ring-primary/20" />
        <button type="submit" aria-label="Send" className="grid size-9 shrink-0 place-items-center rounded bg-primary text-primary-fg hover:bg-primary-hover">
          <SendHorizontal className="size-4" />
        </button>
      </form>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.