Attachment

Shows a file in a form, prompt or message, takes files by drop or pick, and holds an image slot that uploads on press.

Examples

A file through its five states

trace-2026-09-30.json91 KB
ticket-export-october.csv1.9 of 4.8 MB
refund-policy-2026.pdfReading the file
refund-policy-2026.pdf482 KB
ticket-export-october.csvOver the 25 MB limit

Files on a message

refund-policy-2026.pdf482 KB
trace-2026-09-30.json91 KB
damaged-box.png482 KB

A drop zone with limits

Drop ticket exports here

Images, PDF or CSV, up to 25 MB each

An agent avatar slot

State

  • refund-policy-2026.pdf482 KB

    idle

  • refund-policy-2026.pdf193 of 482 KB

    uploading

  • refund-policy-2026.pdfReading the file

    processing

  • refund-policy-2026.pdf482 KB

    donedefault

  • refund-policy-2026.pdfOver the 25 MB limit

    error

Sizes

  • refund-policy-2026.pdf482 KB

    sm

  • refund-policy-2026.pdf482 KB

    defaultdefault

Orientation

  • refund-policy-2026.pdf482 KB

    horizontaldefault

  • damaged-box.png482 KB

    vertical

States

  • refund-policy-2026.pdf193 of 482 KB

    uploading

  • refund-policy-2026.pdfOver the 25 MB limit

    error

  • Drop images here

    PNG, JPG or WebP

    drag accept

  • Drop images here

    PNG, JPG or WebP

    drag reject

Usage

When to use it
Use
A person adds files to a prompt, a ticket or a form and sees them before sending.
Use
A message shows the files it carries.
Use
A page takes files by drop or pick, such as an import or a knowledge upload.
Not for
The files are the composer's own; the prompt input draws its chips with this component. Use Prompt input.
Not for
The file is a table row with columns of its own. Use Table.
Not for
The list is a browsable library of files. Use a Table with a file column.

Rules

8 rules
Must
Pass state="uploading" or "processing" while the file is in flight and "error" when it failed; do not hide the chip. Because the state sets aria-busy, dims the media and puts the fail wash on the chip, and a hidden chip loses the file without a word.
Must
Pair AttachmentProgress with the same figure in AttachmentDescription, and render it only while uploading. Because the 2px bar is easy to miss, and the description and aria-valuenow carry the number for everyone else.
Must
Say why a file failed in AttachmentDescription, from rejectionReason or the server, and offer a retry action when a retry can work. Because a red chip with no reason leaves the person guessing between size, type and a dropped connection.
Must
Give every AttachmentAction an aria-label that names the file. Because with three chips on a row, three buttons all named "Remove" cannot be told apart by a screen reader.
Must
Set accept and maxSize on AttachmentDrop, say them in AttachmentDropDescription, and handle onReject. Because the drag is judged by type before the drop, so the person only learns the limits if the words and the overlay agree.
Must
Include AttachmentDropTrigger in every zone, and in a composer region beside a visible attach button. Because dragging is a pointer gesture, so a keyboard, a screen reader and a phone have no other way in.
Must
Give a variant="tile" an aria-label that says which image it sets: "Change agent avatar". Because the tile has no text, and the Replace hint is aria-hidden.
Should
Nest an AttachmentDrop inside another instead of adding a second drop handler of your own. Because the innermost region claims the drop and the outer one stands down, while two handlers on one drop add the file twice.

Accessibility

6 checks
A chip in flight sets aria-busy; the progress bar is a progressbar with aria-valuenow, aria-valuetext in percent and its own name.
The failure reason is text in the chip, not only a color; the wash and the fail text are extra.
Actions are 28px buttons, 24px in the small chip, each named for its file.
The drag overlay is aria-hidden because dragging is a pointer gesture; the trigger button is the accessible path to the same file picker.
The tile is a button with aria-busy while uploading; its Replace hint shows at rest on devices without hover.
A zone and its overlay survive forced colors with a dashed border.

Keyboard

3 keys
Enter, Space on an action
Removes, retries or downloads the file.
Enter, Space on AttachmentDropTrigger or the tile
Opens the file picker.
Ctrl or Cmd+V in a region with paste
Takes pasted files; plain text pastes as usual.

Edge cases

9 edge cases
Long file name
Cut in the middle, keeping the extension and a few characters before it.
Drag of the wrong type
The overlay turns to the fail wash and says what is accepted before the drop.
Extensions in accept
A drag has only types, so extension-only accepts are judged at the drop.
File over maxSize or past count
Comes back through onReject with reason size or count; the other files still pass.
Image fails to decode
The thumbnail falls back to the image glyph.
Nested regions
The inner one claims the event and the outer one hides its overlay.
Disabled zone
No overlay, a disabled trigger, text-disabled words.
Right to left
The media, rule and actions mirror.
Reduced motion
The tile spinner stays still.

API

15 props
state
'idle' | 'uploading' | 'processing' | 'done' | 'error' default 'done'
size
'sm' | 'default' default 'default'
orientation
'horizontal' | 'vertical' default 'horizontal'
variant
'chip' | 'tile' default 'chip'
AttachmentMedia variant
'icon' | 'image' default 'icon'
AttachmentProgress value, label
number, string default required
AttachmentGroup layout
'wrap' | 'scroll' default 'wrap'
AttachmentTrigger
button props
AttachmentDrop variant
'region' | 'zone' default 'region'
AttachmentDrop accept, maxSize, multiple
string, number, boolean default -, -, true
AttachmentDrop onFiles, onReject
(files: File[]) => void, (r: FileRejection[]) => void
AttachmentDrop paste, disabled, kind
boolean, boolean, Kind default false
AttachmentDrop overlayLabel, rejectLabel, dragState
ReactNode, ReactNode, accept | reject
tile src, fit, loading, onFile, accept
string, cover | contain, boolean, (file) => void, string default -, 'cover', false, -, 'image/*'
Helpers
formatBytes, formatProgress, rejectionReason, fileGlyph

Tokens

19 tokens
--row
40px
--row-sm
32px
--line-control
oklch(var(--ink-c) / 0.16)
--surface-page
var(--paper)
--surface-tile
oklch(var(--ink-c) / 0.08)
--surface-hover
oklch(var(--ink-c) / 0.05)
--accent-wash
var(--surface-selected)
--stale-opacity
0.6
--dim-opacity
0.4
--scale-press
0.98
--fail-line
oklch(60% 0.236 26)
--fail-text
oklch(51% 0.180 26)
--ink
oklch(var(--ink-c))
--border-focus
var(--ink)
--radius-soft
6px
--radius-inner
4px
--dur-fast
120ms
--ease-step
cubic-bezier(0.2, 0, 0, 1)
--z-drop
40

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.