Skip to content
Sign in
ComposerUpload queueMenu

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.

Five files for the analytics agent: two transfer at a time, the PDF extracts its text, the archive fails and resumes, then the queue folds into attached chips.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/upload-queue.json

Usage

tsx
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.

ts
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.

Uploading, queued, processing, done, failed, canceled and a long name in one queue, and a single failed file.

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.

Finished uploads fold into chips in the composer. The list button brings the details back.

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.

Drop or choose files: DropZone checks type and size, the queue shows what was accepted.
tsx
<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

PropTypeNotes
filesUploadQueueFile[]One entry per file, in display order. Empty renders nothing
onCancel(id) => voidCancel button on queued and uploading files
onCancelAll() => voidCancel all in the header while anything is queued or uploading
onRetry(id) => voidRetry on failed and canceled files
onRemove(id) => voidRemove on settled files and on the attached chips
titleReactNodeReplaces the header title, for example a translated one
collapseWhenDonebooleanFold into attachment chips when every file is done. Defaults to true
expanded, defaultExpanded, onExpandedChangebooleanDetails open or folded, controlled or uncontrolled
collapseDelaynumberMs the finished list stays before folding. Defaults to 800
maxHeightnumber | stringFile 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

KeyAction
TabMove through Cancel all, then each row's Cancel, Retry or Remove
Enter, SpaceActivate the focused button
Tab in the attached rowEach chip's Remove, then Show upload details

Accessibility

  • Every bar is a progressbar with a name (Uploading subscriptions_2026_q1.csv, Total upload progress), aria-valuenow in 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 title when 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.