Upload Progress
Per-file and aggregate progress bars for in-flight uploads, reading the ambient uploads and progress state a useFiles instance exposes.
A drop-in progress view. It reads the ambient uploads and progress a useFiles instance exposes, so it lights up for any upload that instance starts — single or bulk, wherever the call originates. Returns null until there’s something to show. (The preview below simulates that state, since the docs have no gateway.)
"use client";
import type { FileUploadState, UseFilesResult } from "files-sdk/react";
import { UploadIcon } from "lucide-react";
import type { ChangeEvent } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { demoFiles } from "@/lib/demo-files";
import { UploadProgress } from "@/registry/files-sdk/upload-progress/upload-progress";
// The docs preview has no gateway, so it simulates the ambient `uploads` /
// `progress` state a real `useFiles()` instance exposes: four seeded files
// (and anything you pick) advance on a timer, one sample fails and one is
// cancelled, so every row state shows. In an app you pass `useFiles()`
// directly — see Usage below.
interface Simulated {
state: FileUploadState;
/** Fraction of the file sent per tick. */
step: number;
/** Stop here with this terminal status instead of finishing. */
stopAt?: { fraction: number; status: "error" | "aborted" };
}
const TICK_MS = 250;
const simulate = (
file: File,
step: number,
stopAt?: Simulated["stopAt"],
// Seeded samples are empty Files with a pretend size, so the preview
// doesn't allocate megabytes just to draw a bar.
size = file.size
): Simulated => ({
state: {
file,
loaded: 0,
name: file.name,
progress: 0,
size,
status: "pending",
total: size,
type: file.type || "application/octet-stream",
},
step,
stopAt,
});
const seeded = (): Simulated[] => [
simulate(
new File([], "notes.txt", { type: "text/plain" }),
0.5,
undefined,
20 * 1024
),
simulate(
new File([], "report.pdf", { type: "application/pdf" }),
0.12,
{ fraction: 0.6, status: "error" },
1200 * 1024
),
simulate(
new File([], "vacation-photo.jpg", { type: "image/jpeg" }),
0.04,
undefined,
3 * 1024 * 1024
),
simulate(
new File([], "draft.docx", {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
}),
0.1,
{ fraction: 0.3, status: "aborted" },
900 * 1024
),
];
const advance = (item: Simulated): Simulated => {
const { state, step, stopAt } = item;
if (state.status !== "pending" && state.status !== "uploading") {
return item;
}
const progress = Math.min(1, state.progress + step);
if (stopAt && progress >= stopAt.fraction) {
return {
...item,
state: { ...state, status: stopAt.status },
};
}
return {
...item,
state: {
...state,
loaded: Math.round(progress * state.total),
progress,
status: progress >= 1 ? "success" : "uploading",
},
};
};
const Example = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [items, setItems] = useState<Simulated[]>(seeded);
const isActive = items.some(
({ state }) => state.status === "pending" || state.status === "uploading"
);
useEffect(() => {
if (!isActive) {
return;
}
const timer = setInterval(() => {
setItems((prev) => prev.map(advance));
}, TICK_MS);
return () => clearInterval(timer);
}, [isActive]);
const files = useMemo<UseFilesResult>(() => {
const uploads = items.map(({ state }) => state);
let loaded = 0;
let total = 0;
for (const upload of uploads) {
loaded += upload.loaded;
total += upload.total;
}
return {
...demoFiles,
isUploading: isActive,
progress: { fraction: total === 0 ? 0 : loaded / total, loaded, total },
// Like the hook's reset(): clear finished rows, keep in-flight ones.
reset: () =>
setItems((prev) =>
prev.filter(
({ state }) =>
state.status === "pending" || state.status === "uploading"
)
),
uploads,
};
}, [isActive, items]);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const picked = [...(event.currentTarget.files ?? [])];
event.currentTarget.value = "";
setItems((prev) => [
...prev,
...picked.map((file) => simulate(file, 0.08)),
]);
};
return (
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<Button
onClick={() => inputRef.current?.click()}
type="button"
variant="outline"
>
<UploadIcon />
Choose files
</Button>
<Button onClick={() => files.reset()} type="button" variant="ghost">
Clear finished
</Button>
<input
aria-label="Choose files to upload"
className="hidden"
multiple
onChange={handleChange}
ref={inputRef}
type="file"
/>
</div>
<UploadProgress files={files} />
</div>
);
};
export default Example;
Installation
npx shadcn@latest add https://files-sdk.dev/r/upload-progress.jsonUsage
import { useFiles } from "files-sdk/react";
import { UploadProgress } from "@/components/files-sdk/upload-progress";
export function Uploader() {
const files = useFiles({ endpoint: "/api/files" });
return (
<>
<button onClick={() => files.upload(/* … */)} type="button">
Upload
</button>
<UploadProgress files={files} />
</>
);
}
Each row shows a file’s name and status: a percentage while it uploads, a check when it’s done, the error message when it fails, or “Cancelled” after an abort. The hook keeps every file’s row across upload calls, so finished rows stay until you call files.reset(), which clears them (in-flight rows stay). Once there’s more than one row, an aggregate bar on top shows how many have finished and the overall byte progress.
Props
filesUseFilesResult
A `useFiles()` instance — reads its ambient `uploads` / `progress`.
UseFilesResultclassName?string
string