File Upload
A validated file picker with dropzone, previews, upload progress, and image editing.
FileUpload handles the complete client-side upload workflow: selecting files, drag-and-drop, validation, previews, queue management, upload progress, retry/remove actions, and controlled values. When enabled for images, it also provides an editor for crop, fixed aspect ratios, free crop, circular output, zoom, moving the crop area, rotation, flipping, and saving the edited file.
Installation
import { FileUpload } from '@mosmox/ui';Basic dropzone
<FileUpload
accept="image/*,.pdf"
multiple
maxFiles={5}
maxSize={8 * 1024 * 1024}
onChange={(files) => console.log(files)}
/>The default presentation is a dropzone. Files can be selected with the native file picker or dragged onto the component. Every accepted file receives a stable id, status, progress value, optional preview URL, and validation error. Set dropzone={false} to remove the dropzone surface; unless variant is set explicitly, the component then uses the compact picker button.
Uploading to a server
Provide an upload callback to connect any transport: fetch, XMLHttpRequest, an SDK, or a signed-upload service. The callback receives the file, an AbortSignal, and a progress reporter.
<FileUpload
autoUpload
upload={async (file, signal, onProgress) => {
const body = new FormData();
body.append('file', file);
const response = await fetch('/api/uploads', { method: 'POST', body, signal });
if (!response.ok) throw new Error('Upload failed.');
onProgress(100);
return { url: (await response.json()).url };
}}
onUpload={(entry) => console.log('Uploaded:', entry.url)}
onError={(message, file) => console.error(file?.name, message)}
/>Without an upload callback, selecting a valid file completes locally and marks it as successful. With autoUpload={false}, queued files show an upload action so users can start each upload independently.
Queue, validation, retry, and cancellation
beforeUpload can approve or reject a file synchronously or asynchronously. concurrency limits simultaneous requests, while retryCount and retryDelay retry failed requests before the file enters the error state. The upload callback receives an AbortSignal; clicking the cancel action aborts the request and returns the file to queued.
<FileUpload
accept="image/*,.pdf"
autoUpload
concurrency={2}
retryCount={2}
retryDelay={800}
beforeUpload={async (file) => {
const allowed = file.size < 10 * 1024 * 1024;
if (!allowed) showToast('The file is too large.');
return allowed;
}}
upload={async (file, signal, onProgress) => {
const body = new FormData();
body.append('file', file);
const response = await fetch('/api/uploads', { method: 'POST', body, signal });
if (!response.ok) throw new Error('Upload failed.');
onProgress(100);
return await response.json();
}}
onCancelUpload={(file) => console.log('Cancelled:', file.file.name)}
/>Use onError for final failures after all retries. retryCount={0} disables automatic retries.
Server metadata
The upload callback may return a URL, server id, display name, and arbitrary metadata. The values are stored on the resulting FileUploadFile entry.
upload={async (file) => ({
id: 'asset_123',
name: file.name,
url: 'https://cdn.example.com/asset_123',
metadata: { folder: 'avatars', checksum: 'sha256:...' },
})}Validation and restrictions
Use accept for MIME types and extensions, maxSize in bytes, maxFiles for the queue limit, and multiple={false} for single-file workflows.
<FileUpload
accept=".png,.jpg,.jpeg"
maxFiles={3}
maxSize={2 * 1024 * 1024}
multiple
onError={(message) => showToast(message)}
/>Accepted values can combine MIME types (image/*, application/pdf) and extensions (.png,.pdf). Invalid files remain visible with an error state so the user understands what happened and can remove them.
Previews and presentation modes
preview="auto" previews image files and uses a file icon for other types. preview="image" keeps image previews enabled explicitly; non-image files still use a file icon. Use none to hide previews. Choose dropzone, compact, or button with variant.
<FileUpload variant="compact" preview="image" accept="image/*" />
<FileUpload variant="button" multiple={false} accept="application/pdf" />Upload without image editing
Set cropEnabled={false} when the component should only select, validate, preview, and upload files. Image files remain ordinary queue entries: the editor does not open automatically and the Edit action is not rendered.
<FileUpload
aria-label="Project files"
accept="image/*,.pdf"
cropEnabled={false}
maxFiles={5}
multiple
/>Image editor
Image editing is enabled by default for image files. The crop button opens an editor with a preview area and settings panel. The panel supports crop ratios, crop size, rotation, horizontal/vertical flipping, and the following image-editing controls:
- Color correction: brightness, exposure, contrast, saturation, hue, temperature, tint, vibrance, grayscale, and sepia.
- Effects: blur, sharpness, noise reduction, vignette, pixelate, posterize, invert colors, background removal, and red-eye correction.
- Presets: original, black and white, warm, cool, vintage, and dramatic.
- Overlays: text and watermark content, color, size, opacity, and position.
- Export: background replacement, output width and height, PNG/JPEG/WebP/original format, and quality/compression.
All controls use the shared Radio, Slider, Select, Input, InputNumber, Checkbox, and Button components. The dialog footer keeps Cancel and Save cropped image actions aligned to the right.
editOnSelect is enabled by default, so selecting or dropping an image opens the editor before the upload callback or upload request runs. Saving the edit returns the file to the queue; with autoUpload, the edited file is uploaded immediately after saving. The Edit action remains available in the file row for later changes.
<FileUpload
accept="image/*"
cropEnabled
editOnSelect
cropAspect="1:1"
cropShape="circle"
maxFiles={1}
multiple={false}
/>The editor applies these controls to the exported canvas. For example, this workflow starts with a landscape crop, exports a WebP at a fixed size, and opens the image editor manually from the file row:
<FileUpload
accept="image/*"
cropAspect="4:3"
editOnSelect={false}
outputFormat="webp"
quality={0.86}
onEditSave={(original, edited) => {
console.log(original.file.name, edited.type, edited.size);
}}
/> All editor changes are applied to the exported canvas when saving. The preview reflects the crop, color filters, background, and transform state immediately; pixel-processing effects such as sharpness, noise reduction, pixelation, posterization, background removal, and red-eye correction are applied during export. The component preserves the existing outputFormat and quality props as the initial export defaults, while the editor also allows changing format and quality per image.
Available crop ratios are:
| Value | Result |
|---|---|
free | Independent width and height selection. |
1:1 | Square crop. |
3:4 | Portrait crop. |
4:3 | Landscape crop. |
16:9 | Wide video/banner crop. |
9:16 | Story/mobile crop. |
4:5 | Social portrait crop. |
5:4 | Photo crop. |
Use cropShape="circle" for avatars and profile images. The saved result preserves the selected geometry and replaces the queued file, so the server receives the edited output.
outputFormat accepts original, png, jpeg, or webp; quality controls the canvas export quality from 0 to 1. Circular crops use a square 1:1 crop area so the editor does not produce an oval selection.
<FileUpload
accept="image/*"
cropAspect="4:3"
cropShape="rectangle"
outputFormat="webp"
quality={0.86}
onEditStart={(file) => console.log('Editing:', file.file.name)}
onEditCancel={(file) => console.log('Cancelled:', file.file.name)}
onEditSave={(original, edited) => console.log('Edited:', original.file.name, edited.type)}
/>Configuring editor controls
The editor shows every control by default. Use editorControls to provide an allow-list of controls for a specific instance, or set the same values through ConfigProvider.components.fileUpload. editorAspectRatios independently controls which crop ratios are available.
<FileUpload
accept="image/*"
editorControls={[
'aspectRatio',
'cropSize',
'brightness',
'contrast',
'watermark',
'watermarkSize',
'watermarkOpacity',
'watermarkPosition',
'format',
'quality',
]}
editorAspectRatios={['1:1', '4:3', '16:9']}
/>Available editorControls values are:
| Value | Controls | Description |
|---|---|---|
aspectRatio | Aspect ratio | Shows the crop ratio selector. |
cropSize | Crop size | Shows the crop-area size slider. |
preset | Preset | Shows Original, Black & White, Warm, Cool, Vintage, and Dramatic presets. |
brightness, exposure, contrast, saturation, hue, temperature, tint, vibrance | Color correction | Shows the corresponding color-adjustment slider. |
blur, sharpness, noiseReduction, grayscale, sepia, vignette, pixelate, posterize | Effects | Shows the corresponding image-effect control. |
invert | Invert colors | Shows the invert-colors checkbox. |
removeBackground | Remove background | Shows the background-removal checkbox. |
redEye | Red-eye correction | Shows the red-eye correction checkbox. |
text | Text | Shows the text overlay input. |
textColor, textSize, textOpacity, textPosition | Text settings | Shows text color, size, opacity, or position control. |
watermark | Watermark | Shows the watermark input. |
watermarkSize, watermarkOpacity, watermarkPosition | Watermark settings | Shows watermark size, opacity, or position control. |
format | Format | Shows the output-format selector. |
width, height | Output dimensions | Shows the width or height number input. |
quality | Quality | Shows the export-quality slider. |
background | Background | Shows the output-background color picker and live editor background. |
zoom | Smaller / Larger | Shows the crop zoom buttons. |
rotate | Rotate left / right | Shows both rotation buttons. |
flipX, flipY | Flip X / Flip Y | Shows the corresponding flip button. |
editorControls is an allow-list: values that are omitted are not rendered. Passing [] hides all optional editor controls. If the list is omitted, the component uses the complete default list.
The same defaults can be set globally:
<ConfigProvider
components={{
fileUpload: {
editorControls: ['aspectRatio', 'cropSize', 'watermark', 'watermarkOpacity', 'watermarkPosition'],
editorAspectRatios: ['1:1', '4:3'],
},
}}
>
<FileUpload accept="image/*" />
</ConfigProvider>editorAspectRatios accepts free, 1:1, 3:4, 4:3, 16:9, 9:16, 4:5, and 5:4. The array order is preserved in the editor. If it is omitted, all eight ratios are displayed. It is recommended that cropAspect is included in the custom list so the initial crop has a visible matching option.
Fixed size presets
Use editorSizePresets when the application must offer predefined output sizes instead of an editable aspect-ratio crop. The editor replaces the aspect-ratio controls with the shared Select component. Selecting a preset updates the visible Width and Height values, while those fields, Crop Size, and crop resize handles are disabled so the user cannot change the configured dimensions.
import type { FileUploadSizePreset } from '@mosmox/ui';
const imageSizes: FileUploadSizePreset[] = [
{ label: 'Avatar', value: 'avatar', width: 400, height: 400 },
{ label: 'Social post', value: 'social-square', width: 1080, height: 1080 },
{ label: 'Story', value: 'story', width: 1080, height: 1920 },
];
<FileUpload
accept="image/*"
editorSizePresets={imageSizes}
editorControls={['aspectRatio', 'width', 'height', 'cropSize', 'quality', 'format']}
/>Each preset requires a unique value and positive width and height values. label is optional. The Select displays label together with the dimensions, for example Story — 1080 × 1920; without a label it displays only the dimensions. When editorSizePresets is provided, the first preset is selected initially.
Custom file rows and localization
renderFile replaces the default file row and receives actions for edit, upload, retry, cancel, and remove. locale customizes the visible upload and action labels.
<FileUpload
locale={{ browse: 'Add project asset', edit: 'Crop', remove: 'Delete' }}
renderFile={(entry, actions) => (
<article>
<strong>{entry.file.name}</strong>
<span>{entry.status} · {entry.progress}%</span>
{entry.status === 'queued' && <button onClick={actions.upload}>Upload</button>}
{entry.status === 'uploading' && <button onClick={actions.cancelUpload}>Cancel</button>}
<button onClick={actions.remove}>Remove</button>
</article>
)}
/>Controlled values
Pass value when the application owns the queue. The value uses FileUploadFile[] entries and can be restored from application state for already uploaded files.
const [files, setFiles] = useState<FileUploadFile[]>([]);
<FileUpload
value={files}
onChange={setFiles}
multiple={false}
maxFiles={1}
/>Use disabled to disable all interaction. The component also supports defaultValue for an uncontrolled initial queue, className, and an accessible aria-label.
ConfigProvider and tokens
<ConfigProvider components={{
fileUpload: {
variant: 'dropzone',
preview: 'auto',
cropEnabled: true,
cropAspect: '4:3',
cropShape: 'rectangle',
},
}}>
<FileUpload accept="image/*" />
</ConfigProvider>The component uses shared surface, border, focus, primary, muted-text, and overlay tokens and follows the active theme.
API reference
FileUpload props
| Prop | Type | Default | Description |
|---|---|---|---|
accept | string | — | MIME types and extensions accepted by the picker. |
aria-label | string | File upload | Accessible name for the file picker. |
className | string | — | Additional class name for the component root. |
multiple | boolean | true | Allows multiple files. |
maxFiles | number | 10 | Maximum queue length. |
maxSize | number | — | Maximum size per file in bytes. |
dropzone | boolean | true | Enables drag-and-drop surface. |
variant | dropzone | compact | button | dropzone | Presentation mode. |
preview | auto | image | none | auto | Preview behavior. |
autoUpload | boolean | false | Uploads valid files immediately. |
upload | (file, signal, onProgress) => Promise<{ id?, metadata?, name?, url? } | void> | — | Server upload adapter. The signal supports cancellation. |
beforeUpload | (file) => boolean | Promise<boolean> | — | Async or sync approval hook before a file enters the queue. |
concurrency | number | Infinity | Maximum number of simultaneous uploads. |
retryCount | number | 0 | Automatic retries after a failed request. |
retryDelay | number | 500 | Delay between retry attempts in milliseconds. |
onCancelUpload | (file) => void | — | Called when an active or queued upload is cancelled. |
outputFormat | original | png | jpeg | webp | original | Image editor output format. |
quality | number | 0.92 | Image editor export quality from 0 to 1. |
renderFile | (file, actions) => ReactNode | — | Custom file-row renderer. |
locale | FileUploadLocale | English | Labels for the dropzone and file actions. |
cropEnabled | boolean | true | Enables image editor actions. |
editOnSelect | boolean | true | Opens the image editor after selecting/dropping an image, before upload. Set to false to keep the editor as a manual row action only. |
editorControls | FileUploadEditorControl[] | all controls | Allow-list of editor controls to render. |
editorAspectRatios | FileUploadCropAspect[] | all ratios | Allow-list of crop ratios shown in the editor. |
editorSizePresets | FileUploadSizePreset[] | — | Replaces the aspect-ratio selector with fixed output-size presets and locks Width, Height, Crop Size, and crop resize handles. |
cropAspect | free | 1:1 | 3:4 | 4:3 | 16:9 | 9:16 | 4:5 | 5:4 | free | Initial editor ratio. |
cropShape | rectangle | circle | rectangle | Crop frame shape. |
value | FileUploadFile[] | — | Controlled queue. |
defaultValue | FileUploadFile[] | [] | Initial uncontrolled queue. |
onChange | (files) => void | — | Called when the queue changes. |
onUpload | (file) => void | — | Called after a file succeeds. |
onError | (message, file?: File) => void | — | Called for validation or final upload errors. |
onEditStart | (file) => void | — | Called when the image editor opens. |
onEditCancel | (file) => void | — | Called when editing is cancelled. |
onEditSave | (original, edited) => void | — | Called after an edited image is generated. |
disabled | boolean | false | Disables interaction. |
FileUploadFile
FileUploadFile contains the queue state for one selected file:
| Field | Type | Description |
|---|---|---|
id | string | Client-generated identifier used by queue actions. |
file | File | The current browser File. After editing, this is the generated edited file. |
status | queued | uploading | success | error | Current queue/upload state. |
progress | number | Upload progress from 0 to 100. |
error | string | Final validation or upload error, when present. |
preview | string | Object URL for image previews, when available. |
url | string | URL returned by the upload adapter. |
serverId | string | id returned by the upload adapter. |
serverName | string | name returned by the upload adapter. |
metadata | Record<string, unknown> | Arbitrary metadata returned by the upload adapter. |
The component owns object URLs created for previews and revokes them when entries are replaced, removed, or unmounted. Do not persist preview as a server URL; persist url or your own server identifier instead.
FileUploadActions
renderFile receives these actions for the corresponding queue entry:
| Action | Description |
|---|---|
edit() | Opens the image editor when the entry is an image and editing is enabled. |
upload() | Queues the entry for upload. |
retry() | Clears its error, resets progress, and queues it again. |
cancelUpload() | Removes it from the pending queue and aborts its active request. |
remove() | Cancels any pending request, revokes its preview, and removes the entry. |
Actions are deliberately passed to custom renderers instead of requiring consumers to manipulate FileUploadFile state directly.
FileUploadUploadResult
The upload callback may resolve with void or an object containing any of these optional fields:
type FileUploadUploadResult = {
id?: string;
name?: string;
url?: string;
metadata?: Record<string, unknown>;
};The resolved fields are copied onto the entry before onUpload runs. Rejecting the promise marks the entry as error after the configured retries and calls onError.
FileUploadLocale
locale is a partial object. Omitted labels use the English defaults:
type FileUploadLocale = {
browse?: string;
cancel?: string;
cancelUpload?: string;
edit?: string;
remove?: string;
retry?: string;
saveCrop?: string;
uploading?: string;
};browse is the dropzone prompt, edit/remove/retry label file-row actions, cancelUpload labels active-upload cancellation, uploading labels in-progress entries, and cancel/saveCrop label the image editor dialog actions.
FileUploadSizePreset
Each fixed-size preset has a unique value and positive output dimensions:
type FileUploadSizePreset = {
value: string;
width: number;
height: number;
label?: string;
};When editorSizePresets is supplied, the editor uses the shared Select to choose a preset, displays the named dimensions, and disables manual width, height, Crop Size, and crop-edge resizing. The selected dimensions describe the saved output; the on-screen crop frame remains responsive to the editor viewport.
Processing and lifecycle details
The normal lifecycle is:
- A user selects files, drops them on the dropzone, or activates the button variant.
- The component applies
accept,maxSize, queue-limit, andbeforeUploadchecks. - Accepted files enter the queue as
queuedentries and triggeronChange. - Images open the editor automatically when
cropEnabledandeditOnSelectare both enabled. - Saving an edit replaces the entry's
File, resets it toqueued, triggersonEditSave, and then starts upload whenautoUploadis enabled. - Uploads run within the
concurrencylimit. Progress updates are emitted throughonChange. - A successful upload becomes
successand triggersonUpload; a final failure becomeserrorand triggersonError.
If upload is omitted, an accepted entry is completed locally with success and progress: 100; no network request is made. If autoUpload is false, the default row exposes an upload action when an upload callback exists.
beforeUpload runs before an entry is added. Returning false or resolving to false rejects the file without adding it. Throwing from the hook rejects it and sends the error to onError.
maxFiles limits additions against the current queue. With multiple={false}, only one file is retained. Setting maxFiles={1} also forces single-file behavior even if multiple is true.
The upload adapter receives an AbortSignal. It should pass that signal to the transport and call onProgress with values from 0 to 100:
upload={async (file, signal, onProgress) => {
const response = await uploadWithYourClient(file, {
signal,
onProgress: (value) => onProgress(Math.round(value)),
});
return { id: response.id, url: response.url };
}}Cancellation returns an active entry to queued. A retry starts from 0 progress. retryCount is the number of additional attempts after the initial request, and retryDelay is applied between attempts.
Complete configuration example
The following example combines queue restrictions, a custom transport, manual editing, fixed editor controls, and controlled state:
import { useState } from 'react';
import { FileUpload, type FileUploadFile, type FileUploadSizePreset } from '@mosmox/ui';
const sizes: FileUploadSizePreset[] = [
{ value: 'avatar', label: 'Avatar', width: 400, height: 400 },
{ value: 'story', label: 'Story', width: 1080, height: 1920 },
];
export function AssetUpload() {
const [files, setFiles] = useState<FileUploadFile[]>([]);
return (
<FileUpload
value={files}
onChange={setFiles}
accept="image/*,.pdf"
multiple
maxFiles={5}
maxSize={8 * 1024 * 1024}
editOnSelect={false}
editorSizePresets={sizes}
editorControls={['aspectRatio', 'format', 'quality', 'background', 'zoom', 'rotate', 'flipX', 'flipY']}
autoUpload
concurrency={2}
retryCount={2}
upload={async (file, signal, onProgress) => {
const body = new FormData();
body.append('file', file);
const response = await fetch('/api/assets', { method: 'POST', body, signal });
if (!response.ok) throw new Error('Upload failed.');
onProgress(100);
return response.json();
}}
/>
);
}