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.
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 attachmentCopies 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.jsonWorks, 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
Larger than 100 MB
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
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.
| Prop | Type | Default |
|---|---|---|
namerequiredThe file name. It is the accessible name of the chip and of its remove button. | string | none |
sizeTextThe size as text, e.g. "2.4 MB". Format it yourself so the language and units match the rest of the page. | string | none |
mediaTypeThe media type, e.g. "application/pdf". It only picks the icon. | string | none |
variantchip is a compact row with an icon. preview is a card with a thumbnail above the name. | "chip" | "preview" | "chip" |
statusready is a normal file. uploading shows a progress bar. error shows the error message. | "ready" | "uploading" | "error" | "ready" |
progressUpload progress from 0 to 100. Leave it out while uploading for an indeterminate bar. | number | none |
errorMessageWhat went wrong, shown when status is error. | string | "Upload failed" |
thumbnailThe picture for the preview variant, usually an img with alt text. Without it the type icon is shown. | node | none |
hrefMakes the name a link that opens the file. | string | none |
onPressMakes the name a button that opens or previews the file. Use href or onPress, not both. | () => void | none |
onRemoveShows a remove button. Called when it is pressed. | () => void | none |
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