Upload queue
FreeFiles on their way up, each with its own progress ring; failures say why with a retry, and rows open and close without the list jumping.
pnpm dlx shadcn@latest add https://caliperui.site/r/upload-queue.jsonUploading 2 of 4 · 4.5 MB of 22.6 MB
- board-pack-q3.pdf4.0 MB
- bank-statements-sept.csv336 KB of 840 KB
- receipts-october.zipWaiting · 17.5 MB
- contract-ostra.docxThe connection dropped. Try again.
When to use #
Use it for
- Attachments, imports and media libraries.
- Any upload of more than one file.
Skip it when
- A single small file; show progress on its button.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/upload-queue.json
Adds its 7 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useEffect, useState } from "react" import { Button } from "@/components/ui/button" import { UploadQueue, type QueuedFile } from "@/components/ui/upload-queue" const startFiles: QueuedFile[] = [ { id: "f1", name: "board-pack-q3.pdf", size: 4_200_000, progress: 100, status: "done" }, { id: "f2", name: "bank-statements-sept.csv", size: 860_000, progress: 40, status: "uploading" }, { id: "f3", name: "receipts-october.zip", size: 18_400_000, progress: 0, status: "queued" }, { id: "f4", name: "contract-ostra.docx", size: 220_000, progress: 62, status: "error", error: "The connection dropped. Try again." }, ]; export function UploadQueueDemo() { const [files, setFiles] = useState(startFiles); useEffect(() => { const id = setInterval(() => { setFiles((current) => { let started = current.some((file) => file.status === "uploading"); return current.map((file) => { if (file.status === "uploading") { const progress = Math.min(100, file.progress + 6 + Math.random() * 10); return progress >= 100 ? { ...file, progress: 100, status: "done" as const } : { ...file, progress }; } if (file.status === "queued" && !started) { started = true; return { ...file, status: "uploading" as const }; } return file; }); }); }, 400); return () => clearInterval(id); }, []); return ( <div className="grid w-full max-w-md gap-3"> <UploadQueue files={files} onRetry={(id) => setFiles((current) => current.map((file) => (file.id === id ? { ...file, status: "queued", progress: 0, error: undefined } : file)))} onCancel={(id) => setFiles((current) => current.filter((file) => file.id !== id))} onClearDone={() => setFiles((current) => current.filter((file) => file.status !== "done"))} /> <Button size="sm" variant="secondary" className="w-fit" onClick={() => setFiles((current) => [...current, { id: `f${Date.now()}`, name: `scan-${current.length + 1}.pdf`, size: 1_300_000, progress: 0, status: "queued" }])}>Add a file</Button> </div> ); }
API reference #
UploadQueue
component
Accessibility #
It exposes status semantics, with aria-label and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.