Attachment
The display side of a file. 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
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.
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.
Add animate for a shimmer sweep on the uploading bar — a live "actively
uploading" cue. Opt-in, and disabled under prefers-reduced-motion.
Actions
The actions slot sits at the end of the card — download, remove, retry.
Grouping
attachment-group labels a set. It stacks by default; orientation="row"
scroll-snaps them horizontally — the shape a chat bubble wants.
In a chat message
The canonical pairing: an attachment group inside a
Message's attachments slot.
{{-- 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:
{{-- 1. $uploading / $progress are your component's own properties --}}
<x-wirekit::attachment
:name="$pendingName"
:bytes="$pendingBytes"
:state="$uploading ? 'uploading' : ($failed ? 'error' : 'done')"
:progress="$progress"
/>
// 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 isaria-hidden, because the card name already carries the meaning. - The upload bar is a real
role="progressbar"witharia-valuenowand anaria-labelnaming the file. attachment-group orientation="row"is a scroll container, so it carriestabindex="0"+role="group"+aria-labeland 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 |
|---|---|
| Tab | Focus the card (when it is a link), the row group, or an action |
| ← / → | Scroll a focused orientation="row" group (native) |
Pitfalls
- Pass raw metadata, not formatted strings.
:bytes="2411520"— notbytes="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'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 |