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
State
idle
uploading
processing
donedefault
error
Sizes
sm
defaultdefault
Orientation
horizontaldefault
vertical
States
uploading
error
drag accept
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, labelnumber, stringdefaultrequiredAttachmentGroup layout'wrap' | 'scroll'default'wrap'AttachmentTriggerbutton propsAttachmentDrop variant'region' | 'zone'default'region'AttachmentDrop accept, maxSize, multiplestring, number, booleandefault-, -, trueAttachmentDrop onFiles, onReject(files: File[]) => void, (r: FileRejection[]) => voidAttachmentDrop paste, disabled, kindboolean, boolean, KinddefaultfalseAttachmentDrop overlayLabel, rejectLabel, dragStateReactNode, ReactNode, accept | rejecttile src, fit, loading, onFile, acceptstring, cover | contain, boolean, (file) => void, stringdefault-, 'cover', false, -, 'image/*'HelpersformatBytes, 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