View as Markdown

Marker

Displays an inline status, system note, bordered row, or labeled separator in a conversation.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Marker>
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:git-branch" />
        </twig:Marker:Icon>
        <twig:Marker:Content>Switched to a new branch</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker role="status">
        <twig:Marker:Icon>
            <twig:Spinner />
        </twig:Marker:Icon>
        <twig:Marker:Content class="animate-pulse">Thinking...</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker variant="separator">
        <twig:Marker:Content>Conversation compacted</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker>
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:search" />
        </twig:Marker:Icon>
        <twig:Marker:Content>Explored 4 files</twig:Marker:Content>
    </twig:Marker>
</div>

Installation

Note

Available since UX Toolkit 3.5.

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

Install the following Composer dependencies:

composer require twig/extra-bundle 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/Marker.html.twig
{%- props
    ## 'default'|'separator'|'border' The visual style variant.
    variant = 'default',
    ## 'div'|'a'|'button' The HTML tag to render.
    as = 'div'
-%}
{%- set style = html_cva(
    base: "group/marker relative flex min-h-4 w-full items-center gap-2 text-start text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [a]:underline [a]:underline-offset-3 [a]:hover:text-foreground",
    variants: {
        variant: {
            default: '',
            separator: 'before:me-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ms-1 after:h-px after:min-w-0 after:flex-1 after:bg-border',
            border: 'border-b border-border pb-2',
        },
    },
) -%}
<{{ as }}
    data-slot="marker"
    data-variant="{{ variant }}"
    {{ attributes.defaults({
        class: style.apply({variant: variant})|tailwind_classes,
        type: as == 'button' ? 'button' : false,
    }) }}
>
    {##- The marker content, typically includes `Marker:Icon` and `Marker:Content`. -#}
    {%- block content %}{% endblock -%}
</{{ as }}>
templates/components/Marker/Content.html.twig
<span
    data-slot="marker-content"
    {{ attributes.defaults({
        class: 'min-w-0 wrap-break-word group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground'|tailwind_classes,
    }) }}
>
    {##- The marker text content. -#}
    {%- block content %}{% endblock -%}
</span>
templates/components/Marker/Icon.html.twig
<span
    data-slot="marker-icon"
    aria-hidden="true"
    {{ attributes.defaults({
        class: "size-4 shrink-0 [&_svg:not([class*='size-'])]:size-4"|tailwind_classes,
    }) }}
>
    {##- The decorative icon rendered inside the marker. -#}
    {%- block content %}{% endblock -%}
</span>

Usage

<twig:Marker variant="default | separator | border">
    <twig:Marker:Icon>
        <twig:ux:icon name="lucide:check" />
    </twig:Marker:Icon>
    <twig:Marker:Content>Explored 4 files</twig:Marker:Content>
</twig:Marker>

Examples

Variants

Use the variant prop to switch between an inline marker, a bordered row, and a labeled separator.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Marker>
        <twig:Marker:Content>A default marker for inline notes.</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker variant="separator">
        <twig:Marker:Content>A separator marker</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker variant="border">
        <twig:Marker:Content>A border marker for row boundaries.</twig:Marker:Content>
    </twig:Marker>
</div>

Status

Set role="status" and include a Spinner for streaming or in-progress markers so updates are announced.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Marker role="status">
        <twig:Marker:Icon>
            <twig:Spinner />
        </twig:Marker:Icon>
        <twig:Marker:Content>Compacting conversation</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker variant="separator" role="status">
        <twig:Marker:Icon>
            <twig:Spinner />
        </twig:Marker:Icon>
        <twig:Marker:Content>Running tests</twig:Marker:Content>
    </twig:Marker>
</div>

Shimmer

Add an animation class to Marker:Content for a streaming-text effect. Shadcn ships a dedicated shimmer utility in its own package; with plain Tailwind, animate-pulse gives the same in-progress cue.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Marker role="status">
        <twig:Marker:Content class="animate-pulse">Thinking...</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker variant="separator" role="status">
        <twig:Marker:Content class="animate-pulse">Reading 4 files</twig:Marker:Content>
    </twig:Marker>
</div>

Separator

Use the separator variant for labeled dividers, such as dates or section breaks, in a conversation.

A labeled separator needs no role: the divider lines are decorative CSS pseudo-elements and the text is announced as ordinary content. Do not add role="separator" — it takes its accessible name from aria-label, so the visible label would not be announced.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
    {% for label in ['Today', 'Worked for 42s', 'Conversation compacted'] %}
        <twig:Marker variant="separator">
            <twig:Marker:Content>{{ label }}</twig:Marker:Content>
        </twig:Marker>
    {% endfor %}
</div>

Border

Use the border variant for status rows that should keep the default marker alignment while separating the next row.

100%
Loading...
{% set rows = [
    { icon: 'lucide:git-branch', label: 'Switched to release-candidate' },
    { icon: 'lucide:search', label: 'Reviewed 8 related files' },
    { icon: 'lucide:file-text', label: 'Opened implementation notes' },
] %}
<div class="flex w-full max-w-sm flex-col gap-3 py-12">
    {% for row in rows %}
        <twig:Marker variant="border">
            <twig:Marker:Icon>
                <twig:ux:icon name="{{ row.icon }}" />
            </twig:Marker:Icon>
            <twig:Marker:Content>{{ row.label }}</twig:Marker:Content>
        </twig:Marker>
    {% endfor %}
</div>

With Icon

Use Marker:Icon to render an icon alongside the content. It is decorative and hidden from assistive technologies, so the adjacent Marker:Content carries the meaning. Use flex-col to stack the icon above the content.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-12 py-12">
    <twig:Marker>
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:git-branch" />
        </twig:Marker:Icon>
        <twig:Marker:Content>Switched to a new branch</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker variant="separator">
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:search" />
        </twig:Marker:Icon>
        <twig:Marker:Content>Explored 4 files</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker class="flex-col">
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:book-open-check" />
        </twig:Marker:Icon>
        <twig:Marker:Content>Syncing completed</twig:Marker:Content>
    </twig:Marker>
</div>

Turn a marker into a link or a button with the as prop on Marker, so it is focusable and exposes the correct role. The accessible name comes from the marker text.

100%
Loading...
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
    <twig:Marker as="a" href="#links-and-buttons">
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:git-branch" />
        </twig:Marker:Icon>
        <twig:Marker:Content>View the pull request</twig:Marker:Content>
    </twig:Marker>
    <twig:Marker as="button" class="transition-colors hover:text-foreground">
        <twig:Marker:Icon>
            <twig:ux:icon name="lucide:rotate-ccw" />
        </twig:Marker:Icon>
        <twig:Marker:Content>Revert this change</twig:Marker:Content>
    </twig:Marker>
</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-4">
    {# Arabic #}
    <div class="flex w-full max-w-sm flex-col gap-6 py-6" dir="rtl">
        <twig:Marker>
            <twig:Marker:Icon>
                <twig:ux:icon name="lucide:git-branch" />
            </twig:Marker:Icon>
            <twig:Marker:Content>تم التبديل إلى فرع جديد</twig:Marker:Content>
        </twig:Marker>
        <twig:Marker variant="separator">
            <twig:Marker:Content>تم ضغط المحادثة</twig:Marker:Content>
        </twig:Marker>
        <twig:Marker variant="border">
            <twig:Marker:Icon>
                <twig:ux:icon name="lucide:search" />
            </twig:Marker:Icon>
            <twig:Marker:Content>تم استكشاف 4 ملفات</twig:Marker:Content>
        </twig:Marker>
    </div>

    {# Hebrew #}
    <div class="flex w-full max-w-sm flex-col gap-6 py-6" dir="rtl">
        <twig:Marker>
            <twig:Marker:Icon>
                <twig:ux:icon name="lucide:git-branch" />
            </twig:Marker:Icon>
            <twig:Marker:Content>עברת לענף חדש</twig:Marker:Content>
        </twig:Marker>
        <twig:Marker variant="separator">
            <twig:Marker:Content>השיחה כווצה</twig:Marker:Content>
        </twig:Marker>
        <twig:Marker variant="border">
            <twig:Marker:Icon>
                <twig:ux:icon name="lucide:search" />
            </twig:Marker:Icon>
            <twig:Marker:Content>נסרקו 4 קבצים</twig:Marker:Content>
        </twig:Marker>
    </div>
</div>

Accessibility

  • Marker is presentational by default. It forwards role, so pick the semantics from what the marker is for rather than relying on a single default.
  • For a streaming or progress marker such as "Thinking..." or a running tool, set role="status" so assistive tech announces the update as it appears.
  • A separator marker that carries text, such as a date or a section label, needs no role: the divider lines are decorative pseudo-elements and the text is announced as ordinary content. Do not add role="separator" to it, since a separator takes its name from aria-label and treats its contents as presentational, so the visible label would be lost.
  • A border marker keeps the same semantics as the default one. The bottom border is decorative.
  • Marker:Icon is hidden from assistive tech with aria-hidden, so Marker:Content carries the meaning. Give an icon-only marker an aria-label so it is not announced as empty.
  • Render an interactive marker with as="button" or as="a", so it is focusable and operable from the keyboard.

API Reference

<twig:Marker>

Prop Type Default
variant 
'default'|'separator'|'border' 'default'
as 
'div'|'a'|'button' 'div'
Block Description
content The marker content, typically includes Marker:Icon and Marker:Content.

<twig:Marker:Content>

Block Description
content The marker text content.

<twig:Marker:Icon>

Block Description
content The decorative icon rendered inside the marker.