Composer Attachments

The Composer Panel carrying attachments: a strip of 56px tiles above the prompt, the thumbnail for images and the plugin document icon with the file name for everything else, each with its dismiss in the corner. Queued files land one after another, the accent ring drawing clockwise around the tile the way the File Upload block traces its drop zone while a 9px percentage counts up in the corner, then at 100 the percentage blurs out and the dismiss blurs in on the same spot.

Mainproject-sea
57%
cat.jpg

Installation

Dark mode ready - this component follows BoardUI semantic tokens automatically. Set up theme switching.

Pro access

Composer Attachments is a Pro component.

Your BoardUI Pro license unlocks this component and all of its source files. Activate your license once, then install it directly into your project with the CLI.

01

Activate your license

Use the key from your Lemon Squeezy purchase email. You only need to do this once per machine.

npx boardui@latest login YOUR_LICENSE_KEY
02

Install the component

The CLI copies the source and resolves every BoardUI and npm dependency for you.

npx boardui@latest add composer-attachments

This component is included in BoardUI Pro

Get BoardUI Pro now and install it right away.

Upload queue

ComposerWithAttachments owns the queue. Any file in attachments with progress: 0 waits its turn, then scales in, draws its ring and counts up in the corner; files without a progress value show as landed straight away. The upload is simulated so the block stays backend-agnostic: tune it with uploadDuration and uploadGap, and listen with onUploadComplete and onAllUploaded. Add files whenever you like; they join the back of the queue.

Mainproject-sea
57%
const [files, setFiles] = useState<ComposerAttachment[]>([]); function attach(picked: File[]) { setFiles((prev) => [ ...prev, ...picked.map((file) => ({ id: crypto.randomUUID(), name: file.name, kind: kindOf(file), progress: 0, // queued: lands after the ones before it })), ]);} <ComposerWithAttachments attachments={files} onAttachmentsChange={setFiles} // dismissals come back through here uploadDuration={1100} // one simulated upload, ms uploadGap={240} // pause before the next ring starts, ms onUploadComplete={(file) => console.log("landed", file.name)} onAllUploaded={() => console.log("all landed")}/>

Tile kinds

Six kinds. image fills the tile with its src and gets a frosted white dismiss; the document, spreadsheet, presentation, code and video kinds carry the 24px plugin icons the add menu already ships, with the file name in 9px underneath, truncated to the tile. Both ComposerAttachmentStrip and the single ComposerAttachmentTile are exported for surfaces outside the composer.

cat.jpg
payrollz-2026.docx
timetable.xlsx
presentation.key
reactypes.tsx
birthday.mp4
{/* Six tile kinds: images show the thumbnail, the rest carry the plugin document icons */}<ComposerAttachmentStrip attachments={[ { id: "1", name: "cat.jpg", kind: "image", src: "/uploads/cat.jpg" }, { id: "2", name: "payroll.docx", kind: "document" }, { id: "3", name: "timetable.xlsx", kind: "spreadsheet" }, { id: "4", name: "launch.key", kind: "presentation" }, { id: "5", name: "types.tsx", kind: "code" }, { id: "6", name: "birthday.mp4", kind: "video" }, ]} onRemove={(id) => remove(id)}/> {/* Or one tile at a time */}<ComposerAttachmentTile attachment={file} onRemove={() => remove(file.id)} />

Bring your own uploader

The ring is just a number. Skip the queue and hand ComposerPanelthe attachments yourself, mapping each file's real transfer progress onto progress: the ring follows it, and clearing the value marks the file landed.

{/* Real uploads: feed the ring from your own progress events on the core panel */}<ComposerPanel attachments={files.map((file) => ({ ...file, progress: uploads[file.id], // 0–100 draws the ring, undefined means landed }))} onRemoveAttachment={(id) => cancel(id)}/>