View as Markdown

Message

Displays a message in a conversation, with optional avatar, header, footer, and alignment.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-6 py-12">
    <twig:Message align="end">
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/shadcn.png" alt="@shadcn" />
                <twig:Avatar:Fallback>ME</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>Deploying to prod real quick.</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message>
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                <twig:Avatar:Fallback>R</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble variant="muted">
                <twig:Bubble:Content>It's 4:55 PM. On a Friday.</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message align="end">
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/shadcn.png" alt="@shadcn" />
                <twig:Avatar:Fallback>ME</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>It's a one-line change.</twig:Bubble:Content>
            </twig:Bubble>
            <twig:Message:Footer>Delivered</twig:Message:Footer>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message>
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                <twig:Avatar:Fallback>R</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble:Group>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>It's always a one-line change 😭.</twig:Bubble:Content>
                </twig:Bubble>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>Alright, let me take a look.</twig:Bubble:Content>
                    <twig:Bubble:Reactions role="img" aria-label="Reaction: thumbs up">
                        <span>👍</span>
                    </twig:Bubble:Reactions>
                </twig:Bubble>
            </twig:Bubble:Group>
        </twig:Message:Content>
    </twig:Message>
    <twig:Marker role="status">
        <twig:Marker:Content class="animate-pulse">
            <span class="font-medium">Oliver</span> is typing...
        </twig:Marker:Content>
    </twig:Marker>
</div>

Installation

Note

Available since UX Toolkit 3.5.

php bin/console ux:install message --kit shadcn

Install the following Composer dependencies:

composer require twig/html-extra:^3.24.0 symfony/ux-twig-component:^3.5 tales-from-a-dev/twig-tailwind-extra:^1.3.0

Copy the following file(s) into your app:

templates/components/Message.html.twig
{%- props
    ## 'start'|'end' The alignment of the message in the conversation.
    align = 'start'
-%}
<div
    data-slot="message"
    data-align="{{ align }}"
    {{ attributes.defaults({
        class: 'group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse'|tailwind_classes,
    }) }}
>
    {##- The message row, typically includes `Message:Avatar` and `Message:Content`. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/Message/Avatar.html.twig
<div
    data-slot="message-avatar"
    {{ attributes.defaults({
        class: 'flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8'|tailwind_classes,
    }) }}
>
    {##- The avatar of the message sender, typically an `Avatar` component. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/Message/Content.html.twig
<div
    data-slot="message-content"
    {{ attributes.defaults({
        class: 'flex w-full min-w-0 flex-col gap-2.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end'|tailwind_classes,
    }) }}
>
    {##- The message content, typically includes `Message:Header`, a `Bubble` and `Message:Footer`. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/Message/Footer.html.twig
<div
    data-slot="message-footer"
    {{ attributes.defaults({
        class: 'flex max-w-full min-w-0 items-center px-3 text-xs font-medium text-muted-foreground group-data-[align=end]/message:justify-end group-has-data-[variant=ghost]/message:px-0'|tailwind_classes,
    }) }}
>
    {##- The content displayed below the message, such as a delivery status or message actions. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/Message/Group.html.twig
<div
    data-slot="message-group"
    {{ attributes.defaults({
        class: 'flex min-w-0 flex-col gap-2'|tailwind_classes,
    }) }}
>
    {##- The grouped messages, typically multiple `Message` components from the same sender. -#}
    {%- block content %}{% endblock -%}
</div>
templates/components/Message/Header.html.twig
<div
    data-slot="message-header"
    {{ attributes.defaults({
        class: 'flex max-w-full min-w-0 items-center px-3 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0'|tailwind_classes,
    }) }}
>
    {##- The content displayed above the message, such as the sender name. -#}
    {%- block content %}{% endblock -%}
</div>

Usage

<twig:Message align="start | end">
    <twig:Message:Avatar>
        <twig:Avatar>
            <twig:Avatar:Image src="https://github.com/shadcn.png" alt="@shadcn" />
            <twig:Avatar:Fallback>CN</twig:Avatar:Fallback>
        </twig:Avatar>
    </twig:Message:Avatar>
    <twig:Message:Content>
        <twig:Message:Header>Olivia</twig:Message:Header>
        <twig:Bubble variant="muted">
            <twig:Bubble:Content>How can I help you today?</twig:Bubble:Content>
        </twig:Bubble>
        <twig:Message:Footer>Delivered</twig:Message:Footer>
    </twig:Message:Content>
</twig:Message>

Message owns the row layout — avatar, alignment, header and footer. Render the visible message surface inside it with Bubble.

Examples

Avatar

Use Message:Avatar to render an avatar next to the message. Set align="end" on the message to align the avatar to the end of the conversation.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-6 py-12">
    <twig:Message>
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                <twig:Avatar:Fallback>R</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble variant="muted">
                <twig:Bubble:Content>The build failed during dependency installation.</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message align="end">
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/shadcn.png" alt="@shadcn" />
                <twig:Avatar:Fallback>CN</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>Can you share the exact error?</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message>
        <twig:Message:Avatar>
            <twig:Avatar>
                <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                <twig:Avatar:Fallback>R</twig:Avatar:Fallback>
            </twig:Avatar>
        </twig:Message:Avatar>
        <twig:Message:Content>
            <twig:Bubble:Group>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>Here's the error from the logs</twig:Bubble:Content>
                </twig:Bubble>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>
                        Something went wrong with the build. The libraries are not installed correctly. Try running the build again.
                    </twig:Bubble:Content>
                </twig:Bubble>
            </twig:Bubble:Group>
        </twig:Message:Content>
    </twig:Message>
</div>

Group

Use Message:Group to stack consecutive messages from the same sender. Render an empty Message:Avatar on the earlier messages to keep them aligned with the avatar on the last one.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-6 py-12">
    <twig:Message:Group>
        <twig:Message>
            <twig:Message:Avatar />
            <twig:Message:Content>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>I checked the registry addresses.</twig:Bubble:Content>
                </twig:Bubble>
            </twig:Message:Content>
        </twig:Message>
        <twig:Message>
            <twig:Message:Avatar>
                <twig:Avatar>
                    <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                    <twig:Avatar:Fallback>CN</twig:Avatar:Fallback>
                </twig:Avatar>
            </twig:Message:Avatar>
            <twig:Message:Content>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>The component and example JSON now live under the UI registry.</twig:Bubble:Content>
                </twig:Bubble>
            </twig:Message:Content>
        </twig:Message>
    </twig:Message:Group>
</div>

Use Message:Header for a sender name and Message:Footer for metadata such as a delivery or read status.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Message>
        <twig:Message:Content>
            <twig:Message:Header>Olivia</twig:Message:Header>
            <twig:Bubble variant="muted">
                <twig:Bubble:Content>I already checked the logs.</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message align="end">
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>Send the report to the team. Ping @shadcn if you need help.</twig:Bubble:Content>
            </twig:Bubble>
            <twig:Message:Footer>
                <div>
                    Read <span class="font-normal">Yesterday</span>
                </div>
            </twig:Message:Footer>
        </twig:Message:Content>
    </twig:Message>
</div>

Actions

Place message-level actions in Message:Footer, such as copy, retry, or feedback buttons.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Message>
        <twig:Message:Content>
            <twig:Bubble variant="muted">
                <twig:Bubble:Content>The install failure is coming from the workspace package.</twig:Bubble:Content>
            </twig:Bubble>
            <twig:Message:Footer>
                <twig:Button variant="ghost" size="icon" aria-label="Copy" title="Copy">
                    <twig:ux:icon name="lucide:copy" />
                </twig:Button>
                <twig:Button variant="ghost" size="icon" aria-label="Like" title="Like">
                    <twig:ux:icon name="lucide:thumbs-up" />
                </twig:Button>
                <twig:Button variant="ghost" size="icon" aria-label="Dislike" title="Dislike">
                    <twig:ux:icon name="lucide:thumbs-down" />
                </twig:Button>
            </twig:Message:Footer>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message align="end">
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>Okay drop me a link. Taking a look...</twig:Bubble:Content>
            </twig:Bubble>
            <twig:Message:Footer class="gap-2">
                <span class="font-normal text-destructive">Failed to send</span>
                <twig:Button variant="ghost" size="icon-xs" aria-label="Retry" title="Retry">
                    <twig:ux:icon name="lucide:refresh-ccw" />
                </twig:Button>
            </twig:Message:Footer>
        </twig:Message:Content>
    </twig:Message>
</div>

Attachment

Render an Attachment inside Message:Content to send a file or an image alongside the message.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Message align="end">
        <twig:Message:Content>
            <twig:Attachment orientation="vertical">
                <twig:Attachment:Media variant="image">
                    <img src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80" alt="Workspace" />
                </twig:Attachment:Media>
            </twig:Attachment>
            <twig:Bubble>
                <twig:Bubble:Content>Here's the image. Can you add it to the PDF? Use it for the cover page.</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message>
        <twig:Message:Content>
            <twig:Bubble variant="muted">
                <twig:Bubble:Content>Done. Here's the PDF with the image added as the cover page.</twig:Bubble:Content>
            </twig:Bubble>
            <twig:Attachment>
                <twig:Attachment:Media>
                    <twig:ux:icon name="lucide:file-text" />
                </twig:Attachment:Media>
                <twig:Attachment:Content>
                    <twig:Attachment:Title>sales-dashboard.pdf</twig:Attachment:Title>
                    <twig:Attachment:Description>PDF · 2.4 MB</twig:Attachment:Description>
                </twig:Attachment:Content>
                <twig:Attachment:Actions>
                    <twig:Attachment:Action variant="secondary" size="icon-sm" aria-label="Download" title="Download">
                        <twig:ux:icon name="lucide:download" />
                    </twig:Attachment:Action>
                </twig:Attachment:Actions>
            </twig:Attachment>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message align="end">
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>Thanks. Looks good.</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
</div>

Markdown

Render assistant text through a ghost bubble, so the response is unframed and can span the full width of the conversation.

100%
Loading...
<div class="mx-auto flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Message align="end">
        <twig:Message:Content>
            <twig:Bubble>
                <twig:Bubble:Content>How do I render markdown in a message?</twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
    <twig:Message>
        <twig:Message:Content>
            <twig:Bubble variant="ghost">
                <twig:Bubble:Content class="flex flex-col gap-4">
                    <p>Here's how to render markdown in a message:</p>
                    <ol class="flex list-decimal flex-col gap-2 ps-5">
                        <li>Render assistant text through <strong>Markdown</strong>.</li>
                        <li>Keep user messages as plain text.</li>
                        <li>Use a <code class="rounded bg-muted px-1 py-0.5 text-xs">ghost</code> bubble so the response is unframed.</li>
                    </ol>
                </twig:Bubble:Content>
            </twig:Bubble>
        </twig:Message:Content>
    </twig:Message>
</div>

RTL

To enable RTL support, set the dir="rtl" attribute on the root element.

100%
Loading...
<div class="flex w-full flex-col items-center gap-8">
    {# Arabic #}
    <div class="flex w-full max-w-sm flex-col gap-6 py-10" dir="rtl">
        <twig:Message>
            <twig:Message:Avatar>
                <twig:Avatar>
                    <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                    <twig:Avatar:Fallback>R</twig:Avatar:Fallback>
                </twig:Avatar>
            </twig:Message:Avatar>
            <twig:Message:Content>
                <twig:Message:Header>أوليفيا</twig:Message:Header>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>فشل البناء أثناء تثبيت الاعتماديات.</twig:Bubble:Content>
                </twig:Bubble>
            </twig:Message:Content>
        </twig:Message>
        <twig:Message align="end">
            <twig:Message:Avatar>
                <twig:Avatar>
                    <twig:Avatar:Image src="https://github.com/shadcn.png" alt="@shadcn" />
                    <twig:Avatar:Fallback>CN</twig:Avatar:Fallback>
                </twig:Avatar>
            </twig:Message:Avatar>
            <twig:Message:Content>
                <twig:Bubble>
                    <twig:Bubble:Content>هل يمكنك مشاركة رسالة الخطأ بالضبط؟</twig:Bubble:Content>
                </twig:Bubble>
                <twig:Message:Footer>تم التسليم</twig:Message:Footer>
            </twig:Message:Content>
        </twig:Message>
    </div>

    {# Hebrew #}
    <div class="flex w-full max-w-sm flex-col gap-6 py-10" dir="rtl">
        <twig:Message>
            <twig:Message:Avatar>
                <twig:Avatar>
                    <twig:Avatar:Image src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
                    <twig:Avatar:Fallback>R</twig:Avatar:Fallback>
                </twig:Avatar>
            </twig:Message:Avatar>
            <twig:Message:Content>
                <twig:Message:Header>אוליביה</twig:Message:Header>
                <twig:Bubble variant="muted">
                    <twig:Bubble:Content>הבנייה נכשלה במהלך התקנת התלויות.</twig:Bubble:Content>
                </twig:Bubble>
            </twig:Message:Content>
        </twig:Message>
        <twig:Message align="end">
            <twig:Message:Avatar>
                <twig:Avatar>
                    <twig:Avatar:Image src="https://github.com/shadcn.png" alt="@shadcn" />
                    <twig:Avatar:Fallback>CN</twig:Avatar:Fallback>
                </twig:Avatar>
            </twig:Message:Avatar>
            <twig:Message:Content>
                <twig:Bubble>
                    <twig:Bubble:Content>תוכל לשתף את הודעת השגיאה המדויקת?</twig:Bubble:Content>
                </twig:Bubble>
                <twig:Message:Footer>נמסר</twig:Message:Footer>
            </twig:Message:Content>
        </twig:Message>
    </div>
</div>

Accessibility

  • Message, Message:Group and every other part of this recipe render plain <div>s and add no role, so a message is announced as ordinary content: who sent it must be clear from Message:Header or the surrounding text, not from the alignment or the bubble colour.
  • The data-align attribute is a styling hook only and carries no semantics. align="end" reverses the visual row order but not the DOM order, so the reading order stays avatar-then-message in both directions.
  • Action buttons in Message:Footer are usually icon-only, so give each one an aria-label, e.g. <twig:Button variant="ghost" size="icon" aria-label="Copy">.
  • An empty Message:Avatar used as a spacer in a Message:Group holds no text and needs no label: it is announced as nothing, which is the intent.
  • For an in-progress message such as "Oliver is typing...", use a Marker with role="status" so assistive tech announces the update as it appears.
  • For a conversation that receives messages after load, put role="log" on the container that wraps the messages so new ones are announced as they arrive. That container belongs to the consumer, not to this recipe.

API Reference

<twig:Message>

Prop Type Default
align 
'start'|'end' 'start'
Block Description
content The message row, typically includes Message:Avatar and Message:Content.

<twig:Message:Avatar>

Block Description
content The avatar of the message sender, typically an Avatar component.

<twig:Message:Content>

Block Description
content The message content, typically includes Message:Header, a Bubble and Message:Footer.

<twig:Message:Footer>

Block Description
content The content displayed below the message, such as a delivery status or message actions.

<twig:Message:Group>

Block Description
content The grouped messages, typically multiple Message components from the same sender.

<twig:Message:Header>

Block Description
content The content displayed above the message, such as the sender name.