Mosmox UI
Current version
GitHubnpmjs.com
Content & Media

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:

ValueResult
freeIndependent width and height selection.
1:1Square crop.
3:4Portrait crop.
4:3Landscape crop.
16:9Wide video/banner crop.
9:16Story/mobile crop.
4:5Social portrait crop.
5:4Photo 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:

ValueControlsDescription
aspectRatioAspect ratioShows the crop ratio selector.
cropSizeCrop sizeShows the crop-area size slider.
presetPresetShows Original, Black & White, Warm, Cool, Vintage, and Dramatic presets.
brightness, exposure, contrast, saturation, hue, temperature, tint, vibranceColor correctionShows the corresponding color-adjustment slider.
blur, sharpness, noiseReduction, grayscale, sepia, vignette, pixelate, posterizeEffectsShows the corresponding image-effect control.
invertInvert colorsShows the invert-colors checkbox.
removeBackgroundRemove backgroundShows the background-removal checkbox.
redEyeRed-eye correctionShows the red-eye correction checkbox.
textTextShows the text overlay input.
textColor, textSize, textOpacity, textPositionText settingsShows text color, size, opacity, or position control.
watermarkWatermarkShows the watermark input.
watermarkSize, watermarkOpacity, watermarkPositionWatermark settingsShows watermark size, opacity, or position control.
formatFormatShows the output-format selector.
width, heightOutput dimensionsShows the width or height number input.
qualityQualityShows the export-quality slider.
backgroundBackgroundShows the output-background color picker and live editor background.
zoomSmaller / LargerShows the crop zoom buttons.
rotateRotate left / rightShows both rotation buttons.
flipX, flipYFlip X / Flip YShows 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

PropTypeDefaultDescription
acceptstring—MIME types and extensions accepted by the picker.
aria-labelstringFile uploadAccessible name for the file picker.
classNamestring—Additional class name for the component root.
multiplebooleantrueAllows multiple files.
maxFilesnumber10Maximum queue length.
maxSizenumber—Maximum size per file in bytes.
dropzonebooleantrueEnables drag-and-drop surface.
variantdropzone | compact | buttondropzonePresentation mode.
previewauto | image | noneautoPreview behavior.
autoUploadbooleanfalseUploads 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.
concurrencynumberInfinityMaximum number of simultaneous uploads.
retryCountnumber0Automatic retries after a failed request.
retryDelaynumber500Delay between retry attempts in milliseconds.
onCancelUpload(file) => void—Called when an active or queued upload is cancelled.
outputFormatoriginal | png | jpeg | webporiginalImage editor output format.
qualitynumber0.92Image editor export quality from 0 to 1.
renderFile(file, actions) => ReactNode—Custom file-row renderer.
localeFileUploadLocaleEnglishLabels for the dropzone and file actions.
cropEnabledbooleantrueEnables image editor actions.
editOnSelectbooleantrueOpens the image editor after selecting/dropping an image, before upload. Set to false to keep the editor as a manual row action only.
editorControlsFileUploadEditorControl[]all controlsAllow-list of editor controls to render.
editorAspectRatiosFileUploadCropAspect[]all ratiosAllow-list of crop ratios shown in the editor.
editorSizePresetsFileUploadSizePreset[]—Replaces the aspect-ratio selector with fixed output-size presets and locks Width, Height, Crop Size, and crop resize handles.
cropAspectfree | 1:1 | 3:4 | 4:3 | 16:9 | 9:16 | 4:5 | 5:4freeInitial editor ratio.
cropShaperectangle | circlerectangleCrop frame shape.
valueFileUploadFile[]—Controlled queue.
defaultValueFileUploadFile[][]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.
disabledbooleanfalseDisables interaction.

FileUploadFile

FileUploadFile contains the queue state for one selected file:

FieldTypeDescription
idstringClient-generated identifier used by queue actions.
fileFileThe current browser File. After editing, this is the generated edited file.
statusqueued | uploading | success | errorCurrent queue/upload state.
progressnumberUpload progress from 0 to 100.
errorstringFinal validation or upload error, when present.
previewstringObject URL for image previews, when available.
urlstringURL returned by the upload adapter.
serverIdstringid returned by the upload adapter.
serverNamestringname returned by the upload adapter.
metadataRecord<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:

ActionDescription
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:

  1. A user selects files, drops them on the dropzone, or activates the button variant.
  2. The component applies accept, maxSize, queue-limit, and beforeUpload checks.
  3. Accepted files enter the queue as queued entries and trigger onChange.
  4. Images open the editor automatically when cropEnabled and editOnSelect are both enabled.
  5. Saving an edit replaces the entry's File, resets it to queued, triggers onEditSave, and then starts upload when autoUpload is enabled.
  6. Uploads run within the concurrency limit. Progress updates are emitted through onChange.
  7. A successful upload becomes success and triggers onUpload; a final failure becomes error and triggers onError.

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();
      }}
    />
  );
}