Skip to main content
WireKit
Copy for LLM

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

A file attachment
quarterly-report.pdf PDF · 2.3 MB

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.

An image attachment
dashboard-screenshot.png PNG · 180 KB

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.

Upload lifecycle
archive.zip ZIP · 50 MB · Uploading
invoice.pdf PDF · 96 KB · Uploaded
video.mov MOV · 700 MB · Upload failed

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

Animated upload bar
archive.zip ZIP · 50 MB · Uploading

Actions

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

Attachment with actions
contract.pdf PDF · 304 KB

Grouping

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

A group of attachments
brief.pdf PDF · 200 KB
budget.xlsx XLSX · 50 KB
logo.png PNG · 15 KB

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 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
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" — 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'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

Was this page helpful?

Voting requires cookies or local storage. What we store