Attachments
The attachment tray — structural slots for items, a remove affordance, a drop zone, and a file picker.
Usage guidelines
- Pending-file tray — the strip above the composer input, showing attachment chips with a remove affordance.
- Model included — accept matching and blob-URL lifecycle ship in the package; the tray's layout and motion are yours.
- No media taxonomy —
Attachments.Itemis a structural slot; read the item'smediaTypeand decide what an image, a PDF, or a file looks like. - Drop + pick — a
Dropzoneoverlay (in place, or portalled elsewhere viaportalSelector) plus aTriggerfile picker. - Get started — see Quick start to add the package.
Anatomy
{items.length > 0 && (
<Attachments.Root>
{items.map((item) => (
<Attachments.Item key={item.id}>
<span>{item.filename}</span>
<Attachments.Remove onRemove={() => remove(item.id)} filename={item.filename} />
</Attachments.Item>
))}
</Attachments.Root>
)}With a drop zone and a picker trigger:
<>
<Attachments.Dropzone visible={isDragging} portalSelector="#app-shell" />
<Attachments.Root>
{items.map((item) => (
<Attachments.Item key={item.id}>
<span>{item.filename}</span>
<Attachments.Remove onRemove={() => remove(item.id)} filename={item.filename} />
</Attachments.Item>
))}
</Attachments.Root>
<Attachments.Trigger onClick={openFileDialog} />
</>Examples
Dropping, picking and rejecting files
The whole intake path in one tray. The package ships the mechanics —
matchesAccept and toAttachmentItem are plain functions — and none of the
policy: what counts as too large, and what the message says, are yours.
Validation emits a code rather than copy, which is why the wording lives in one map in the demo and can be localised there.
API reference
Every part accepts className, style, and render (see
Styling) and emits a bespoke part attribute (data-<part>) unless noted.
Attachments
The tray container. Mount it only when there are items to show; it renders no
layout of its own. Renders a <div> element.
Attachments.Item
One attachment, as a structural slot with no media taxonomy of its own. Read
the item's mediaType and decide what an image or a PDF looks like. Renders a
<div> element.
| Attribute | Description |
|---|---|
data-attachments-item | The chip. |
Attachments.Remove
The removal affordance. Named "Remove attachment" by default, or
Remove {filename} when filename is set, so a row of them does not announce
identically. Renders a <button> element.
| Prop | Type | Default |
|---|---|---|
onRemove | () => void | (required) |
filename | string | — |
Attachments.Dropzone
The drop overlay. portalSelector moves it elsewhere in the document, so files
can be dropped anywhere rather than only over the tray. Renders a <div>
element.
| Prop | Type | Default |
|---|---|---|
visible | boolean | false |
keepMounted | boolean | false |
portalSelector | string | — |
| Attribute | Description |
|---|---|
data-attachments-dropzone | The overlay. |
data-visible | Present while visible is true. |
Attachments.Error
The validation slot, as a live region: whatever appears inside announces
immediately. Validation emits an AttachmentErrorCode — "accept",
"max_file_size" or "max_files" — never copy, so the message is yours to
write and localise. Renders a <span> element with role="alert".
Attachments.Trigger
The file-picker button, named "Add attachment" by default. The package owns no
file input; wire this to your own. Renders a <button> element.
Utilities
@intentface/chat/attachments exports the generic mechanics:
toAttachmentItem (the default blob ingestion), matchesAccept, and
revokeAttachmentUrl.
Everything above that is yours: the accept and size policy, the media taxonomy that decides what an image or a PDF looks like, and the adapter that turns submitted items into whatever your transport expects — AI SDK file parts, signed uploads, or anything else. The package imposes none of it.