File uploader: Validation
The file uploader checks every picked or dropped file against accept, maxSize, minSize, maxFiles and your own validate rule. A refused file is never added; its reason is listed under the uploader, in a polite live region.
import { FileUploader, useFileUploaderContext } from "slotsmith/file-uploader";
const KB = 1024;
const MB = 1024 * KB;
/** A file made in the page, so each rule can be tried without finding one on disk. */
const make = (name: string, type: string, size: number) => new File([new Uint8Array(size)], name, { type });
/** Buttons that add made-up files, as a drop would. Inside the provider, so they can call `add`. */
function TryFiles() {
const { add } = useFileUploaderContext();
const tries = [
{ label: "A 6 MB photo", files: () => [make("holiday.jpg", "image/jpeg", 6 * MB)] },
{ label: "A text file", files: () => [make("notes.txt", "text/plain", 2 * KB)] },
{ label: "An empty image", files: () => [make("blank.png", "image/png", 10)] },
{ label: "A draft", files: () => [make("cover-draft.png", "image/png", 80 * KB)] },
{
label: "Four photos",
files: () => ["one", "two", "three", "four"].map((name) => make(`${name}.jpg`, "image/jpeg", 120 * KB)),
},
];
return (
<div role="group" aria-label="Try a file" style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
{tries.map((entry) => (
<button key={entry.label} type="button" onClick={() => add(entry.files())} style={{ background: "transparent" }}>
{entry.label}
</button>
))}
</div>
);
}
export default function Validation() {
return (
<FileUploader.Provider
multiple
// The same tokens as the native input: extensions, MIME types or `image/*`.
accept="image/png,image/jpeg"
// Sizes are in bytes.
maxSize={5 * MB}
minSize={1 * KB}
// The most items the list may hold, counting the ones already in it.
maxFiles={3}
// Your own rule: return a message to refuse the file.
validate={(file) => (file.name.includes("draft") ? `${file.name} is a draft. Upload the final version.` : null)}
>
<div style={{ display: "grid", gap: "0.75rem" }}>
<TryFiles />
<FileUploader.Root>
<FileUploader.Dropzone />
<FileUploader.List />
<FileUploader.Rejections />
</FileUploader.Root>
</div>
</FileUploader.Provider>
);
}The buttons add made-up files, as a drop would, so each rule can be tried without finding a file. Add a 6 MB photo: it is refused as too large. Add four photos: three fit and the fourth is refused.
The rules
Each file is checked in this order, and the first rule it breaks is the one reported:
maxFiles: how many items the list may hold, counting the ones already in it. Withoutmultiplethe limit is 1, and a new file replaces the old one instead of being refused. Dropping twenty files into a list with room for three keeps the first three.accept: the same tokens as the native input, as a string or an array: an extension (.pdf), a MIME type (image/png) or a wildcard (image/*). The browser applies it only to the file dialog, so dropped files are checked here too.maxSizeandminSize, in bytes:5 * 1024 * 1024is 5 MB.validate: your own rule. Return a message to refuse the file, or nothing to allow it.
Sizes are shown with 1024 per step, 5 MB or 512 KB, in the hint and in the messages.
The messages
Refused files are listed by the Rejections slot, a role="status" region with aria-live="polite". New refusals are added to the list, and the Dismiss button clears it. The region is created with the first refusal, so some screen readers announce only the later ones; this is a known library issue. onReject is called with each batch, as { file, reason, message }, for your own handling.
The wording comes from the validation messages: wrongType, tooLarge, tooSmall and tooMany. Override them with validationLabels, or let a locale pack translate them; a pack also writes the sizes in its language’s number format.
The compound parts
This demo is built from the compound parts, so the buttons can reach the uploader: FileUploader.Provider holds the state, and TryFiles reads add from useFileUploaderContext(). FileUploader.Root, .Dropzone, .List and .Rejections render the same parts as the default layout. The overview lists every part.