Upload queue
Several uploads at once: per-file progress, speed and time left, processing, failures with retry and cancel, an overall header with Cancel all, and a fold into attachment chips when every file is done.
On this page
Installation
Usage
import { UploadQueue } from "@/components/ui/chatcn/upload-queue"
<UploadQueue
files={uploads}
onCancel={(id) => uploader.cancel(id)}
onCancelAll={() => uploader.cancelAll()}
onRetry={(id) => uploader.retry(id)}
onRemove={(id) => uploader.remove(id)}
/>The queue is plain props: your uploader owns the transfers and passes one object per file with its status and progress from 0 to 1. Pass bytesPerSecond if your uploader knows it; otherwise the rate is measured from progress. Either way it is smoothed and shown at most once a second, and time left rounds more coarsely as it grows, so neither number jitters.
type UploadQueueFile = {
id: string
name: string
size: number // bytes
type?: string // MIME type, picks the icon with the extension
status: "queued" | "uploading" | "processing" | "done" | "failed" | "canceled"
progress?: number // 0 to 1 while uploading
bytesPerSecond?: number
error?: ReactNode // say how to fix it
processingLabel?: string // "Extracting text"
previewUrl?: string // image thumbnail on the attached chip
}File type icons, sizes and the attached chips come from Attachment, so the queue and the composer's attached row always match. Drag and drop stays with Drop zone: wrap your surface in DropZone and add the accepted files to the queue.
Examples
States
Queued waits with Cancel. Uploading shows a determinate bar, the bytes sent and speed with time left. Processing names the work, such as Extracting text; only the first processing row shimmers. Failed explains the fix and offers Retry. Canceled keeps Retry and Remove. File names truncate in the middle of the name, never the extension.
Attached row
When every file is done, the queue holds the finished list for collapseDelay (800 ms) so the last check registers, then folds into attachment chips. The list button reopens the details. Control it with expanded and onExpandedChange, or turn it off with collapseWhenDone={false}. Inside a composer, use variant="plain" so the queue sits on the composer's surface instead of drawing a card in a card.
With a drop zone
DropZone shows the overlay and rejects wrong types and sizes before the drop; the queue only sees accepted files. Choose files is always there, so dragging is never the only way in.
<DropZone accept=".csv,.pdf,.zip" maxSize={100_000_000} onFiles={(files) => uploader.add(files)}>
<UploadQueue variant="plain" files={uploads} onCancel={uploader.cancel} onRetry={uploader.retry} />
</DropZone>Props
| Prop | Type | Notes |
|---|---|---|
files | UploadQueueFile[] | One entry per file, in display order. Empty renders nothing |
onCancel | (id) => void | Cancel button on queued and uploading files |
onCancelAll | () => void | Cancel all in the header while anything is queued or uploading |
onRetry | (id) => void | Retry on failed and canceled files |
onRemove | (id) => void | Remove on settled files and on the attached chips |
title | ReactNode | Replaces the header title, for example a translated one |
collapseWhenDone | boolean | Fold into attachment chips when every file is done. Defaults to true |
expanded, defaultExpanded, onExpandedChange | boolean | Details open or folded, controlled or uncontrolled |
collapseDelay | number | Ms the finished list stays before folding. Defaults to 800 |
maxHeight | number | string | File list height before it scrolls. Defaults to 320 |
variant | "card" | "plain" | plain sits inside a surface you already have |
UploadQueueItem renders a single row for your own layouts, and formatBytesOf, formatSpeed and formatTimeLeft are exported for matching copy elsewhere.
Keyboard
| Key | Action |
|---|---|
Tab | Move through Cancel all, then each row's Cancel, Retry or Remove |
Enter, Space | Activate the focused button |
Tab in the attached row | Each chip's Remove, then Show upload details |
Accessibility
- Every bar is a
progressbarwith a name (Uploading subscriptions_2026_q1.csv, Total upload progress),aria-valuenowin percent and a value text that includes the time left. - A polite live region announces a failure with the file name, and the end of the batch. Progress is never announced per tick.
- Icon buttons are named with the file: Cancel invoices_2026_03.xlsx, Retry churn-interviews-transcripts.zip.
- Status never rides on color: each state has a label and a glyph, and failures say how to fix them in words.
- The full file name is in the
titlewhen it truncates. Names isolate their direction, so Arabic and Latin names both read correctly in a right-to-left page. - Buttons grow to 44 px on touch screens.
Motion
- Progress fills move with a 200 ms linear transform: they track bytes, not a feeling. Width never animates.
- State glyphs swap with the kit's 150 ms blur crossfade. Files added after the list first renders rise in over 150 ms; files already there never animate.
- The fold into chips waits 800 ms after the last check, then the chips enter with Attachment's 150 ms scale and fade.
- Reduced motion drops the rise and the scale; progress jumps instead of gliding.