---
title: Attachment
description: File and image attachment cards with formatted metadata, upload state, and actions
visibility: guest
draft: false
---

# Attachment

The **display** side of a file. [File Upload](/components/file-upload) is the
input — the dropzone that collects files. Attachment renders a file you already
have: in a chat bubble, a mail row, an activity feed, a detail panel.

Pass **raw** file metadata — a byte count and a MIME type — and the component
formats it: `2411520` becomes `2.3 MB`, `report.pdf` becomes a `PDF` label. You
never hand-roll that.

## Basic Usage

:::preview{title="A file attachment"}
<x-wirekit::attachment name="quarterly-report.pdf" type="application/pdf" :bytes="2411520" />
:::

## Images

Give a `thumbnail` and the media tile shows the picture instead of the file
glyph. Thumbnails are lazy-loaded and decorative — the card already carries the
accessible name.

:::preview{title="An image attachment"}
<x-wirekit::attachment
    name="dashboard-screenshot.png"
    type="image/png"
    :bytes="184320"
    thumbnail="/placeholder/320x320?bg=0f766e&gradient=0891b2&pattern=none&fg=ffffff&label=PNG"
/>
:::

## Upload state

`state` drives the lifecycle. Every state renders as **text**, never color
alone — and `uploading` with a `progress` value renders an accessible progress
bar.

:::preview{title="Upload lifecycle"}
<x-wirekit::stack gap="sm">
    <x-wirekit::attachment name="archive.zip" :bytes="52428800" state="uploading" :progress="42" />
    <x-wirekit::attachment name="invoice.pdf" :bytes="98304" state="done" />
    <x-wirekit::attachment name="video.mov" :bytes="734003200" state="error" />
</x-wirekit::stack>
:::

Add `animate` for a shimmer sweep on the uploading bar — a live "actively
uploading" cue. Opt-in, and disabled under `prefers-reduced-motion`.

:::preview{title="Animated upload bar"}
<x-wirekit::attachment name="archive.zip" type="application/zip" :bytes="52428800" state="uploading" :progress="42" animate />
:::

## Actions

The `actions` slot sits at the end of the card — download, remove, retry.

:::preview{title="Attachment with actions"}
<div style="width: 24rem; max-width: 100%; margin-inline: auto;">
    <x-wirekit::attachment name="contract.pdf" type="application/pdf" :bytes="311296">
        <x-slot:actions>
            <x-wirekit::button size="sm" intent="neutral" surface="ghost" aria-label="Download contract.pdf">
                <x-wirekit::icon name="download" size="sm" />
            </x-wirekit::button>
            <x-wirekit::button size="sm" intent="danger" surface="ghost" aria-label="Remove contract.pdf">
                <x-wirekit::icon name="trash" size="sm" />
            </x-wirekit::button>
        </x-slot:actions>
    </x-wirekit::attachment>
</div>
:::

:::source{language="blade"}
<x-wirekit::attachment name="contract.pdf" type="application/pdf" :bytes="311296">
    <x-slot:actions>
        <x-wirekit::button size="sm" intent="neutral" surface="ghost" aria-label="Download contract.pdf">
            <x-wirekit::icon name="download" size="sm" />
        </x-wirekit::button>
        <x-wirekit::button size="sm" intent="danger" surface="ghost" aria-label="Remove contract.pdf">
            <x-wirekit::icon name="trash" size="sm" />
        </x-wirekit::button>
    </x-slot:actions>
</x-wirekit::attachment>
:::

## Grouping

`attachment-group` labels a set. It **stacks** by default; `orientation="row"`
scroll-snaps them horizontally — the shape a chat bubble wants.

:::preview{title="A group of attachments"}
<x-wirekit::attachment-group label="3 attachments">
    <x-wirekit::attachment name="brief.pdf" type="application/pdf" :bytes="204800" />
    <x-wirekit::attachment name="budget.xlsx" type="application/vnd.ms-excel" :bytes="51200" />
    <x-wirekit::attachment name="logo.png" type="image/png" :bytes="15360" thumbnail="/placeholder/320x320?bg=4f46e5&gradient=7c3aed&pattern=none&fg=ffffff&label=LOGO" />
</x-wirekit::attachment-group>
:::

## In a chat message

The canonical pairing: an attachment group inside a
[Message](/components/message)'s `attachments` slot.

```blade
{{-- 1. The group fills message's existing attachments slot --}}
<x-wirekit::message :author="$message->author" :timestamp="$message->created_at">
    Here is the signed contract.

    <x-slot:attachments>
        <x-wirekit::attachment-group orientation="row" label="1 attachment">
            {{-- 2. Raw metadata in, formatted metadata out --}}
            <x-wirekit::attachment
                :name="$file->name"
                :type="$file->mime_type"
                :bytes="$file->size"
                :href="route('files.download', $file)"
            />
        </x-wirekit::attachment-group>
    </x-slot:attachments>
</x-wirekit::message>
```

## Livewire uploads

Drive `state` and `progress` from your component while a `wire:model` upload is
in flight:

```blade
{{-- 1. $uploading / $progress are your component's own properties --}}
<x-wirekit::attachment
    :name="$pendingName"
    :bytes="$pendingBytes"
    :state="$uploading ? 'uploading' : ($failed ? 'error' : 'done')"
    :progress="$progress"
/>
```

```php
// 2. Livewire's upload hooks give you the numbers to bind
public bool $uploading = false;
public int $progress = 0;

public function updatedFile(): void
{
    $this->uploading = true;
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | string | `''` | File name; also the card's accessible name |
| `bytes` | int\|null | `null` | Raw byte count — formatted for display (`2.3 MB`) |
| `type` | string\|null | `null` | MIME type — rendered as a short label (`PDF`) |
| `thumbnail` | string\|null | `null` | Image source; shown instead of the file glyph |
| `state` | string | `'idle'` | `idle`, `uploading`, `done`, `error` |
| `progress` | int\|null | `null` | 0–100; renders a progress bar while `uploading` |
| `href` | string\|null | `null` | Renders the card as a link (download / open) |
| `icon` | string\|null | `null` | Override the media glyph with an icon name |
| `animate` | bool | `false` | Shimmer the uploading progress bar (opt-in; reduced-motion safe) |
| `scope` | string\|null | `null` | Scoped personalization name |

### attachment-group

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | string | `'Attachments'` | Accessible name for the group |
| `orientation` | string | `'stack'` | `stack` (vertical list) or `row` (horizontal scroll-snap) |
| `scope` | string\|null | `null` | Scoped personalization name |

## Slots

| Slot | Description |
| --- | --- |
| `actions` | End-aligned controls — download, remove, retry |

## Accessibility

- The card carries a full accessible name: file name, type, size, and state
  ("contract.pdf, PDF, 304 KB, Uploaded") — so a screen reader gets everything
  the sighted user sees in one read.
- **State is never color-only** (WCAG 1.4.1) — every state renders its own text.
- The media tile is decorative: thumbnails use `alt=""` and the glyph is
  `aria-hidden`, because the card name already carries the meaning.
- The upload bar is a real `role="progressbar"` with `aria-valuenow` and an
  `aria-label` naming the file.
- `attachment-group orientation="row"` is a scroll container, so it carries
  `tabindex="0"` + `role="group"` + `aria-label` and a visible focus ring —
  reachable and operable by keyboard (WCAG 2.1.1).
- Icon-only actions need their own `aria-label` — see the actions example.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| <kbd>Tab</kbd> | Focus the card (when it is a link), the row group, or an action |
| <kbd>←</kbd> / <kbd>→</kbd> | Scroll a focused `orientation="row"` group (native) |

## Pitfalls

- **Pass raw metadata, not formatted strings.** `:bytes="2411520"` — not
  `bytes="2.3 MB"`. Formatting is the component's job and stays consistent
  everywhere.
- **Do not use it as an uploader.** Collecting files is
  [File Upload](/components/file-upload)'s job; Attachment renders the result.
- **Give icon-only actions a label.** A bare glyph button is unusable with a
  screen reader.

## Design Tokens

| Element | Token |
| --- | --- |
| Card surface | `--color-wk-bg-elevated` |
| Card border | `--color-wk-border` |
| Card radius | `--radius-wk-md` |
| Media tile | `--color-wk-bg-muted`, `--radius-wk-sm` |
| Name text | `--text-wk-sm` |
| Meta text | `--text-wk-xs`, `--color-wk-text-muted` |
| Gap / padding | `--gap-wk-sm`, `--padding-wk-x-sm` |
| Focus ring (row group) | `--color-wk-ring` |

## Further Reading

- [WCAG 1.4.1: Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html)
- [WCAG 2.1.1: Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html)
- [MDN: CSS scroll snap](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap)
