Skip to content

Attachment

A chip or card for a file attached to a message or a prompt: its name, size, a type icon, an upload progress bar or error, and an optional remove button. A preview variant shows a thumbnail for pictures. AttachmentList groups several as a list.

AIv0.1.0experimentalWCAG 2.2 AAView spec

Bring your own model.

These components only show what you give them, in one message shape, and report what the person does. They never call a model or a server, so they work with any backend. They are built for how assistive technology handles streaming text, tool steps and approvals.

  • Q3 report.pdf2.4 MB
  • customers.csv180 KB
  • assets.zip12.1 MB
import { useState } from "react";
import { Attachment, AttachmentList } from "@rdloom/react";

const initial = [
  { id: "a", name: "Q3 report.pdf", sizeText: "2.4 MB", mediaType: "application/pdf" },
  { id: "b", name: "customers.csv", sizeText: "180 KB", mediaType: "text/csv" },
  { id: "c", name: "assets.zip", sizeText: "12.1 MB", mediaType: "application/zip" },
];

export default function AttachmentPromptChipsExample() {
  const [files, setFiles] = useState(initial);
  return (
    <div className="flex w-full justify-center">
      <AttachmentList aria-label="Files for this prompt" className="max-w-md justify-center">
        {files.map((file) => (
          <Attachment
            key={file.id}
            name={file.name}
            sizeText={file.sizeText}
            mediaType={file.mediaType}
            onRemove={() => setFiles((all) => all.filter((f) => f.id !== file.id))}
          />
        ))}
      </AttachmentList>
    </div>
  );
}

Installation

npx rdloom add attachment

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/attachment.json

Works, but without upgrade tracking.

Usage

import { Attachment, AttachmentList } from "@rdloom/react";

<div className="flex w-full justify-center">
  <AttachmentList aria-label="Files for this prompt" className="max-w-md justify-center">
    {files.map((file) => (
      <Attachment
        key={file.id}
        name={file.name}
        sizeText={file.sizeText}
        mediaType={file.mediaType}
        onRemove={() => setFiles((all) => all.filter((f) => f.id !== file.id))}
      />
    ))}
  </AttachmentList>
</div>

Upload states

  • contract.pdf1.2 MB
  • notes.txt4 KB
  • video.mov220 MB
  • huge.zip
import { Attachment, AttachmentList } from "@rdloom/react";

export default function AttachmentUploadStatesExample() {
  return (
    <div className="flex w-full justify-center">
      <AttachmentList aria-label="Uploads" className="w-[20rem] max-w-full flex-col">
        <Attachment name="contract.pdf" sizeText="1.2 MB" mediaType="application/pdf" onRemove={() => {}} />
        <Attachment name="notes.txt" sizeText="4 KB" status="uploading" progress={45} onRemove={() => {}} />
        <Attachment name="video.mov" sizeText="220 MB" status="uploading" onRemove={() => {}} />
        <Attachment name="huge.zip" sizeText="2.1 GB" mediaType="application/zip" status="error" errorMessage="Larger than 100 MB" onRemove={() => {}} />
      </AttachmentList>
    </div>
  );
}

Image previews

  • Orange to red gradient
    sunrise.png820 KB
  • Blue gradient
    lake.jpg1.4 MB
  • draft.pdf310 KB
import { Attachment, AttachmentList } from "@rdloom/react";

const swatch = (a: string, b: string) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="160" height="96"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${a}"/><stop offset="1" stop-color="${b}"/></linearGradient></defs><rect width="160" height="96" fill="url(#g)"/></svg>`)}`;

export default function AttachmentImagePreviewsExample() {
  return (
    <div className="flex w-full justify-center">
      <AttachmentList aria-label="Photos" className="justify-center">
        <Attachment variant="preview" name="sunrise.png" sizeText="820 KB" mediaType="image/png" thumbnail={<img src={swatch("#f59e0b", "#ef4444")} alt="Orange to red gradient" />} onRemove={() => {}} />
        <Attachment variant="preview" name="lake.jpg" sizeText="1.4 MB" mediaType="image/jpeg" thumbnail={<img src={swatch("#38bdf8", "#1e3a8a")} alt="Blue gradient" />} href="#lake" />
        <Attachment variant="preview" name="draft.pdf" sizeText="310 KB" mediaType="application/pdf" status="uploading" progress={60} />
      </AttachmentList>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
namerequired

The file name. It is the accessible name of the chip and of its remove button.

stringnone
sizeText

The size as text, e.g. "2.4 MB". Format it yourself so the language and units match the rest of the page.

stringnone
mediaType

The media type, e.g. "application/pdf". It only picks the icon.

stringnone
variant

chip is a compact row with an icon. preview is a card with a thumbnail above the name.

"chip" | "preview""chip"
status

ready is a normal file. uploading shows a progress bar. error shows the error message.

"ready" | "uploading" | "error""ready"
progress

Upload progress from 0 to 100. Leave it out while uploading for an indeterminate bar.

numbernone
errorMessage

What went wrong, shown when status is error.

string"Upload failed"
thumbnail

The picture for the preview variant, usually an img with alt text. Without it the type icon is shown.

nodenone
href

Makes the name a link that opens the file.

stringnone
onPress

Makes the name a button that opens or previews the file. Use href or onPress, not both.

() => voidnone
onRemove

Shows a remove button. Called when it is pressed.

() => voidnone

Accessibility

Role listitem when inside AttachmentList, otherwise a plain group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab: moves to the name when it is a link or button, then to the remove button
  • Enter: opens the file from the name
  • Enter or Space: removes the file from the remove button

Screen readers announce

  • "report.pdf, 2.4 MB" then "Remove report.pdf, button"
  • "Uploading report.pdf, progress bar, 40"
  • "Upload failed"

What your code must do

  • The remove button is named "Remove <file name>" so several of them are told apart
  • Uploading shows a progressbar named "Uploading <file name>" with its value; without progress it is indeterminate
  • An error is shown as text with an icon, announced as an alert, and never by color alone
  • The type icon is decorative; the name and size are real text
  • A thumbnail has its own alt text; the card does not repeat it
  • The progress bar stands still for reduced motion

Guidelines

Use it when

  • Showing files attached to a prompt before it is sent
  • Showing files inside a sent message
  • Showing upload progress and failures for each file

Avoid it when

  • Choosing files or dropping them: use FileUpload
  • A table of documents with many columns: use a table

Don't

  • A remove button for a file in an already sent message
  • Showing an uploading state with no way to see it fail: always handle the error status
  • A thumbnail with no alt text

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-subtle
  • --rd-color-surface-default
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-border-default
  • --rd-color-action-primary
  • --rd-color-feedback-danger
  • --rd-color-focus-ring
  • --rd-radius-control
  • --rd-radius-overlay