Marker
Displays an inline status, system note, bordered row, or labeled separator in a conversation.
<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
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:
{%- 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 }}>
<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>
<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.
<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.
<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.
<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.
<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.
{% 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.
<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>
Links and Buttons
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.
<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.
<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
Markeris presentational by default. It forwardsrole, 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
separatormarker 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 addrole="separator"to it, since a separator takes its name fromaria-labeland treats its contents as presentational, so the visible label would be lost. - A
bordermarker keeps the same semantics as the default one. The bottom border is decorative. Marker:Iconis hidden from assistive tech witharia-hidden, soMarker:Contentcarries the meaning. Give an icon-only marker anaria-labelso it is not announced as empty.- Render an interactive marker with
as="button"oras="a", so it is focusable and operable from the keyboard.
API Reference
<twig:Marker>
| Prop | Type | Default |
|---|---|---|
variant The visual style variant.
|
'default'|'separator'|'border' |
'default' |
as The HTML tag to render.
|
'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. |