
File uploads look simple until you handle the real cases: someone drops a 40 MB video into an avatar picker, or picks a PDF where you wanted a photo. This tutorial builds an upload field that handles those politely.
import { FormFieldSet } from "oks-ui/form-field-set";
import "oks-ui/form-field-set.css";1. A basic file field
The value is an array of the browser's own File objects:
const [files, setFiles] = useState<File[]>([]);
<FormFieldSet
type="file"
name="attachment"
label="Attachment"
onChange={(v: unknown) => setFiles(v as File[])}
/>2. Drag and drop
ui="dropzone" turns the field into a drop area, with your own wording:
<FormFieldSet
type="file"
name="attachment"
label="Attachment"
ui="dropzone"
isDroppable
dropLabel="Drop your file here"
dropHelperText="or click to browse — PDF or PNG, up to 5 MB"
browseLabel="Choose a file"
/>Keep the click-to-browse path. Drag and drop is impossible on a phone and awkward with a keyboard.
3. Limits that are enforced, not just written
Three props do the work: accept for file types, maxFileSize in bytes, and maxFiles:
<FormFieldSet
type="file"
name="photos"
label="Photos"
multiple
accept="image/png,image/jpeg"
maxFileSize={5 * 1024 * 1024}
maxFiles={4}
preview="thumbnails"
description="PNG or JPEG, up to 5 MB each, 4 files."
/>A file that doesn't match is refused with a message, so the user finds out immediately instead of after a failed upload. preview="thumbnails" shows picked images as small tiles with a remove button.
Always write the limits in the description too. An error is a poor substitute for knowing the rule in advance.
4. The same rules inside a form
Inside an oks-ui Form, express the limits as validation rules so the form won't submit:
<FormFieldSet
type="file"
name="resume"
label="Résumé"
accept=".pdf"
validation={{
rules: { required: true, accept: ".pdf", maxFileSize: 2 * 1024 * 1024 },
message: { maxFileSize: "That file is over 2 MB — try compressing it." },
}}
/>5. Showing upload progress
The field can show per-file progress while you upload. Give it a function that returns each file's state:
const [progress, setProgress] = useState<Record<string, number>>({});
<FormFieldSet
type="file"
name="photos"
multiple
onChange={(v: unknown) => upload(v as File[])}
getFileUploadState={(file) => ({
status: progress[file.name] === 100 ? "completed" : "uploading",
progress: progress[file.name] ?? 0,
})}
/>Your upload function sends each file and updates progress; the field shows a bar per file and marks it complete.
6. Check the limits on the server too
Browser-side limits are a convenience, not a defence — anything can post to your endpoint. Repeat every check on the server: the file type (by content, not just the name), the size, and how many files. Store uploads outside your web root or in object storage, and give them names you generate rather than the ones you were sent.
A note on accessibility
The field is a real <input type="file"> behind the drop zone, so it works with the keyboard and screen readers, and the error message is announced when a file is refused. Test it by tabbing to the field and pressing Enter.
See every option on the file field page.

