# Message

> A chat message row with an avatar, name, bubbles and status, where new messages rise in from the sender's side.

Docs: https://hextaui.com/docs/message
Markdown: https://hextaui.com/docs/message.md

```tsx title="components/examples/message/demo.tsx"
"use client"

import * as React from "react"
import { IconArrowUp, IconChecks } from "@tabler/icons-react"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupTextarea,
} from "@/components/ui/input-group"
import { Marker, MarkerContent, MarkerTime } from "@/components/ui/marker"
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageGroup,
  MessageHeader,
} from "@/components/ui/message"

type Entry = { id: number; from: "me" | "preet"; text: string }

const replies = [
  "Ha, that rise-in is nice.",
  "Try sending a few in a row.",
  "Each one comes in from your side.",
]

export function MessageDemo() {
  const [entries, setEntries] = React.useState<Entry[]>([
    { id: 1, from: "preet", text: "Did the message component land?" },
    { id: 2, from: "preet", text: "Send me something and see how it arrives." },
  ])
  const [draft, setDraft] = React.useState("")
  const nextId = React.useRef(3)
  const replyIndex = React.useRef(0)
  const replyTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined)
  const threadRef = React.useRef<HTMLDivElement>(null)

  React.useEffect(() => () => clearTimeout(replyTimer.current), [])

  React.useEffect(() => {
    const thread = threadRef.current
    if (thread) {
      thread.scrollTo({ top: thread.scrollHeight, behavior: "smooth" })
    }
  }, [entries])

  const send = () => {
    const text = draft.trim()
    if (!text) {
      return
    }
    setEntries((current) => [
      ...current,
      { id: nextId.current++, from: "me", text },
    ])
    setDraft("")
    clearTimeout(replyTimer.current)
    replyTimer.current = setTimeout(() => {
      const reply = replies[replyIndex.current % replies.length]
      replyIndex.current += 1
      setEntries((current) => [
        ...current,
        { id: nextId.current++, from: "preet", text: reply },
      ])
    }, 900)
  }

  const runs = entries.reduce<Entry[][]>((groups, entry) => {
    const last = groups.at(-1)
    if (last && last[0].from === entry.from) {
      last.push(entry)
    } else {
      groups.push([entry])
    }
    return groups
  }, [])

  return (
    <div className="flex w-full max-w-md flex-col overflow-hidden rounded-xl border">
      <div
        ref={threadRef}
        className="h-80 overflow-y-auto overscroll-none px-4 py-4"
      >
        <MessageGroup>
          <Marker variant="separator">
            <MarkerContent>
              <MarkerTime date={new Date()} />
            </MarkerContent>
          </Marker>
          {runs.map((run, index) => {
            const mine = run[0].from === "me"
            const lastRun = index === runs.length - 1
            return (
              <Message key={run[0].id} align={mine ? "end" : "start"}>
                {!mine && (
                  <MessageAvatar>
                    <Avatar>
                      <AvatarImage
                        src="https://github.com/preetsuthar17.png"
                        alt=""
                      />
                      <AvatarFallback>PS</AvatarFallback>
                    </Avatar>
                  </MessageAvatar>
                )}
                <MessageContent>
                  {!mine && <MessageHeader>Preet</MessageHeader>}
                  <BubbleGroup shape="tail">
                    {run.map((entry) => (
                      <Bubble
                        key={entry.id}
                        variant={mine ? "default" : "secondary"}
                      >
                        <BubbleContent>{entry.text}</BubbleContent>
                      </Bubble>
                    ))}
                  </BubbleGroup>
                  {mine && lastRun && (
                    <MessageFooter>
                      <IconChecks />
                      Read
                    </MessageFooter>
                  )}
                </MessageContent>
              </Message>
            )
          })}
        </MessageGroup>
      </div>
      <form
        className="border-t p-3"
        onSubmit={(event) => {
          event.preventDefault()
          send()
        }}
      >
        <InputGroup>
          <InputGroupTextarea
            value={draft}
            onChange={(event) => setDraft(event.target.value)}
            onKeyDown={(event) => {
              if (
                event.key === "Enter" &&
                !event.shiftKey &&
                !event.nativeEvent.isComposing
              ) {
                event.preventDefault()
                send()
              }
            }}
            rows={1}
            placeholder="Message Preet"
            aria-label="Message"
            className="min-h-10"
          />
          <InputGroupAddon align="inline-end">
            <InputGroupButton
              type="submit"
              variant="default"
              size="icon-xs"
              aria-label="Send"
              disabled={!draft.trim()}
            >
              <IconArrowUp />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </form>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/message.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/message.tsx"
"use client"

import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cn } from "cn"

import { BubbleAlignContext } from "@/components/ui/bubble"

type MessageAlign = "start" | "end"

const MessageGroupContext = React.createContext<{ ready: boolean } | null>(null)

function dataAttributes(slot: string, extra?: Record<string, string>) {
  return { "data-slot": slot, ...extra } as Record<string, string>
}

function MessageGroup({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  const [ready, setReady] = React.useState(false)

  React.useEffect(() => {
    setReady(true)
  }, [])

  const context = React.useMemo(() => ({ ready }), [ready])

  const element = useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      { className: cn("flex min-w-0 flex-col gap-4", className) },
      props,
      dataAttributes("message-group")
    ),
  })

  return (
    <MessageGroupContext.Provider value={context}>
      {element}
    </MessageGroupContext.Provider>
  )
}

type MessageProps = useRender.ComponentProps<"div"> & {
  align?: MessageAlign
  animated?: boolean
}

function Message({
  className,
  align = "start",
  animated = true,
  render,
  children,
  ...props
}: MessageProps) {
  const group = React.useContext(MessageGroupContext)
  const [entering] = React.useState(() => Boolean(animated && group?.ready))

  const element = useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "group/message relative flex w-full min-w-0 items-end gap-2 text-sm data-entering:origin-bottom-left data-[align=end]:flex-row-reverse data-[align=end]:data-entering:origin-bottom-right data-entering:motion-safe:animate-message-in motion-reduce:data-entering:animate-in motion-reduce:data-entering:fade-in-0 rtl:data-entering:origin-bottom-right rtl:data-[align=end]:data-entering:origin-bottom-left",
          className
        ),
        children: (
          <BubbleAlignContext.Provider value={align}>
            {children}
          </BubbleAlignContext.Provider>
        ),
      },
      props,
      dataAttributes("message", {
        "data-align": align,
        ...(entering ? { "data-entering": "" } : {}),
      })
    ),
  })

  return element
}

function MessageAvatar({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full group-has-[[data-slot=message-footer]]/message:mb-[calc(var(--spacing)*4+var(--spacing)*1.5)] [&>img]:size-8 [&>img]:object-cover [&>svg]:size-8 [&>svg]:rounded-full [&>svg]:bg-muted [&>svg]:p-2 [&>svg]:text-foreground",
          className
        ),
      },
      props,
      dataAttributes("message-avatar")
    ),
  })
}

function MessageContent({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "flex w-full min-w-0 flex-col gap-1.5 wrap-break-word",
          className
        ),
      },
      props,
      dataAttributes("message-content")
    ),
  })
}

const metaClassName =
  "flex max-w-full min-w-0 items-center gap-1.5 px-3 text-xs font-medium text-muted-foreground group-has-[[data-variant=ghost]]/message:px-0 group-data-[align=start]/message:group-has-[[data-shape=tail]]/message:ps-[calc(var(--spacing)*4.5)] group-data-[align=end]/message:group-has-[[data-shape=tail]]/message:pe-[calc(var(--spacing)*4.5)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5"

function MessageHeader({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          metaClassName,
          "min-h-4 group-data-[align=end]/message:justify-end",
          className
        ),
      },
      props,
      dataAttributes("message-header")
    ),
  })
}

function MessageFooter({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          metaClassName,
          "h-4 overflow-hidden whitespace-nowrap group-data-[align=end]/message:justify-end",
          className
        ),
      },
      props,
      dataAttributes("message-footer")
    ),
  })
}

export {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageGroup,
  MessageHeader,
}
export type { MessageAlign, MessageProps }
```

```tsx title="components/ui/bubble.tsx"
"use client"

import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

type BubbleShape = "uniform" | "joined" | "tail"

const BubbleShapeContext = React.createContext<BubbleShape | undefined>(
  undefined
)

const BubbleAlignContext = React.createContext<"start" | "end" | undefined>(
  undefined
)

function dataAttributes(slot: string, extra?: Record<string, string>) {
  return { "data-slot": slot, ...extra } as Record<string, string>
}

type BubbleGroupProps = useRender.ComponentProps<"div"> & {
  shape?: BubbleShape
}

function BubbleGroup({ className, shape, render, ...props }: BubbleGroupProps) {
  const element = useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      { className: cn("flex min-w-0 flex-col gap-1", className) },
      props,
      dataAttributes("bubble-group", shape ? { "data-shape": shape } : {})
    ),
  })

  return (
    <BubbleShapeContext.Provider value={shape}>
      {element}
    </BubbleShapeContext.Provider>
  )
}

const bubbleVariants = cva(
  "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 [--bubble-bg:transparent] [--bubble-border:transparent] [--bubble-fg:currentColor] [--bubble-radius-bottom:var(--bubble-radius)] [--bubble-radius-joined:var(--radius-sm)] [--bubble-radius-top:var(--bubble-radius)] [--bubble-radius:var(--radius-xl)] [--bubble-tail:none] group-data-[align=end]/message:ms-auto in-data-[slot=bubble-content]:[--bubble-radius:max(calc(var(--radius-sm)*0.5),calc(var(--bubble-content-radius)-var(--spacing)*2))]",
  {
    variants: {
      variant: {
        default:
          "[--bubble-bg:var(--color-primary)] [--bubble-fg:var(--color-primary-foreground)]",
        secondary:
          "[--bubble-bg:var(--color-secondary)] [--bubble-fg:var(--color-secondary-foreground)]",
        muted:
          "[--bubble-bg:var(--color-muted)] [--bubble-fg:var(--color-foreground)]",
        tinted:
          "[--bubble-bg:oklch(from_var(--color-primary)_0.94_calc(c*0.4)_h)] [--bubble-fg:var(--color-foreground)] dark:[--bubble-bg:oklch(from_var(--color-primary)_0.3_calc(c*0.4)_h)]",
        outline:
          "[--bubble-bg:var(--color-background)] [--bubble-border:var(--color-border)] [--bubble-fg:var(--color-foreground)] dark:[--bubble-border:var(--color-input)]",
        ghost:
          "max-w-full [--bubble-radius:var(--radius-md)] *:data-[slot=bubble-content]:p-0",
        destructive:
          "[--bubble-accent:var(--color-destructive)] [--bubble-bg:color-mix(in_oklab,var(--color-destructive)_10%,var(--color-background))] [--bubble-fg:var(--color-foreground)] dark:[--bubble-bg:color-mix(in_oklab,var(--color-destructive)_18%,var(--color-background))]",
      },
      align: {
        start:
          "[--bubble-tail-flip:-1] *:data-[slot=bubble-content]:rounded-ss-(--bubble-radius-top) *:data-[slot=bubble-content]:rounded-es-(--bubble-radius-bottom) rtl:[--bubble-tail-flip:1]",
        end: "ms-auto [--bubble-tail-flip:1] *:data-[slot=bubble-content]:self-end *:data-[slot=bubble-content]:rounded-se-(--bubble-radius-top) *:data-[slot=bubble-content]:rounded-ee-(--bubble-radius-bottom) rtl:[--bubble-tail-flip:-1]",
      },
      shape: {
        uniform: "",
        joined: "",
        tail: "[--bubble-tail-cut:calc(var(--spacing)*2)] [--bubble-tail-height:calc(var(--spacing)*5)] [--bubble-tail-overlap:calc(var(--spacing)*2.5)] [--bubble-tail-width:calc(var(--spacing)*1.5)] [--bubble-tail:block] *:data-[slot=bubble-content]:relative *:data-[slot=bubble-content]:overflow-visible *:data-[slot=bubble-content]:after:pointer-events-none *:data-[slot=bubble-content]:after:absolute *:data-[slot=bubble-content]:after:bottom-0 *:data-[slot=bubble-content]:after:[display:var(--bubble-tail)] *:data-[slot=bubble-content]:after:h-(--bubble-tail-height) *:data-[slot=bubble-content]:after:w-[calc(var(--bubble-tail-width)+var(--bubble-tail-overlap))] *:data-[slot=bubble-content]:after:[scale:var(--bubble-tail-flip)_1] *:data-[slot=bubble-content]:after:rounded-bl-[calc(var(--spacing)*3.25)_calc(var(--spacing)*2.75)] *:data-[slot=bubble-content]:after:bg-(--bubble-bg) *:data-[slot=bubble-content]:after:[mask-image:linear-gradient(to_right,black_var(--bubble-tail-overlap),transparent_var(--bubble-tail-overlap)),radial-gradient(circle_var(--bubble-tail-cut)_at_100%_0,transparent_var(--bubble-tail-cut),black_calc(var(--bubble-tail-cut)+0.5px))] *:data-[slot=bubble-content]:after:[mask-size:100%_100%,var(--bubble-tail-cut)_var(--bubble-tail-cut)] *:data-[slot=bubble-content]:after:[mask-position:0_0,var(--bubble-tail-overlap)_100%] *:data-[slot=bubble-content]:after:[mask-repeat:no-repeat]",
      },
    },
    compoundVariants: [
      {
        shape: "joined",
        align: "start",
        className:
          "[[data-slot=bubble-group]>&:has(+[data-slot=bubble][data-align=start])]:[--bubble-radius-bottom:var(--bubble-radius-joined)] [[data-slot=bubble-group]>[data-slot=bubble][data-align=start]+&]:[--bubble-radius-top:var(--bubble-radius-joined)]",
      },
      {
        shape: "joined",
        align: "end",
        className:
          "[[data-slot=bubble-group]>&:has(+[data-slot=bubble][data-align=end])]:[--bubble-radius-bottom:var(--bubble-radius-joined)] [[data-slot=bubble-group]>[data-slot=bubble][data-align=end]+&]:[--bubble-radius-top:var(--bubble-radius-joined)]",
      },
      {
        shape: "tail",
        align: "start",
        className:
          "ps-(--bubble-tail-width) *:data-[slot=bubble-content]:after:start-[calc(var(--bubble-tail-width)*-1)] [[data-slot=bubble-group]>&:has(+[data-slot=bubble][data-align=start])]:[--bubble-tail:none]",
      },
      {
        shape: "tail",
        align: "end",
        className:
          "pe-(--bubble-tail-width) *:data-[slot=bubble-content]:after:end-[calc(var(--bubble-tail-width)*-1)] [[data-slot=bubble-group]>&:has(+[data-slot=bubble][data-align=end])]:[--bubble-tail:none]",
      },
      {
        shape: "tail",
        variant: ["outline", "ghost"],
        className: "[--bubble-tail:none]",
      },
    ],
    defaultVariants: {
      variant: "default",
      align: "start",
      shape: "joined",
    },
  }
)

type BubbleVariant = NonNullable<VariantProps<typeof bubbleVariants>["variant"]>
type BubbleAlign = NonNullable<VariantProps<typeof bubbleVariants>["align"]>

type BubbleProps = useRender.ComponentProps<"div"> & {
  variant?: BubbleVariant
  align?: BubbleAlign
  shape?: BubbleShape
}

function Bubble({
  className,
  variant = "default",
  align: alignProp,
  shape: shapeProp,
  render,
  ...props
}: BubbleProps) {
  const groupShape = React.useContext(BubbleShapeContext)
  const inheritedAlign = React.useContext(BubbleAlignContext)
  const align = alignProp ?? inheritedAlign ?? "start"
  const shape = shapeProp ?? groupShape ?? "joined"

  const element = useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      { className: cn(bubbleVariants({ variant, align, shape }), className) },
      props,
      dataAttributes("bubble", {
        "data-variant": variant,
        "data-align": align,
        "data-shape": shape,
      })
    ),
  })

  return (
    <BubbleShapeContext.Provider value={undefined}>
      {element}
    </BubbleShapeContext.Provider>
  )
}

function BubbleContent({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "w-fit max-w-full min-w-0 overflow-hidden rounded-(--bubble-radius) bg-(--bubble-bg) px-3 py-2 text-sm leading-relaxed wrap-anywhere text-(--bubble-fg) inset-ring-(length:--hairline) inset-ring-(--bubble-border) outline-none [--bubble-content-radius:var(--bubble-radius)] focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden forced-colors:border [&:is(button,a)]:text-start [&>svg]:inline-block [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:align-[-0.1875rem] [&>svg]:text-(--bubble-accent,currentColor)",
          className
        ),
      },
      props,
      dataAttributes("bubble-content")
    ),
  })
}

const bubbleReactionsVariants = cva(
  "relative z-10 flex w-max max-w-[calc(100%-var(--spacing)*3)] flex-wrap items-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm leading-5 ring-3 ring-background select-none has-[>button]:p-0 *:data-[slot=button]:rounded-full",
  {
    variants: {
      side: {
        top: "order-first -mb-2.5",
        bottom: "-mt-2.5",
      },
      align: {
        start: "ms-3 self-start",
        end: "me-3 self-end",
      },
    },
    defaultVariants: {
      side: "bottom",
      align: "end",
    },
  }
)

type BubbleReactionsProps = useRender.ComponentProps<"div"> & {
  side?: NonNullable<VariantProps<typeof bubbleReactionsVariants>["side"]>
  align?: NonNullable<VariantProps<typeof bubbleReactionsVariants>["align"]>
}

function BubbleReactions({
  className,
  side = "bottom",
  align = "end",
  render,
  ...props
}: BubbleReactionsProps) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(bubbleReactionsVariants({ side, align }), className),
      },
      props,
      dataAttributes("bubble-reactions", {
        "data-side": side,
        "data-align": align,
      })
    ),
  })
}

export {
  Bubble,
  BubbleAlignContext,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
  bubbleReactionsVariants,
  bubbleVariants,
}
export type {
  BubbleAlign,
  BubbleGroupProps,
  BubbleProps,
  BubbleReactionsProps,
  BubbleShape,
  BubbleVariant,
}
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageGroup,
  MessageHeader,
} from "@/components/ui/message"
```

```tsx
<MessageGroup>
  <Message align="end">
    <MessageContent>
      <Bubble>
        <BubbleContent>On my way.</BubbleContent>
      </Bubble>
      <MessageFooter>Read</MessageFooter>
    </MessageContent>
  </Message>
</MessageGroup>
```

A message lays out an avatar, a name, one or more `<Bubble />`s and a status line. Bubbles inherit the message's `align`, so you set it once.

## Composition

```text
MessageGroup
└── Message
    ├── MessageAvatar
    └── MessageContent
        ├── MessageHeader
        ├── Bubble
        └── MessageFooter
```

## Examples

### Alignment

`align="end"` puts the avatar and bubbles on the other side for your own messages.

```tsx title="components/examples/message/alignment.tsx"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageGroup,
} from "@/components/ui/message"

export function MessageAlignment() {
  return (
    <MessageGroup className="w-full max-w-md">
      <Message>
        <MessageAvatar>
          <Avatar>
            <AvatarImage src="https://github.com/preetsuthar17.png" alt="" />
            <AvatarFallback>PS</AvatarFallback>
          </Avatar>
        </MessageAvatar>
        <MessageContent>
          <Bubble variant="secondary" shape="tail">
            <BubbleContent>Start is for the people you talk to.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message align="end">
        <MessageAvatar>
          <Avatar>
            <AvatarFallback>ME</AvatarFallback>
          </Avatar>
        </MessageAvatar>
        <MessageContent>
          <Bubble shape="tail">
            <BubbleContent>End is for you. The bubble follows.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
    </MessageGroup>
  )
}
```

### Header and footer

`<MessageHeader />` holds the name and time, and `<MessageFooter />` a single line of status. The avatar always lines up with the last bubble, with or without a footer.

```tsx title="components/examples/message/header-footer.tsx"
import { IconChecks, IconClock } from "@tabler/icons-react"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageGroup,
  MessageHeader,
} from "@/components/ui/message"

export function MessageHeaderFooter() {
  return (
    <MessageGroup className="w-full max-w-md">
      <Message>
        <MessageAvatar>
          <Avatar>
            <AvatarImage src="https://github.com/preetsuthar17.png" alt="" />
            <AvatarFallback>PS</AvatarFallback>
          </Avatar>
        </MessageAvatar>
        <MessageContent>
          <MessageHeader>
            Preet Suthar
            <span className="font-normal">9:41</span>
          </MessageHeader>
          <BubbleGroup shape="tail">
            <Bubble variant="secondary">
              <BubbleContent>Pushed the fix.</BubbleContent>
            </Bubble>
            <Bubble variant="secondary">
              <BubbleContent>Can you check the preview?</BubbleContent>
            </Bubble>
          </BubbleGroup>
          <MessageFooter>Edited</MessageFooter>
        </MessageContent>
      </Message>
      <Message align="end">
        <MessageContent>
          <Bubble shape="tail">
            <BubbleContent>Looks right. Merging.</BubbleContent>
          </Bubble>
          <MessageFooter>
            <IconChecks />
            Read 9:43
          </MessageFooter>
        </MessageContent>
      </Message>
      <Message align="end">
        <MessageContent>
          <Bubble variant="muted" shape="tail">
            <BubbleContent>Sending the release notes…</BubbleContent>
          </Bubble>
          <MessageFooter>
            <IconClock />
            Sending
          </MessageFooter>
        </MessageContent>
      </Message>
    </MessageGroup>
  )
}
```

### Assistant

A `ghost` bubble spans the full width for long answers, and an icon in `<MessageAvatar />` becomes a round tile.

```tsx title="components/examples/message/assistant.tsx"
import { IconSparkles } from "@tabler/icons-react"

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageGroup,
  MessageHeader,
} from "@/components/ui/message"

export function MessageAssistant() {
  return (
    <MessageGroup className="w-full max-w-md">
      <Message align="end">
        <MessageContent>
          <Bubble variant="secondary">
            <BubbleContent>What does a concentric radius mean?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageAvatar>
          <IconSparkles />
        </MessageAvatar>
        <MessageContent>
          <MessageHeader>Assistant</MessageHeader>
          <Bubble variant="ghost">
            <BubbleContent>
              An inner corner whose radius is the outer radius minus the gap
              between the two edges. Nested shapes then curve together instead
              of fighting each other at the corners.
            </BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
    </MessageGroup>
  )
}
```

### Long content

Bubbles wrap long links, and header and footer text can truncate without pushing the avatar.

```tsx title="components/examples/message/long-content.tsx"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageHeader,
} from "@/components/ui/message"

export function MessageLongContent() {
  return (
    <div className="w-72 max-w-full">
      <Message>
        <MessageAvatar>
          <Avatar>
            <AvatarImage src="https://github.com/preetsuthar17.png" alt="" />
            <AvatarFallback>PS</AvatarFallback>
          </Avatar>
        </MessageAvatar>
        <MessageContent>
          <MessageHeader>
            <span className="truncate">
              Preet Suthar from the HextaUI design systems team
            </span>
          </MessageHeader>
          <Bubble variant="secondary">
            <BubbleContent>
              https://example.com/a/very/long/link/that/does/not/break/naturally/at/all
            </BubbleContent>
          </Bubble>
          <MessageFooter>
            <span className="truncate">
              Delivered to every device signed in to this workspace
            </span>
          </MessageFooter>
        </MessageContent>
      </Message>
    </div>
  )
}
```

### Right to left

Start and end swap sides, and new messages grow from the matching corner.

```tsx title="components/examples/message/rtl.tsx"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageGroup,
  MessageHeader,
} from "@/components/ui/message"

export function MessageRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md">
      <MessageGroup>
        <Message>
          <MessageAvatar>
            <Avatar>
              <AvatarImage src="https://github.com/preetsuthar17.png" alt="" />
              <AvatarFallback>م</AvatarFallback>
            </Avatar>
          </MessageAvatar>
          <MessageContent>
            <MessageHeader>ميرا</MessageHeader>
            <Bubble variant="secondary">
              <BubbleContent>هل وصل المكون الجديد؟</BubbleContent>
            </Bubble>
          </MessageContent>
        </Message>
        <Message align="end">
          <MessageContent>
            <Bubble>
              <BubbleContent>نعم، إنه على الصفحة الآن.</BubbleContent>
            </Bubble>
            <MessageFooter>تمت القراءة</MessageFooter>
          </MessageContent>
        </Message>
      </MessageGroup>
    </div>
  )
}
```

## Accessibility

- Messages are plain content. For a live chat, wrap the list in a `role="log"` element so screen readers read new messages as they arrive.
- Put the sender's name in a header or in visually hidden text, since alignment alone doesn't say who wrote a message.
- The entrance only plays for messages added after the list first renders. With reduced motion they fade in.

## API reference

Every part renders a `<div>` and accepts `render`.

### MessageGroup

The thread. Messages added after its first render animate in.

| Attribute | Description |
| --- | --- |
| `data-slot="message-group"` | Target the thread in CSS. |

### Message

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "end"` | `"start"` | Side of the conversation. Bubbles inherit it. |
| `animated` | `boolean` | `true` | Rise in when added to a MessageGroup after its first render. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="message"` | Target messages in CSS. |
| `data-align` | The side. |
| `data-entering` | Present on messages that animate in. |

### MessageAvatar

| Attribute | Description |
| --- | --- |
| `data-slot="message-avatar"` | Lines up with the last bubble. A bare icon or image is sized to 32px. |

### MessageContent

| Attribute | Description |
| --- | --- |
| `data-slot="message-content"` | Stacks the header, bubbles and footer. |

### MessageHeader and MessageFooter

| Attribute | Description |
| --- | --- |
| `data-slot="message-header"` | Name and time above the bubbles. |
| `data-slot="message-footer"` | One line of status below. Keep it to a single line so the avatar stays aligned. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
