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} />
</>

Accessibility

Attachments.Remove and Attachments.Trigger are real buttons with default overridable names ("Remove attachment" / "Add attachment"). Pass filename to Remove so each chip announces distinctly ("Remove report.pdf") — without it, N remove buttons all read the same. Attachments.Error is a role="alert" live region: content appearing inside it announces immediately (the copy is still yours).

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. Renders data-attachments and takes no props of its own — mount it conditionally when there are items to show.

Attachments.Item

One attachment chip — a structural slot with no props of its own. Render the filename, size, thumbnail or icon inside it however you like. Renders data-attachments-item.

AttributeDescription
data-attachments-itemThe chip.

Attachments.Remove

Removal affordance, shown on hover. Renders <button data-attachments-remove> named "Remove attachment", or Remove {filename} when filename is set.

PropTypeDefault
onRemove() => void
(required)
filenamestring

Attachments.Dropzone

Drop overlay. Renders data-attachments-dropzone. Pass portalSelector to portal it elsewhere — into the app shell, for instance, so files can be dropped anywhere.

PropTypeDefault
visibleboolean
false
keepMountedboolean
false
portalSelectorstring
AttributeDescription
data-attachments-dropzoneThe overlay.
data-visiblePresent while visible is true.

Attachments.Error

Validation message slot. Renders <span role="alert" data-attachments-error>, so content appearing inside it announces immediately. Validation emits a structured AttachmentErrorCode ("accept", "max_file_size", "max_files") on the composer's attachments state — you map codes to your own (localized) copy; the package ships no messages.

Attachments.Trigger

The file-picker button. Renders <button data-attachments-trigger> named "Add attachment" by default.

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.