Attachment

Displays a file or image attachment with media, metadata, upload state, and a remove button.

Workspace
workspace.pngPNG · 820 KB
Desk
desk-reference.jpgJPG · 1.1 MB
Office
office-reference.jpgJPG · 940 KB
sales-dashboard.pdfUploading · 64%
message-renderer.tsxTypeScript · 12 KB

The Attachment component displays a file or image attachment, its media, name, and metadata, with an optional remove button and upload state. Use it for files and images in chat composers, message threads, and upload lists.

Installation

import {
  Attachment,
  AttachmentGroup,
} from "@o/pipeline/experimental/components/chat/attachment";

Usage

import { IconFile } from "@o/pipeline/experimental/icons";
import { Attachment } from "@o/pipeline/experimental/components/chat/attachment";
<Attachment.Root>
  <Attachment.Media>
    <IconFile weight="duotone" />
  </Attachment.Media>
  <Attachment.Content>
    <Attachment.Title>sales-dashboard.pdf</Attachment.Title>
    <Attachment.Description>PDF · 2.4 MB</Attachment.Description>
  </Attachment.Content>
  <Attachment.Remove aria-label="Remove sales-dashboard.pdf" />
</Attachment.Root>

Composition

Use the following composition to build an attachment:

Attachment.Root
├── Attachment.Media
├── Attachment.Content
│   ├── Attachment.Title
│   └── Attachment.Description
├── Attachment.Remove
└── Attachment.Trigger

Use AttachmentGroup to lay out multiple attachments in a scrollable row:

AttachmentGroup
├── Attachment.Root
└── Attachment.Root

Features

  • Icon and image media through Attachment.Media
  • Upload states: idle, uploading, processing, error, and done with built-in styling, and a spinner over the media and a shimmer on the title while in progress
  • A corner Attachment.Remove button that appears on hover or keyboard focus
  • Three sizes and horizontal or vertical orientation
  • A full-card Attachment.Trigger that opens a link or dialog while the remove button stays independently clickable
  • Scrollable, snapping AttachmentGroup with an edge fade
  • Customizable styling through the className prop on every part

Image

Set variant="image" on Attachment.Media and render an <img> inside it. Use orientation="vertical" to stack the media above the content.

Workspace
workspace.pngPNG · 820 KB
Desk
desk-reference.jpgJPG · 1.1 MB
Office
office-reference.jpgJPG · 940 KB

States

Set state to reflect the upload lifecycle. uploading and processing show a spinner in place of the media’s icon, or over a washed image, and shimmer the title, and error switches to a destructive treatment. To let people retry a failed upload, put a link in Attachment.Description, such as Upload failed. <button>Try again</button>, instead of an icon button.

selected-file.pdfReady to upload
design-system.zipUploading · 64%
market-research.pdfProcessing document
financial-model.xlsxUpload failed.
uploaded-report.pdfUploaded · 1.8 MB

Remove

Add Attachment.Remove to let people remove an attachment, such as one staged in a composer. It renders a solid, circular sm Button inside the attachment’s top-right corner, notched out of the surface and any media beneath it so the background shows around it, hidden until the attachment is hovered or holds focus, and always shown on touch screens. While it shows, the title truncates clear of it.

<Attachment.Root orientation="vertical">
  <Attachment.Media variant="image">
    <img src={url} alt="workspace.png" />
  </Attachment.Media>
  <Attachment.Remove aria-label="Remove workspace.png" onClick={onRemove} />
</Attachment.Root>

Sizes

Use size to switch between default, sm, and xs.

Default attachmentPDF · 2.4 MB
Small attachmentPDF · 2.4 MB
Extra small attachment

Group

Wrap attachments in AttachmentGroup to lay them out in a horizontally scrollable, snapping row with an edge fade.

briefing-notes.pdfPDF · 1.4 MB
workspace.png
workspace.pngPNG · 820 KB
customers.csvCSV · 18 KB
renderer.tsxTSX · 12 KB

Trigger

Add an Attachment.Trigger to make the whole card open a link or dialog. It fills the card behind the remove button and any description link, so they stay clickable.

Dialog

Compose Attachment.Trigger with Dialog.Trigger to open a file preview while keeping the remove button independently clickable.

research-summary.pdfOpen preview dialog

Place Lightbox.Trigger inside Attachment.Media so the visible thumbnail opens the full-size image and acts as the view-transition source. This works with either a horizontal or vertical attachment.

workspace.pngPNG · 820 KB
desk-reference.jpgJPG · 1.1 MB

Accessibility

Attachment.Remove renders a Button, and Attachment.Trigger renders a real <button> (or your element via render). Follow the guidance below so both are operable and announced.

Label the remove button

Attachment.Remove is icon-only, so give it an aria-label naming the attachment it removes.

<Attachment.Remove aria-label="Remove sales-dashboard.pdf" />

Label the trigger

Attachment.Trigger covers the card with no text of its own, so give it an aria-label for what activating it does.

<Attachment.Trigger
  render={
    <a
      href={url}
      target="_blank"
      rel="noreferrer"
      aria-label="Open workspace.png"
    />
  }
/>

The trigger sits behind the remove button in the stacking order, so Attachment.Remove and Attachment.Trigger never trap each other — both remain separately focusable and clickable.

Keyboard scrolling

An AttachmentGroup scrolls horizontally. When its attachments are interactive: a trigger or a remove button, keyboard users reach off-screen items by tabbing to them. For a row of presentational attachments, make the group itself focusable and scrollable by adding tabIndex={0}, role="group", and an aria-label.

Meaning beyond color

The error state uses a destructive color. Keep the failure reason in Attachment.Description so the state is not conveyed by color alone.

API Reference

Attachment.Root

The root attachment container.

PropTypeDefaultDescription
state"idle" | "uploading" | "processing" | "error" | "done""done"The upload state. Drives styling and the shimmer.
size"default" | "sm" | "xs""default"The attachment size.
orientation"horizontal" | "vertical""horizontal"Lay the media beside or above the content.
classNamestring-Additional classes to apply to the root element.

Attachment.Media

The media slot for an icon or image preview.

PropTypeDefaultDescription
variant"icon" | "image""icon"Whether the media holds an icon or an <img>.
classNamestring-Additional classes to apply to the media slot.

Attachment.Content

Wraps the title and description.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the content slot.

Attachment.Title

The attachment name. Shimmers while the attachment is uploading or processing.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the title.

Attachment.Description

Secondary metadata such as the file type, size, or upload status.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the description.

Attachment.Remove

A corner button that removes the attachment. Renders a solid, sm, circular Button with a close icon, and accepts all of its props.

PropTypeDefaultDescription
aria-labelstring-Names the action and its target.
...propsReact.ComponentProps<typeof Button>-Props spread to the underlying Button.

Attachment.Trigger

A full-card overlay that activates the attachment. Renders a <button> by default.

PropTypeDefaultDescription
renderReactElement | function-Render as a different element, such as a link.
...propsReact.ComponentProps<"button">-Props spread to the trigger element.

AttachmentGroup

Lays out attachments in a horizontally scrollable, snapping row.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the group.