hiip/ui
14 items

Components / Chat

Message

A chat row and what it can carry: bubble fills, a link preview, a file, a sticker, a time and reactions.

CR
Creator18 days ago
Can I still join the campaign?
Mình cf nha
❤️
OP
Operator18 days ago
You can — briefing is open until Friday.
sample-document-with-a-long-filename.pdfPDF · 1004.3 KB
Seen
🎉
demos.tsx
import {
  Message,
  MessageAvatar,
  MessageBubble,
  MessageBubbleContent,
  MessageContent,
  MessageFile,
  MessageFooter,
  MessageGroup,
  MessageHeader,
  MessageLink,
  MessageReactions,
  MessageSticker,
  MessageTime,
} from "@/components/ui/hiip-ui/message";

// Placeholder marks, not brand assets: every image and icon in these parts is a
// prop, so the demo supplies plain shapes rather than shipping someone's logo
// inside a registry payload.
function PlayPlate() {
  return (
    <div className="flex aspect-video w-full items-center justify-center bg-foreground">
      <svg viewBox="0 0 24 24" className="size-10 fill-background" role="img">
        <title>Video</title>
        <path d="M8 5v14l11-7z" />
      </svg>
    </div>
  );
}

function DocIcon() {
  return (
    <svg viewBox="0 0 24 24" className="fill-primary" role="img">
      <title>Document</title>
      <path d="M6 2h8l4 4v16H6z" opacity="0.25" />
      <path d="M8 12h8v1.5H8zm0 3h8v1.5H8zm0 3h5v1.5H8z" />
    </svg>
  );
}

function MessageDemo() {
  return (
    <MessageGroup className="w-full max-w-sm">
      {/* `variant` on the ROW, not on each bubble: one author, one fill, and a
          grouped run that cannot drift message by message. */}
      <Message variant="received">
        <MessageAvatar className="size-8 text-xs">CR</MessageAvatar>
        <MessageContent>
          {/* Name then time, written once. The header mirrors itself on the
              outgoing row below, so the same order reads correctly on both. */}
          <MessageHeader>
            Creator
            <span className="font-normal text-muted-foreground">
              18 days ago
            </span>
          </MessageHeader>
          <MessageBubble>
            <MessageBubbleContent>
              Can I still join the campaign?
            </MessageBubbleContent>
          </MessageBubble>
          {/* A grouped run: one author line, and only the LAST bubble carries
              a time. */}
          <MessageBubble>
            <MessageBubbleContent>
              Mình cf nha
              <MessageTime dateTime="2026-08-27">18 days ago</MessageTime>
            </MessageBubbleContent>
            {/* No `align`: it follows the row, so the badge hangs on the side
                facing the thread rather than out at the margin. */}
            <MessageReactions>❤️</MessageReactions>
          </MessageBubble>
        </MessageContent>
      </Message>

      <Message align="end" variant="sent">
        <MessageAvatar className="size-8 bg-primary text-xs text-primary-foreground">
          OP
        </MessageAvatar>
        <MessageContent>
          <MessageHeader>
            Operator
            <span className="font-normal text-muted-foreground">
              18 days ago
            </span>
          </MessageHeader>
          <MessageBubble>
            <MessageBubbleContent>
              You can — briefing is open until Friday.
            </MessageBubbleContent>
          </MessageBubble>
          <MessageBubble>
            <MessageBubbleContent>
              <MessageFile
                icon={<DocIcon />}
                name="sample-document-with-a-long-filename.pdf"
                meta="PDF · 1004.3 KB"
                action={
                  <button
                    type="button"
                    aria-label="Download sample-document-with-a-long-filename.pdf"
                    className="flex size-9 items-center justify-center rounded-full bg-foreground text-background"
                  >
                    <svg
                      viewBox="0 0 24 24"
                      className="size-4 fill-current"
                      role="img"
                    >
                      <title>Download</title>
                      <path d="M12 3v10.6l3.3-3.3 1.4 1.4L12 17.4l-4.7-5.7 1.4-1.4 3.3 3.3V3zM5 19h14v2H5z" />
                    </svg>
                  </button>
                }
              />
              <MessageTime dateTime="2026-09-08">6 days ago</MessageTime>
            </MessageBubbleContent>
          </MessageBubble>
          {/* The footer is the STATUS slot, now that the timestamp lives inside
              the bubble. Two different facts, so two different places. */}
          <MessageFooter>Seen</MessageFooter>
        </MessageContent>
      </Message>

      {/* No avatar and no header — a row is still a row without them. */}
      <Message variant="received">
        <MessageContent>
          <MessageBubble>
            <MessageBubbleContent>
              <MessageLink
                href="https://example.com/media/0000000000000000000"
                media={<PlayPlate />}
                site="example.com"
                title="Watch the campaign cut"
                description="A 30-second edit for the brand channel, ready to re-upload."
              />
            </MessageBubbleContent>
          </MessageBubble>
        </MessageContent>
      </Message>

      {/* A lone bubble in the row: `align="end"` reverses the row, so it packs
          right with no content column to put it there. */}
      <Message align="end">
        <MessageBubble>
          <MessageSticker>
            <span className="block p-4 text-5xl leading-none">🎉</span>
            <MessageTime dateTime="2026-08-14">a month ago</MessageTime>
          </MessageSticker>
        </MessageBubble>
      </Message>
    </MessageGroup>
  );
}

Install — needs @hiip registered in components.json

bunx shadcn@latest add @hiip/message

Or by URL, with the token in the query

bunx shadcn@latest add "https://ui.hiip.ai/r/message.json?token=$HIIP_REGISTRY_TOKEN"

Both need a token — set one up.

Anatomy

Import from components/ui/hiip-ui/message.

<MessageGroup>
  {/* `variant` is the row's, so a grouped run cannot drift bubble by bubble */}
  <Message align="start" variant="received">
    <MessageAvatar />
    <MessageContent>
      <MessageHeader />

      {/* one or more, each wearing the row's variant */}
      <MessageBubble>
        <MessageBubbleContent>
          {/* text, or one of the parts below */}
          <MessageLink href="…" media={…} site="…" title="…" description="…" />
          <MessageFile icon={…} name="…" meta="…" action={…} />
          <MessageTime dateTime="…">…</MessageTime>
        </MessageBubbleContent>
        <MessageReactions>👍</MessageReactions>
      </MessageBubble>

      <MessageBubble>
        <MessageSticker>
          {/* a sticker or emoji, instead of MessageBubbleContent */}
        </MessageSticker>
      </MessageBubble>

      <MessageFooter />
    </MessageContent>
  </Message>
</MessageGroup>

API Reference

Generated from the source. Props forwarded to the underlying element are not listed.

MessageGroup

No props of its own.

Message

PropTypeDefault
align"end" | "start" | undefined"start"
variant

The fill every bubble in this row wears.

MessageVariant | undefined"default"

MessageAvatar

No props of its own.

MessageContent

No props of its own.

MessageHeader

No props of its own.

MessageFooter

No props of its own.

MessageBubble

One bubble in a row.

PropTypeDefault
variant

Overrides the row's variant.

MessageVariant | undefined—

MessageBubbleContent

PropTypeDefault
render

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}> | undefined—

MessageReactions

PropTypeDefault
align

`auto` follows the row's own side.

"auto" | "end" | "start" | undefined"auto"
side"top" | "bottom" | undefined"bottom"

MessageTime

The in-bubble timestamp: the last message of a group carries its own time INSIDE the fill, rather than pushing another line under the thread.

No props of its own.

A link preview: an optional media plate, the raw URL, then the site's own metadata.

PropTypeDefault
descriptionReactNode—
icon

The favicon or platform mark beside the title.

ReactNode—
media

Full-bleed plate above the URL — a thumbnail, a player, a placeholder.

ReactNode—
siteReactNode—
titleReactNode—

MessageFile

An attachment row: icon tile, name and size, and whatever the caller wants the trailing control to be.

PropTypeDefault
namerequiredReactNode—
actionReactNode—
iconReactNode—
meta

Type and size, e.g. `PDF · 1004.3 KB`.

ReactNode—

MessageSticker

A sticker or an emoji sent on its own.

No props of its own.

Type
registry:ui
npm packages
@base-ui/react class-variance-authority
Files written
  • components/ui/hiip-ui/message.tsx