File uploader
The slotsmith FileUploader is a React file uploader. Drop or browse files, check them against your limits, preview images, and upload them with progress, retry and cancellation. Leave out the upload function and it is a picker that never touches the network. Every part is a slot you can replace.
Overview
The demo below uses the fallbacks only, and its uploads are simulated in the page: nothing leaves your browser. Drop up to four images or PDFs, or press Enter on the drop zone to browse. Tick the box to make the next uploads fail, then retry them.
No files yet.
import { useState } from "react";
import { FileUploader, type UploadItem } from "slotsmith/file-uploader";
import { fakeUpload } from "../shared/fake-upload";
export default function Overview() {
const [items, setItems] = useState<UploadItem[]>([]);
const [fail, setFail] = useState(false);
const done = items.filter((item) => item.status === "done").length;
return (
<div style={{ display: "grid", gap: "0.75rem" }}>
<label style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
<input type="checkbox" checked={fail} onChange={(event) => setFail(event.target.checked)} />
Make the next uploads fail
</label>
{/* Up to four images or PDFs of 5 MB each, uploaded two at a time. */}
<FileUploader
multiple
accept="image/*,.pdf"
maxSize={5 * 1024 * 1024}
maxFiles={4}
concurrency={2}
value={items}
onValueChange={setItems}
upload={(file, context) => fakeUpload(file, context, { fail })}
/>
<p>
{items.length === 0 ? "No files yet." : `${done} of ${items.length} ${items.length === 1 ? "file" : "files"} uploaded.`}
</p>
</div>
);
}/** What the uploader hands an `upload` function, as far as this stand-in reads it. */
export interface FakeUploadContext {
signal: AbortSignal;
onProgress(percent: number): void;
}
/** Five steps of 200 ms: the whole upload takes one second. */
const STEPS = 5;
const STEP_MS = 200;
/** Waits `ms`, and rejects with an `AbortError` as soon as the signal aborts. */
function wait(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined;
const abort = () => {
clearTimeout(timer);
reject(new DOMException("The upload was cancelled.", "AbortError"));
};
if (signal.aborted) return abort();
timer = setTimeout(() => {
signal.removeEventListener("abort", abort);
resolve();
}, ms);
signal.addEventListener("abort", abort, { once: true });
});
}
/**
* A stand-in for an upload endpoint. Nothing leaves the browser: it reports
* progress in five steps over one second, stops as soon as `signal` aborts,
* and resolves with `/samples/uploaded/<name>`. With `fail`, it fails at 60%
* the way a server error would, so the retry path can be tried.
*/
export async function fakeUpload(
file: File,
{ signal, onProgress }: FakeUploadContext,
{ fail = false }: { fail?: boolean } = {},
): Promise<{ url: string }> {
for (let step = 1; step <= STEPS; step += 1) {
await wait(STEP_MS, signal);
if (fail && step === 3) throw new Error("The server answered 503. Try again.");
onProgress((step / STEPS) * 100);
}
return { url: `/samples/uploaded/${encodeURIComponent(file.name)}` };
}Each feature has a guide with its own live demo: the picker for forms that send files themselves, uploads with progress, cancel and retry, the tile and compact layouts, editing a record that already has files, validation, and virtual lists for batches of hundreds.
Install
npm i slotsmithpnpm add slotsmithyarn add slotsmithbun add slotsmithVirtualFileUploader from slotsmith/virtual also needs @tanstack/react-virtual.
import "slotsmith/file-uploader.css";Import the stylesheet once, at your app's entry. slotsmith/styles.css holds every component's styles instead.
Quick start
Render FileUploader and read the files from onFilesChange. With no upload prop nothing is sent. accept filters the file dialog and is checked again on drop, and maxSize is in bytes.
No files picked.
import { useState } from "react";
import { FileUploader } from "slotsmith/file-uploader";
import "slotsmith/file-uploader.css";
export default function QuickStart() {
// With no `upload` prop nothing is sent: you get the picked File objects.
const [files, setFiles] = useState<File[]>([]);
return (
<div style={{ display: "grid", gap: "0.75rem" }}>
<FileUploader multiple accept="image/*,.pdf" maxSize={5 * 1024 * 1024} onFilesChange={setFiles} />
<p>{files.length === 0 ? "No files picked." : `Picked: ${files.map((file) => file.name).join(", ")}`}</p>
</div>
);
}Guides
- The pickerUse the slotsmith FileUploader as a file picker: leave out upload, read the File objects from onFilesChange, and send them with your form.
- UploadsGive the slotsmith FileUploader an upload function: files are queued with progress, cancel through an AbortSignal, and retry after a failure.
- The tile layoutThe slotsmith FileUploader's tile variant: one square where the picked image replaces the drop zone, for avatars, logos and covers.
- The compact layoutThe slotsmith FileUploader's compact variant: a Browse button and the limits on one line, with the files listed under it, for tight forms and toolbars.
- Editing a recordShow the files a record already has in the slotsmith FileUploader: pass their URLs as defaultValue, then save the URLs onUrlsChange gives back.
- ValidationLimit what the slotsmith FileUploader accepts with accept, maxSize, minSize, maxFiles and your own validate rule; refused files are explained and announced.
- Virtual listsPass virtual to the slotsmith FileUploader for batches of hundreds of files: only the rows in view are rendered, and the rest works as before.
Reference
The API page is generated from the library’s source, so it lists every slot, prop and label the installed version has. The file uploader has two label sections: the parts’ text, and the validation messages for refused files.
- SlotsEvery part you can replace, the props it receives and its fallback.
- PropsEvery prop, grouped, with its type and default.
- Labels & localesEvery string it shows or announces, and how to translate them.
- StylingIts class names and the tokens its stylesheet reads.
- AdaptersReady slot maps for shadcn/ui, MUI, Chakra, Ant Design and Radix Themes.
FileUploader is also a set of compound parts, for a layout of your own. FileUploader.Provider runs the uploader and renders no markup; the others render the parts in it, and useFileUploaderContext() reads its state anywhere inside. The validation guide’s demo is built from them. The list is read from the installed library:
FileUploader.ProviderFileUploader.RootFileUploader.DropzoneFileUploader.CompactFileUploader.ListFileUploader.VirtualListFileUploader.Item(FileUploaderItemView)FileUploader.ActionsFileUploader.Rejections
Adapters
The file uploader works with any design system. Each slot takes a component, so a library’s own surface, buttons, avatar and progress bar can replace the fallbacks while the queue, the validation and the previews stay the same. The Adapters page has ready slot maps for shadcn/ui, MUI, Chakra, Ant Design and Radix Themes, as examples of well-known libraries; the same approach works with your own.
Accessibility
- The drop zone has
role="button"and is in the tab order (tabindex="0", or-1whiledisabled, witharia-disabled). It is named by thedropzonelabel, “Add files” by default. A click, Enter or Space opens the file dialog. Dragging a file over it setsdata-draggingand switches the title to thedropHerelabel. - The file input is a real
<input type="file">, visually hidden but left in the accessibility tree and named by the same label. It is out of the tab order, because the zone and the Browse button open it, but Chrome exposes it as a second “Add files” button to a screen reader’s browse mode; this is a known library issue. Itsacceptandmultiplefollow your props. - The Browse button is a
<button type="button">inside the zone, so it never submits a form. A button inside arole="button"element is a nested interactive control, which some screen readers announce poorly; this is a known library issue. - Progress is a
role="progressbar"witharia-valuenow(the percentage, rounded),aria-valuemin="0"andaria-valuemax="100", named “Uploading” and the file name by theprogresslabel. See it in the uploads guide. - Each item’s actions are buttons named by the
remove,retryandcancellabels. The names do not include the file name, so a list of several files has several “Remove file” buttons; the button sits in the file’s list item. - Image previews are
<img>elements with alt text from thepreviewlabel (“Preview of photo.png”). Other files show a decorative glyph, hidden from assistive technology. - Refused files are listed in a
role="status"region witharia-live="polite", and stay until the Dismiss button, named by thedismisslabel, clears them. The region appears with the first refusal, which some screen readers do not announce; later refusals are added to it and announced without interrupting. This is a known library issue. See the validation guide. - Status changes of an item (Uploading, Uploaded, Failed) are shown as text in the item and are not announced on their own.
With focus on the drop zone, or in the compact layout on the Browse button:
| Keys | Action |
|---|---|
| EnterSpace | Open the file dialog. |
| Tab | Move to the Browse button, then to each item's Cancel, Retry and Remove buttons, then to Dismiss when files were refused. |
Every label can be translated or replaced; see Labels & locales.
Limitations
FAQ
How does the file uploader send files?
Through the upload function you pass: upload(file, { signal, onProgress }) returns a promise with the stored URL. Use fetch, axios or a presigned PUT. Pass signal on so Cancel stops the request, and call onProgress with a percentage. Uploads are queued, three at a time by default.
Can I use the file uploader in a form without uploading?
Yes. Leave out upload and nothing is ever sent: the items stay ready and onFilesChange hands you the File objects, so the form sends them with the rest of its fields.
How do I show the files a record already has?
Pass their URLs as defaultValue. Each becomes a finished item, with a preview when the URL ends in an image extension, and can be removed like any other. onUrlsChange gives you the URLs to save.
In what unit are maxSize and minSize?
Bytes, so 5 MB is 5 * 1024 * 1024. The hint and the rejection messages show the limit formatted with 1024 per step (5 MB, 512 KB). maxFiles counts the items already in the list.
Does the file uploader work in Arabic and right to left?
Yes. A locale pack translates the parts' text and the rejection messages, two sections called fileUploader and fileValidation, and writes the sizes in that language's number format. The layout uses logical properties, so it mirrors inside a right-to-left ancestor.