Components · Files
FileUpload
A drop zone and browse button that checks type, size and count, explains refusals and hands files to the product.
With an upload queue
Drag and drop files here
Images or PDF, up to 10 MB each, 5 files max
import { useEffect, useRef, useState } from 'react';
import { FileUpload, UploadQueue, type UploadQueueItem } from '@rookcom/monarch-react/collaboration';
const MB = 1024 * 1024;
/** Simulates a transfer so the example shows progress; a real product reports its own. */
function UploadWithQueue() {
const [items, setItems] = useState<UploadQueueItem[]>([]);
const timers = useRef(new Map<string, ReturnType<typeof setInterval>>());
useEffect(() => {
const map = timers.current;
return () => map.forEach((t) => clearInterval(t));
}, []);
const start = (id: string) => {
clearInterval(timers.current.get(id));
timers.current.set(
id,
setInterval(() => {
setItems((list) =>
list.map((i) => {
if (i.id !== id || i.status !== 'uploading') return i;
const progress = Math.min(100, (i.progress ?? 0) + 20);
if (progress < 100) return { ...i, progress };
clearInterval(timers.current.get(id));
return { ...i, progress, status: 'done' };
}),
);
}, 400),
);
};
return (
<div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 480 }}>
<FileUpload
accept="image/*,application/pdf"
maxSize={10 * MB}
maxFiles={5}
currentCount={items.filter((i) => i.status !== 'cancelled').length}
hint="Images or PDF, up to 10 MB each, 5 files max"
paste
onFiles={(accepted) => {
const added = accepted.map((f) => ({
id: `${f.name}-${f.lastModified}-${Math.random()}`,
name: f.name,
size: f.size,
status: 'uploading' as const,
progress: 0,
}));
setItems((list) => [...list, ...added]);
added.forEach((a) => start(a.id));
}}
/>
{items.length ? (
<UploadQueue
items={items}
onCancel={(id) => {
clearInterval(timers.current.get(id));
setItems((l) => l.map((i) => (i.id === id ? { ...i, status: 'cancelled' } : i)));
}}
onRetry={(id) => {
setItems((l) =>
l.map((i) => (i.id === id ? { ...i, status: 'uploading', progress: 0 } : i)),
);
start(id);
}}
onRemove={(id) => setItems((l) => l.filter((i) => i.id !== id))}
onClearFinished={() => setItems((l) => l.filter((i) => i.status === 'uploading'))}
/>
) : null}
</div>
);
}
<UploadWithQueue />Usage examples
In a field, single file
import { Field, FieldDescription, FieldLabel } from '@rookcom/monarch-react';
import { FileUpload } from '@rookcom/monarch-react/collaboration';
const MB = 1024 * 1024;
<Field style={{ width: '100%', maxWidth: 480 }}>
<FieldLabel>Résumé</FieldLabel>
<FileUpload accept=".pdf,.doc,.docx" maxFiles={1} maxSize={5 * MB} />
<FieldDescription>
PDF or Word document. Shared with the hiring team.
</FieldDescription>
</Field>Compact, and limit reached
import { FileUpload } from '@rookcom/monarch-react/collaboration';
const MB = 1024 * 1024;
<div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 480 }}>
<FileUpload variant="compact" accept="image/*" maxSize={2 * MB} />
<FileUpload variant="compact" maxFiles={3} currentCount={3} />
</div>The same checks without the zone
<p style={{ margin: 0 }}>
<code>validateFiles</code> returns <code>{'{ accepted, rejected }'}</code> with the
same messages, for a composer’s own attach button.
</p>Purpose
Lets people choose files by dropping them, browsing or pasting, checks them against the limits the product sets, and gives the accepted files to the product, which uploads them.
Use when
- A form or screen accepts attachments, images or documents: profile photos, résumés, banners, ticket attachments.
- Limits on type, size or count must be explained before upload instead of failing on the server.
- Pair with UploadQueue to show the progress the product reports.
Avoid when
- Files are attached inline in a composer with its own attach button: use the composer’s button and call
validateFilesyourself. - The user picks from files already stored in the product: use a picker of existing items.
Design principles
- The component never uploads. It emits
onFiles(accepted, rejected); the product transfers, and reports progress through UploadQueue. - The browse button is always the keyboard and screen reader path; the drop zone is an extra for pointer users and is not focusable.
- Refusals are specific and name the file: "scan.png is larger than 10 MB.", not "Invalid file".
- Limits are stated up front in the hint (generated from
maxSizeandmaxFilesunlesshintis given). - The same checks are available on their own as
validateFiles, so composers and other entry points refuse files the same way.
API reference
import { FileUpload, fileMatchesAccept, formatBytes, validateFiles } from '@rookcom/monarch-react/collaboration';| Export | Kind | Rendering | Root class |
|---|---|---|---|
FileUpload | component | client | .mn-file-upload |
fileMatchesAccept | utility | client | – |
formatBytes | utility | client | – |
validateFiles | utility | client | – |
onFiles(accepted, rejected) runs on every pick, drop and paste. rejected items are { file, code, message }, with code one of file-invalid-type, file-too-large, too-many-files, custom.
accept uses the input syntax (image/*,.pdf,application/zip) and is checked again after the pick, because drops and pastes skip the browser’s filter.
maxFiles counts currentCount (files already attached); when it is reached the zone is disabled and says so.
validateFiles(files, options), fileMatchesAccept(file, accept) and formatBytes(bytes) are exported for use elsewhere.
Inside a Field, the browse button takes the Field label and description and reflects invalid.
The ref is the root <div>; other div attributes and drag/paste handlers pass through.
Generated from the TypeScript declarations of packages/react/src/components/files/file-upload.
Properties
FileUpload
A drop zone with a browse button for choosing files. It checks type, size and count, shows clear messages for refused files, and hands the accepted ones to the product, which does the upload.
| Prop | Type | Description |
|---|---|---|
accept | string | Same syntax as the input `accept` attribute: `image/*,.pdf,application/zip`. |
capture | "user" | "environment" | The input `capture` attribute, for camera capture on phones. |
className | string | |
currentCount | number | Files already attached, which count towards `maxFiles`. Default 0. |
disabled | boolean | |
hint | ReactNode | Text under the label. Default: generated from `maxSize` and `maxFiles`. |
icon | ReactNode | Icon at the start of the zone. Decorative. |
id | string | |
invalid | boolean | Mark the zone invalid, for example when a required file is missing. |
maxFiles | number | Most files allowed in total, counting `currentCount`. |
maxSize | number | Largest allowed size per file, in bytes. |
messages | Partial<FileUploadMessages> | Override labels and messages. |
multiple | boolean | Allow choosing several files. Default: true unless `maxFiles` is 1. |
name | string | Name of the hidden file input. |
onFiles | ((accepted: File[], rejected: FileRejection[]) => void) | Called with every pick, drop or paste: the files that passed the checks and the ones that did not. The product uploads `accepted` and reports progress (for example through UploadQueue). |
paste | boolean | Also accept files pasted (Ctrl/Cmd+V) while focus is inside the component. |
showErrors | boolean | Show refusal messages under the zone (default true). Set false to show them yourself from `onFiles`. |
validate | ((file: File) => string | null | undefined) | Extra check. Return a message to refuse the file, or null to allow it. |
variant | "compact" | "zone" | `zone` (default): a bordered drop area. `compact`: one line, for tight spaces. |
Variants
| Prop | Values | Description |
|---|---|---|
variant | 'zone''compact' | zone (default): a centred, dashed drop area. compact: one row with the icon, text and button, for side panels and dense forms. |
multiple | 'true''false' | Default true unless maxFiles is 1. A single-file upload refuses extra dropped files. |
paste | 'true''false' | Also take files pasted while focus is inside the component. |
showErrors | 'true''false' | Show refusal messages under the zone (default), or leave them to the product. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Drag over | data-drag="accept" | Files are over the zone: accent border and background, "Drop to upload". |
| Drag rejected | data-drag="reject" | The dragged files’ types do not match accept (when the browser exposes them): danger border, "This file type is not allowed". |
| Refused files | data-invalid | The last pick had refused files: their messages are listed under the zone. |
| Disabled or full | data-disabled | disabled, or currentCount has reached maxFiles ("Limit reached"). The button is disabled and drops are ignored. |
| Focus | – | The browse button shows the Monarch focus ring on :focus-visible. |
Interaction behaviour
- Browse opens the system file picker. The hidden input is cleared after each pick, so choosing the same file again works.
- Dropping files anywhere on the component takes them; dragging text or links over it does nothing.
- Each pick replaces the previous refusal messages.
- Files over the count limit are refused in order, after the type and size checks, so the first valid files are kept.
validateadds a synchronous check of your own (return a message to refuse). Checks that need the network, such as virus scanning, belong in the upload flow and show in UploadQueue.- On phones,
capture="environment"or"user"opens the camera.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves to the browse button. |
| Enter / Space | Opens the system file picker. |
| Ctrl+V / Cmd+V | With paste: adds files from the clipboard while focus is inside. |
Accessibility
- Role
- button (browse); the zone has no role
- Accessible name
- "Browse files" by default. Inside a Field it is "{Field label} Browse files".
- The button is described by the hint, the Field description and any refusal messages, so they are read with it.
- Refusal messages sit in a polite live region and are announced when they appear; accepted files are announced as "2 files added."
- The file input is visually hidden, out of the tab order and hidden from assistive technology; the button is the single control.
- The drop zone is never the only way in (people with keyboards, switches or voice use the button).
- Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/file-upload.
Responsive behaviour
- The zone fills its container and wraps its text; it works at 320px.
compactwraps the button under the text when the row is too narrow.- Long file names in messages break anywhere rather than overflow.
Theme support
- Colours are Monarch semantic tokens only, so light, dark and more-contrast themes apply automatically.
- The zone uses
--mn-color-bg-subtlewith a dashed--mn-color-border-strongborder; drag states use the accent and danger subtle/strong pairs. - Reduced motion removes the border and background transition. Forced colours draw the drag state with
Highlight.
Do and don't
Do
- Set
accept,maxSizeandmaxFilesto the limits the server enforces, so people learn them before uploading. - Keep checking on the server too: client checks are for clarity, not security.
- Show the upload itself with UploadQueue, including failures and retry.
Don't
- Do not rely on drag and drop alone: keep the browse button visible.
- Do not show a generic "Upload failed" for a refusal the component already explained.
- Do not put business rules about storage (buckets, keys, quotas) in the component; pass the resulting limits.
Related components
- UploadQueue: Shows the progress, status and retry of the files this component hands over.
- Field: Gives the browse button a label, description and invalid state.
- Progress: A single progress bar, when a list is not needed.
Dependencies
Testing coverage
- Accept rules (MIME, wildcard, extension), size, custom and count checks, with messages that name the file; byte formatting.
- Browse opens the picker from the keyboard; picked files are split into accepted and refused; messages describe the button and are cleared on the next pick; the input is reset.
- Single-file mode refuses extra files; drag-over accept and reject states; drops; paste only when enabled.
- Disabled and limit-reached states ignore input; errors can be left to the product.
- Field label, description and invalid state reach the button; compact variant, custom icon and translated messages; axe; server rendering.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/file-upload; Cross-browser axe: /components/file-upload |
Tests: packages/react/src/components/files/file-upload