# Support thread with sender alignment

> A two sided billing thread where align and variant carry the sender.

A shadcn/ui block from the [@reui](https://reui.io) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/reui/c-bubble-1).

- Page, with a live preview: https://shadcn.exchange/r/reui/c-bubble-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/reui/c-bubble-1-light-707fdb7a288f1359.png
- Registry item: https://reui.io/r/new-york-v4/c-bubble-1.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://reui.io/r/new-york-v4/c-bubble-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `avatar`, `bubble`, `card`

## Files

### `c-bubble-1.tsx`

```tsx
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

// The sender is carried by the variant and the side together. Colour alone
// leaves anyone who cannot separate the two shades with an unreadable thread,
// and a side alone reads as decoration rather than as authorship.
const thread: { id: string; from: "agent" | "you"; text: string }[] = [
  {
    id: "m1",
    from: "you",
    text: "Invoice INV-4417 was charged twice on Tuesday. Can you take a look?",
  },
  {
    id: "m2",
    from: "agent",
    text: "Checking now. I can see two authorisations against INV-4417, both for $248.00.",
  },
  {
    id: "m3",
    from: "agent",
    text: "The second one was a retry after the processor timed out, so only the first should have settled.",
  },
  {
    id: "m4",
    from: "you",
    text: "That matches the bank statement. Please refund the retry.",
  },
  {
    id: "m5",
    from: "agent",
    text: "Refunded. It leaves us today and reaches your card in 3 to 5 business days.",
  },
]

export function Pattern() {
  return (
    <Card className="w-full max-w-sm">
      {/* CardHeader is a grid by default, so the flex override is what puts the
          avatar and the name on one row, and border-b is what switches on the
          header's own bottom-padding rung. */}
      <CardHeader className="flex flex-row items-center gap-3 border-b">
        <Avatar size="sm">
          <AvatarImage
            src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=96&h=96&dpr=2&q=80"
            alt="Lena Ortiz"
          />
          <AvatarFallback>LO</AvatarFallback>
        </Avatar>
        {/* The header's gap reaches its own children, not these two, so the
            column needs a gap or the two lines render flush in every style. */}
        <div className="flex min-w-0 flex-col gap-1">
          <CardTitle>Lena Ortiz</CardTitle>
          <CardDescription>
            Billing support, replies in a few minutes
          </CardDescription>
        </div>
      </CardHeader>
      {/* align="end" resolves to align-self, so the turns have to sit in a flex
          column. In a grid or a plain block every outbound turn silently stays
          left. */}
      <CardContent className="flex flex-col gap-3">
        {thread.map((turn) => (
          <Bubble
            key={turn.id}
            align={turn.from === "you" ? "end" : "start"}
            variant={turn.from === "you" ? "default" : "muted"}
          >
            <BubbleContent>
              {/* Side and variant are both purely visual, so the turn names its
                  own sender for a reader who receives neither. */}
              <span className="sr-only">
                {turn.from === "you" ? "You said: " : "Lena said: "}
              </span>
              {turn.text}
            </BubbleContent>
          </Bubble>
        ))}
      </CardContent>
    </Card>
  )
}
```
