Attachment
Shows a file a person attached, uploaded, or referenced. UseFileAttachment for a ready-made row, card, mention, or chip, and the Attachment parts when you need your own layout.
File attachments
import { AttachmentGroup, FileAttachment } from "@tulipfarm/sprout/attachment";
<AttachmentGroup variant="carousel" aria-label="Attached files">
{uploads.map((item) => (
<FileAttachment
key={item.id}
variant="chip"
file={{
id: item.id,
name: item.file.name,
mediaType: item.file.type,
size: "48 KB",
state: item.kind, // "ready" | "uploading" | "processing" | "error"
source: { kind: "local", file: item.file },
}}
preview
viewable
onRemove={() => remove(item.id)}
onRetry={() => retry(item)}
/>
))}
</AttachmentGroup>variant picks the shape:
"list"(default): a row with icon, name, and status line."grid": a card with a large preview above the name."inline": a compact pill for text."chip": a 36px chip for composers and messages.
The status line shows file.size when ready, which can also be a short source such as “Google Drive” or “Saved in Files”. Busy files show a spinner, and "error" shows “Upload failed” with a Retry button when you pass onRetry. onRemoveadds a remove button. Images with a url show a thumbnail.
Opening: viewable opens the file in the full-screenFileViewer on click, with syntax colors for code and text. PassonOpen instead to open it your own way, such as a canvas or a download; it is disabled while the file is busy. With neither, the name is plain text.actionLabel="Download"names the button “Download …” instead of “Inspect …”.
preview peeks at the file in a card after hovering or focusing the name for 400ms: the image, or the first 24 lines of text, code, JSON, or Markdown. Other formats and failed uploads get no card. Both props need file.source or file.url. Wrap the app in FileRenderersProvider to view sheets, Office files, and formatted Markdown.
<FileAttachment
variant="chip"
file={{ id, name: "Supplier price sheet", mediaType, size: "Google Drive", state: "ready" }}
onOpen={openInCanvas}
/>
<FileAttachment
variant="chip"
file={{ id, name: "Loading file…", mediaType: "", size: "", state: "processing" }}
onOpen={() => undefined}
/>
<FileAttachment
variant="chip"
actionLabel="Download"
file={{ id, name: "Price comparison.xlsx", mediaType, size: "52 KB", state: saving ? "processing" : "ready" }}
onOpen={save}
/>Groups
AttachmentGroup lays out attachments. "carousel"(default) scrolls one row sideways with fading edge arrows;"grid" makes two columns, three from 640px;"inline" wraps; "list" stacks. Give a carousel anaria-label. Viewable attachments in a group share one viewer that steps through them in page order with Previous, Next, and the arrow keys. Closing it returns focus to the last file shown.
Parts
<Attachment state="uploading" size="sm" orientation="horizontal">
<AttachmentTrigger aria-label="Open Q3 launch deck.pptx" onClick={open} />
<AttachmentMedia>
<FileTypeIcon name="Q3 launch deck.pptx" mediaType={type} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Q3 launch deck.pptx</AttachmentTitle>
<AttachmentDescription>Uploading 1.2 MB of 2.8 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove Q3 launch deck.pptx" onClick={remove}>
<X />
</AttachmentAction>
</AttachmentActions>
</Attachment>state:"idle"dashes the border,"uploading"and"processing"shimmer the title,"error"turns it red,"done"(default) is plain.size:"default"(40px media),"sm"(32px),"xs"(28px).orientation:"horizontal"(default) or"vertical", which stacks the media above the text and floats actions over its corner.AttachmentMedia variant="image"crops animg; the default"icon"centers an icon or spinner.AttachmentTriggerstretches over the card so a click anywhere opens it. It acceptsrenderfor a link.
File type icons
<FileTypeIcon name="Returns policy.pdf" mediaType="application/pdf" />
<FileTypeIcon name="Price list 2026.xlsx" mediaType={type} size="lg" />A decorative paper glyph with the extension in a colored badge: red for PDF, green for sheets, blue for documents and Google files, orange for slides, light blue for images, and gray for everything else. "sm" (default) is 20 × 24px; "lg" is 30 × 36px. A name without an extension shows no badge.
Local files
<AttachmentDropzone accept=".txt,.md,.pdf,.docx" disabled={busy} onFiles={add} />
{files.map((file, index) => (
<LocalFileAttachment key={index} file={file} preview viewable onRemove={() => remove(index)} />
))}AttachmentDropzone takes drops or a “Choose files” button and hands the files to onFiles. It uploads nothing.accept defaults to documents, sheets, slides, PDF, text, JSON, Google shortcuts, images, and common source files.LocalFileAttachment shows a File as a chip with a live image thumbnail, and passes preview andviewable through.
Accessibility
Each open button is named “Inspect name” or “Download name”; remove and retry buttons are named “Remove name” and “Retry name”. Busy chips have a spinner named “Uploading…” or “Processing…”. File type icons are hidden from assistive tech because the name is always beside them.
The carousel is a labelled region. Each item is a group, focus scrolls it into view, and Left and Right scroll it when focus is inside. The edge arrows are for pointers, so Tab never lands on a button that vanishes at the end of the row. The drop zone's button opens the system file picker. Transitions take 120 to 180ms; with reduced motion the title shimmer and smooth scrolling stop.
Theme tokens
Cards use --sprout-surface, --sprout-line,--sprout-hover, and --sprout-danger. File type icons use --sprout-file-paper,--sprout-file-edge, --sprout-file-label, and one badge color per kind: --sprout-file-generic,-pdf, -sheet, -document,-slides, and -image. Each has a light and a dark value.
