Attachment
Displays a file or image attachment with media, metadata, upload state, and actions.
<div class="mx-auto flex w-full max-w-sm flex-col gap-3">
<twig:Attachment:Group>
<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:Content>
<twig:Attachment:Title>workspace.png</twig:Attachment:Title>
<twig:Attachment:Description>PNG · 820 KB</twig:Attachment:Description>
</twig:Attachment:Content>
</twig:Attachment>
<twig:Attachment orientation="vertical">
<twig:Attachment:Media variant="image">
<img src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80" alt="Desk" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>desk-reference.jpg</twig:Attachment:Title>
<twig:Attachment:Description>JPG · 1.1 MB</twig:Attachment:Description>
</twig:Attachment:Content>
</twig:Attachment>
<twig:Attachment orientation="vertical">
<twig:Attachment:Media variant="image">
<img src="https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80" alt="Office" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>office-reference.jpg</twig:Attachment:Title>
<twig:Attachment:Description>JPG · 940 KB</twig:Attachment:Description>
</twig:Attachment:Content>
</twig:Attachment>
</twig:Attachment:Group>
<twig:Attachment state="uploading" class="w-full">
<twig:Attachment:Media>
<twig:Spinner />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>sales-dashboard.pdf</twig:Attachment:Title>
<twig:Attachment:Description>Uploading · 64%</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Cancel upload">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
<twig:Attachment class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-code" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>message-renderer.tsx</twig:Attachment:Title>
<twig:Attachment:Description>TypeScript · 12 KB</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Remove message-renderer.tsx">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
</div>
Installation
Available since UX Toolkit 3.5.
php bin/console ux:install attachment --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
## 'idle'|'uploading'|'processing'|'error'|'done' The upload state, which drives the styling.
state = 'done',
## 'default'|'sm'|'xs' The attachment size.
size = 'default',
## 'horizontal'|'vertical' Whether the media sits beside or above the content.
orientation = 'horizontal'
-%}
{%- set style = html_cva(
base: 'group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed',
variants: {
size: {
default: 'gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2',
sm: 'gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5',
xs: 'gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1',
},
orientation: {
horizontal: 'min-w-40 items-center',
vertical: 'w-24 flex-col has-data-[slot=attachment-content]:w-30',
},
},
) -%}
<div
data-slot="attachment"
data-state="{{ state }}"
data-size="{{ size }}"
data-orientation="{{ orientation }}"
{{ attributes.defaults({
class: style.apply({size: size, orientation: orientation})|tailwind_classes,
}) }}
>
{##- The attachment content, typically includes `Attachment:Media`, `Attachment:Content`, `Attachment:Actions` and/or `Attachment:Trigger`. -#}
{%- block content %}{% endblock -%}
</div>
{%- props
## 'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' The visual style variant.
variant = 'ghost',
## 'default'|'xs'|'sm'|'lg'|'icon'|'icon-xs'|'icon-sm'|'icon-lg' The button size.
size = 'icon-xs'
-%}
<twig:Button variant="{{ variant }}" size="{{ size }}" data-slot="attachment-action" {{ ...attributes }}>
{##- The action label, usually an icon. -#}
{{- block(outerBlocks.content) -}}
</twig:Button>
<div
data-slot="attachment-actions"
{{ attributes.defaults({
class: 'relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:end-3 group-data-[orientation=vertical]/attachment:gap-1'|tailwind_classes,
}) }}
>
{##- The actions of the attachment, typically one or more `Attachment:Action`. -#}
{%- block content %}{% endblock -%}
</div>
<div
data-slot="attachment-content"
{{ attributes.defaults({
class: 'max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1'|tailwind_classes,
}) }}
>
{##- The main content area, typically includes `Attachment:Title` and `Attachment:Description`. -#}
{%- block content %}{% endblock -%}
</div>
<span
data-slot="attachment-description"
{{ attributes.defaults({
class: 'mt-0.5 block max-w-full min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80'|tailwind_classes,
}) }}
>
{##- The secondary metadata, such as the file type, size or upload status. -#}
{%- block content %}{% endblock -%}
</span>
<div
data-slot="attachment-group"
{{ attributes.defaults({
class: 'flex min-w-0 snap-x snap-mandatory scroll-px-1 no-scrollbar gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start'|tailwind_classes,
}) }}
>
{##- The grouped attachments, typically multiple `Attachment` components. -#}
{%- block content %}{% endblock -%}
</div>
{%- props
## 'icon'|'image' Whether the media holds an icon or an image.
variant = 'icon'
-%}
{%- set style = html_cva(
base: "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
variants: {
variant: {
icon: '',
image: 'opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover',
},
},
) -%}
<div
data-slot="attachment-media"
data-variant="{{ variant }}"
{{ attributes.defaults({
class: style.apply({variant: variant})|tailwind_classes,
}) }}
>
{##- The visual element, typically an icon, a `Spinner` or an `img` tag. -#}
{%- block content %}{% endblock -%}
</div>
<span
data-slot="attachment-title"
{{ attributes.defaults({
class: 'block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:animate-pulse group-data-[state=uploading]/attachment:animate-pulse'|tailwind_classes,
}) }}
>
{##- The attachment name. -#}
{%- block content %}{% endblock -%}
</span>
{%- props
## 'button'|'a' The HTML tag to render; use `a` when the trigger is a link.
as = 'button'
-%}
<{{ as }}
data-slot="attachment-trigger"
{{ attributes.defaults({
class: 'absolute inset-0 z-10 outline-none'|tailwind_classes,
type: as == 'button' ? 'button' : false,
}) }}
>
{##- The optional trigger label, usually left empty as the trigger covers the whole attachment. -#}
{%- block content %}{% endblock -%}
</{{ as }}>
{%- props
## 'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' The visual style variant.
variant = 'default',
## 'default'|'xs'|'sm'|'lg'|'icon'|'icon-xs'|'icon-sm'|'icon-lg' The button size.
size = 'default',
## 'button'|'a' The HTML tag to render; use `a` when the button navigates.
as = 'button'
-%}
{%- set style = html_cva(
base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
icon: 'size-8',
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
) -%}
<{{ as }}
data-slot="button"
data-size="{{ size }}"
data-variant="{{ variant }}"
{{ attributes.defaults({
class: style.apply({variant: variant, size: size})|tailwind_classes,
type: as == 'button' ? 'button' : false,
}) }}
>
{##- The button label and/or icon. -#}
{%- block content %}{% endblock -%}
</{{ as }}>
Usage
<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 aria-label="Remove sales-dashboard.pdf">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
Examples
Image
Set variant="image" on Attachment:Media and render an img tag inside it. Use orientation="vertical" to stack the media above the content.
{% set images = [
{ src: 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80', alt: 'Workspace', name: 'workspace.png', meta: 'PNG · 820 KB' },
{ src: 'https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80', alt: 'Desk', name: 'desk-reference.jpg', meta: 'JPG · 1.1 MB' },
{ src: 'https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80', alt: 'Office', name: 'office-reference.jpg', meta: 'JPG · 940 KB' },
] %}
<div class="mx-auto w-full max-w-sm">
<twig:Attachment:Group class="w-full">
{% for image in images %}
<twig:Attachment orientation="vertical">
<twig:Attachment:Media variant="image">
<img src="{{ image.src }}" alt="{{ image.alt }}" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>{{ image.name }}</twig:Attachment:Title>
<twig:Attachment:Description>{{ image.meta }}</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Remove {{ image.name }}">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
<twig:Attachment:Trigger
as="a"
href="{{ image.src }}"
target="_blank"
rel="noreferrer"
aria-label="Open {{ image.name }}"
/>
</twig:Attachment>
{% endfor %}
</twig:Attachment:Group>
</div>
States
Set state to reflect the upload lifecycle. uploading and processing animate the title, and error switches to a destructive treatment.
<div class="mx-auto flex w-full max-w-sm flex-col gap-2">
<twig:Attachment state="idle" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:clock" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>selected-file.pdf</twig:Attachment:Title>
<twig:Attachment:Description>Ready to upload</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Remove selected-file.pdf">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
<twig:Attachment state="uploading" class="w-full">
<twig:Attachment:Media>
<twig:Spinner />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>design-system.zip</twig:Attachment:Title>
<twig:Attachment:Description>Uploading · 64%</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Cancel upload">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
<twig:Attachment state="processing" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-text" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>market-research.pdf</twig:Attachment:Title>
<twig:Attachment:Description>Processing document</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Remove market-research.pdf">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
<twig:Attachment state="error" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-warning" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>financial-model.xlsx</twig:Attachment:Title>
<twig:Attachment:Description>Upload failed. Try again.</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Retry upload">
<twig:ux:icon name="lucide:refresh-cw" />
</twig:Attachment:Action>
<twig:Attachment:Action aria-label="Remove financial-model.xlsx">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
<twig:Attachment state="done" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:check" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>uploaded-report.pdf</twig:Attachment:Title>
<twig:Attachment:Description>Uploaded · 1.8 MB</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Remove uploaded-report.pdf">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
</div>
Sizes
Use size to switch between default, sm, and xs.
<div class="mx-auto flex w-full max-w-sm flex-col gap-3">
<twig:Attachment size="default" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-text" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>Default attachment</twig:Attachment:Title>
<twig:Attachment:Description>PDF · 2.4 MB</twig:Attachment:Description>
</twig:Attachment:Content>
</twig:Attachment>
<twig:Attachment size="sm" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-text" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>Small attachment</twig:Attachment:Title>
<twig:Attachment:Description>PDF · 2.4 MB</twig:Attachment:Description>
</twig:Attachment:Content>
</twig:Attachment>
<twig:Attachment size="xs" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-text" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>Extra small attachment</twig:Attachment:Title>
</twig:Attachment:Content>
</twig:Attachment>
</div>
Group
Wrap attachments in Attachment:Group to lay them out in a horizontally scrollable, snapping row.
{% set files = [
{ icon: 'lucide:file-text', name: 'briefing-notes.pdf', meta: 'PDF · 1.4 MB' },
{ src: 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80', name: 'workspace.png', meta: 'PNG · 820 KB' },
{ icon: 'lucide:table', name: 'customers.csv', meta: 'CSV · 18 KB' },
{ icon: 'lucide:file-code', name: 'renderer.tsx', meta: 'TSX · 12 KB' },
] %}
<div class="mx-auto w-full max-w-sm">
<twig:Attachment:Group class="w-full">
{% for file in files %}
<twig:Attachment class="w-64">
{% if file.src is defined %}
<twig:Attachment:Media variant="image">
<img src="{{ file.src }}" alt="{{ file.name }}" />
</twig:Attachment:Media>
{% else %}
<twig:Attachment:Media>
<twig:ux:icon name="{{ file.icon }}" />
</twig:Attachment:Media>
{% endif %}
<twig:Attachment:Content>
<twig:Attachment:Title>{{ file.name }}</twig:Attachment:Title>
<twig:Attachment:Description>{{ file.meta }}</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Remove {{ file.name }}">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
{% endfor %}
</twig:Attachment:Group>
</div>
Trigger
Add an Attachment:Trigger to make the whole card open a link or a dialog. It fills the card behind the actions, so the actions stay clickable.
<div class="mx-auto w-full max-w-sm">
<twig:Dialog id="attachment-preview">
<twig:Attachment class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-search" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>research-summary.pdf</twig:Attachment:Title>
<twig:Attachment:Description>Open preview dialog</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="Copy link">
<twig:ux:icon name="lucide:copy" />
</twig:Attachment:Action>
<twig:Attachment:Action aria-label="Remove research-summary.pdf">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
<twig:Dialog:Trigger>
<twig:Attachment:Trigger {{ ...dialog_trigger_attrs }} aria-label="Preview research-summary.pdf" />
</twig:Dialog:Trigger>
</twig:Attachment>
<twig:Dialog:Content class="sm:max-w-md">
<twig:Dialog:Header>
<twig:Dialog:Title>research-summary.pdf</twig:Dialog:Title>
<twig:Dialog:Description>
The attachment trigger fills the card and opens the dialog, while the actions stay independently clickable above it.
</twig:Dialog:Description>
</twig:Dialog:Header>
</twig:Dialog:Content>
</twig:Dialog>
</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-8">
{# Arabic #}
<div class="flex w-full max-w-sm flex-col gap-3" dir="rtl">
<twig:Attachment state="uploading" class="w-full">
<twig:Attachment:Media>
<twig:Spinner />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>تقرير-المبيعات.pdf</twig:Attachment:Title>
<twig:Attachment:Description>جارٍ الرفع · ٦٤٪</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="إلغاء الرفع">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
<twig:Attachment state="error" class="w-full">
<twig:Attachment:Media>
<twig:ux:icon name="lucide:file-warning" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>النموذج-المالي.xlsx</twig:Attachment:Title>
<twig:Attachment:Description>فشل الرفع. حاول مرة أخرى.</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="إعادة المحاولة">
<twig:ux:icon name="lucide:refresh-cw" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
</div>
{# Hebrew #}
<div class="flex w-full max-w-sm flex-col gap-3" dir="rtl">
<twig:Attachment orientation="vertical" class="w-full">
<twig:Attachment:Media variant="image">
<img src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80" alt="שולחן עבודה" />
</twig:Attachment:Media>
<twig:Attachment:Content>
<twig:Attachment:Title>שולחן-עבודה.png</twig:Attachment:Title>
<twig:Attachment:Description>PNG · 820 ק״ב</twig:Attachment:Description>
</twig:Attachment:Content>
<twig:Attachment:Actions>
<twig:Attachment:Action aria-label="הסר את שולחן-עבודה.png">
<twig:ux:icon name="lucide:x" />
</twig:Attachment:Action>
</twig:Attachment:Actions>
</twig:Attachment>
</div>
</div>
Accessibility
Attachment:Actionis usually icon-only, so give each one anaria-labelnaming the action and its target, such asaria-label="Remove sales-dashboard.pdf".Attachment:Triggercovers the whole card and carries no text of its own. Give it anaria-labeldescribing what activating it does. It sits behind the actions in the stacking order, so an action and the trigger never trap each other.Attachment:Groupscrolls horizontally. Keyboard users reach off-screen attachments by tabbing to their trigger or actions. For a row of purely presentational attachments, make the group itself reachable withtabindex="0",role="group"and anaria-label.- The
errorstate is shown with a destructive colour. Keep the reason for the failure inAttachment:Descriptionso the state is not conveyed by colour alone. - An icon inside
Attachment:Mediais decorative and<twig:ux:icon>hides it from assistive tech, so the name of the file belongs inAttachment:Title.
API Reference
<twig:Attachment>
| Prop | Type | Default |
|---|---|---|
state The upload state, which drives the styling.
|
'idle'|'uploading'|'processing'|'error'|'done' |
'done' |
size The attachment size.
|
'default'|'sm'|'xs' |
'default' |
orientation Whether the media sits beside or above the content.
|
'horizontal'|'vertical' |
'horizontal' |
| Block | Description |
|---|---|
content |
The attachment content, typically includes Attachment:Media, Attachment:Content, Attachment:Actions and/or Attachment:Trigger. |
<twig:Attachment:Action>
| Prop | Type | Default |
|---|---|---|
variant The visual style variant.
|
'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' |
'ghost' |
size The button size.
|
'default'|'xs'|'sm'|'lg'|'icon'|'icon-xs'|'icon-sm'|'icon-lg' |
'icon-xs' |
| Block | Description |
|---|---|
content |
The action label, usually an icon. |
<twig:Attachment:Actions>
| Block | Description |
|---|---|
content |
The actions of the attachment, typically one or more Attachment:Action. |
<twig:Attachment:Content>
| Block | Description |
|---|---|
content |
The main content area, typically includes Attachment:Title and Attachment:Description. |
<twig:Attachment:Description>
| Block | Description |
|---|---|
content |
The secondary metadata, such as the file type, size or upload status. |
<twig:Attachment:Group>
| Block | Description |
|---|---|
content |
The grouped attachments, typically multiple Attachment components. |
<twig:Attachment:Media>
| Prop | Type | Default |
|---|---|---|
variant Whether the media holds an icon or an image.
|
'icon'|'image' |
'icon' |
| Block | Description |
|---|---|
content |
The visual element, typically an icon, a Spinner or an img tag. |
<twig:Attachment:Title>
| Block | Description |
|---|---|
content |
The attachment name. |
<twig:Attachment:Trigger>
| Prop | Type | Default |
|---|---|---|
as The HTML tag to render; use
a when the trigger is a link. |
'button'|'a' |
'button' |
| Block | Description |
|---|---|
content |
The optional trigger label, usually left empty as the trigger covers the whole attachment. |