Upload Queue Progress
A free upload progress queue react component with real progressbar semantics and one polite summary.
Upload progress
4 files: 2 uploading, 1 waiting, 1 failed.
- campaign-brief.pdf1.2 MB
- hero-render.png8.4 MB
- handover-notes.docx240 KB
- voiceover-take-3.wav42 MB
Customise
Code
<LoaderUploadQueue files={files} onCancel={handleCancel} onRetry={handleRetry} />Upload Queue Progress is a free upload progress queue react component for Next.js. Every row states its own progress and status in words, Cancel and Retry appear only where they can actually do something, and nothing in the component mutates the data it was given: an action reports an id and waits for you to decide what happens next.
The part worth copying is the announcement. One polite summary describes the queue, and it is built from how many rows are in each state rather than from their progress, so it is identical across every percentage tick and changes only when a row moves between states. A live region fed by progress talks several times a second and drowns out everything else on the page. Individual rows keep exact progressbar semantics while they are active and become plain status text once they are complete or failed, because a progressbar frozen at a value is a control that looks live and is not.
Install
Add the @te registry to your components.json once:
{
"registries": {
"@te": "https://templateempire.io/r/{name}.json"
}
}Then install the component:
npx shadcn@latest add @te/loader-upload-queueUsage
"use client";
import { useState } from "react";
import { LoaderUploadQueue } from "@/components/loader-upload-queue";
export default function Page() {
const [files, setFiles] = useState([
{ id: "a", name: "brief.pdf", size: "1.2 MB", progress: 42, status: "uploading" },
{ id: "b", name: "notes.docx", size: "240 KB", progress: 0, status: "queued" },
]);
return (
<LoaderUploadQueue
files={files}
onCancel={(id) => setFiles((rows) => rows.filter((row) => row.id !== id))}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| files | ReadonlyArray<{ id: string; name: string; size: string; progress: number; status: "queued" | "uploading" | "complete" | "error" }> | required | One to eight controlled rows with unique ids; duplicates are dropped, first wins. Progress is clamped to 0-100 and made to agree with status: queued shows 0, complete shows 100, and an error row keeps the value it was given. |
| onCancel | (id: string) => void | undefined | Supply to enable Cancel on queued and uploading rows. Omit it and no Cancel button is rendered at all, rather than one that does nothing. |
| onRetry | (id: string) => void | undefined | Supply to enable Retry on error rows. Omit it and no Retry button is rendered. |
| density | "comfortable" | "compact" | "comfortable" | Row spacing and padding. |
| barHeight | number | 4 | Progress track height in CSS pixels, clamped to 2-10. The track holds its height whatever the fill is doing, so a row's box never moves. |
| settleDuration | number | 0.35 | How long a state transition takes in seconds, clamped to 0-0.8. Also the duration of the completed check's draw. |
| stagger | number | 0.04 | Reveal stagger between newly added rows in seconds, clamped to 0-0.12. |
| iconTravel | number | 8 | How far the state mark travels as it crossfades between states, in CSS pixels, clamped to 0-16. |
| radius | number | 18 | Queue and row corner radius in CSS pixels, clamped to 0-36. |
| label | string | "Upload progress" | The visible heading, which is also the section's accessible name. |
| className | string | undefined | Additional classes merged onto the status section with cn(). |
Customisation
Upload Queue Progress declares three tokens of its own, each falling back to a global semantic token, so it retints from an ancestor without touching the component source. Geometry and timing come from props rather than tokens, because they are clamped: --upload-queue-accent colours the fill, the completed check and every focus ring, --upload-queue-track is the inactive part of the bar, and --upload-queue-error is the failure accent.
:root {
--upload-queue-accent: var(--chart-2);
--upload-queue-track: oklch(from var(--border) l c h / 0.8);
--upload-queue-error: oklch(0.62 0.21 25);
}
<LoaderUploadQueue files={files} density="compact" barHeight={2} radius={8} />Frequently asked questions
- Is Upload Queue Progress free for commercial use?
- Yes. Upload Queue Progress is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Does the live region announce every percentage change?
- No, and the mechanism is worth understanding because it is easy to get wrong. The polite summary is built only from how many rows are in each state, never from their progress, so its text is byte-for-byte identical across every tick and there is nothing for assistive technology to announce. It changes exactly when a row moves between queued, uploading, complete and error, which is the only moment a listener needs telling. The exact percentages are still available on demand: each active row is a progressbar with aria-valuenow, and the number is visible beside it.
- Is Upload Queue Progress accessible for keyboard and screen reader users?
- Yes. The queue is a labelled section containing an ordered list, and each active row exposes a progressbar labelled by its own filename with min, max and now. Once a row completes or fails the progressbar is replaced by plain status text, because a progressbar stuck at a value looks live and is not. Status is always stated in words rather than by colour or icon alone, and every Cancel and Retry button includes its filename in its accessible name so a column of them is still navigable. With reduced motion enabled the stagger, the reflow, the icon travel and the check's draw are all dropped, and nothing about progress, status, actions or focus changes.