Attachments

The attachment tray — structural slots for items, a remove affordance, a drop zone, and a file picker.

quarterly-report.pdf242 KB
meeting-notes.txt1 KB

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 taxonomyAttachments.Item is a structural slot; read the item's mediaType and decide what an image, a PDF, or a file looks like.
  • Drop + pick — a Dropzone overlay (in place, or portalled elsewhere via portalSelector) plus a Trigger file 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.

Images and PDFs, up to 2 MB. Try a .txt to see a rejection.

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.

AttributeValuesDetails
data-attachments-item

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.

PropTypeDefault
onRemove() => void
(required)
filenamestring

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.

PropTypeDefault
visibleboolean
false
keepMountedboolean
false
portalSelectorstring
AttributeValuesDetails
data-attachments-dropzone
data-visible

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.