File uploader: The picker
Leave out the upload prop and the file uploader is a picker: nothing is sent, and onFilesChange hands you the picked File objects. Use it when the files go out with the rest of a form, in one request you write.
import { useState, type FormEvent } from "react";
import { FileUploader, formatBytes } from "slotsmith/file-uploader";
export default function Picker() {
// No `upload` prop: nothing is sent, and the value is plain File objects.
const [files, setFiles] = useState<File[]>([]);
const [sent, setSent] = useState<string | null>(null);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
// The files go out with the rest of the form, in one request you control.
const body = new FormData();
for (const file of files) body.append("attachments", file);
// A real form would now call: fetch("/api/reports", { method: "POST", body });
void body;
setSent(files.length ? files.map((file) => `${file.name} (${formatBytes(file.size)})`).join(", ") : "no files");
};
return (
<form onSubmit={submit} style={{ display: "grid", gap: "0.75rem" }}>
<FileUploader
multiple
accept=".csv,.xlsx,.pdf"
maxFiles={3}
onFilesChange={setFiles}
labels={{ title: "Drop a spreadsheet or a PDF, or browse", hint: () => "CSV, Excel or PDF · up to 3 files · sent on submit" }}
/>
<div>
<button type="submit" style={{ background: "transparent" }}>
Send the form
</button>
</div>
<p>{sent === null ? "Nothing sent yet." : `The form would send: ${sent}.`}</p>
</form>
);
}Pick or drop two files, then send the form: the line under it lists what the request would carry. Nothing is uploaded.
What a picker does
Without upload every item stays ready, and the item shows its name and size but no status. Everything else works as in an uploader: the drop zone, the constraints, the image previews and the Remove button.
onFilesChangeis called with every picked file after each change, removals included.onValueChangegives the full items instead, andgetFiles(items)turns them into files.multipleis off by default, like the native input: a second pick replaces the first file.
Sending the files
Build the request from the files you were handed: append each one to a FormData, as the demo does in its submit handler, and send it with fetch or your form library.
In a plain HTML form, give the uploader a name. The form then sends every item under that name. A file picked or dropped in the browser goes as the file, as <input type="file" name> would send it. A file already on a server, shown from a URL in an edit form or uploaded by upload, goes as its URL, so the server can tell the files to keep from the new ones. new FormData(form).getAll(name) returns both, and a form posted without script sends them as multipart/form-data (set encType on the form).
import { useState, type FormEvent } from "react";
import { FileUploader } from "slotsmith/file-uploader";
export default function Form() {
const [sent, setSent] = useState<string[] | null>(null);
// A plain form: the uploader's hidden input sends the files under "attachments".
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const files = new FormData(event.currentTarget).getAll("attachments") as File[];
setSent(files.map((file) => file.name));
// A real form would post here: fetch("/api/messages", { method: "POST", body: new FormData(event.currentTarget) }).
};
return (
<form onSubmit={submit} style={{ display: "grid", gap: "0.75rem", maxWidth: 420 }}>
<FileUploader name="attachments" multiple maxFiles={3} />
<button type="submit" style={{ justifySelf: "start", background: "transparent" }}>
Send
</button>
<p>{sent === null ? "Nothing sent yet." : `Sent: ${sent.join(", ") || "no files"}`}</p>
</form>
);
}Pick or drop files and press Send: the form’s own FormData lists them.
Custom text
The demo replaces two labels: the zone’s title, and the hint under it. The hint is a function that receives the limits (accept, maxSize, maxFiles) already formatted; the default joins them, as in “image/*, .pdf · up to 5 MB”.