Form controls

File Upload
v1.1.4
Documentation Under Review

A flexible file upload component with drag-and-drop support, file validation, progress tracking, and customizable trigger coloration built for product applications.

Prefer AI-assisted development?

This documentation is available via the Catalyst MCP Server. Learn how to connect →

Catalyst File Upload wraps react-dropzone with a compound-component API and built-in Catalyst design tokens — giving you drag-and-drop, validation, progress tracking, and error recovery with zero boilerplate.

Preview
Maximum of 10 files allowed

Installation

npm install @pmi/catalyst-file-upload
import {
  FileUploadDropzone,
  FileUploadDropzoneArea,
  FileUploadTrigger,
  FileUploadDescription,
  FileUploadMessage,
  FileUploadFileList,
  FileUploadFileListItem,
  FileUploadFileMessage,
  FileUploadRemoveFile,
  FileUploadRetryFile,
  useFileUpload,
  useFileUploadFileListContext,
} from '@pmi/catalyst-file-upload';

Add the preset to your tailwind.config.ts:

import { tailwindFileUploadPreset } from '@pmi/catalyst-file-upload';

export default {
  presets: [tailwindFileUploadPreset],
  // ...
};

Variants

Background

Use coloration="background" on FileUploadTrigger to apply a subtle filled background to the dropzone area.

Preview
PNG, JPG or JPEG images up to 10MB

Neutral

Use coloration="neutral" for a white (light) / dark-surface (dark) dropzone background.

Preview
Any file type is accepted

Danger

Set isInvalid on FileUploadDropzone to communicate a validation error — the trigger and message adopt red tones, and FileUploadDescription is hidden so FileUploadMessage takes its place.

Preview
File upload failed. Please try uploading the file again.

Disabled

Set disabled on FileUploadDropzone to prevent file selection and apply reduced-opacity styles.

Preview
Upload functionality is disabled

Common Patterns

Card Wrapper

Wrap the upload component in a Card for elevated visual hierarchy on content-rich pages.

Preview
Any file type is accepted

Preview Image

Display uploaded images with aspect-ratio preview thumbnails and per-item progress bars.

Preview
Maximum of 5 images allowed

Preview Image Card

Combine image previews with a Card wrapper for an elevated, content-rich upload experience. Uploaded images render as aspect-ratio thumbnails inside a bordered card container.

Preview
Maximum of 5 images allowed

Slow Upload Announcements

For long-running uploads, keep screen reader users informed with a single role="status" / aria-atomic="true" live region that is always rendered. Completion is always announced ("File uploaded successfully: photo.jpg."); files that finish together are combined into one message ("2 files uploaded successfully: a.jpg, b.png."). Once an upload takes longer than 3 seconds, the region also announces when it starts ("Uploading photo.jpg…") and its progress every 10 seconds. Focus never moves.

Preview
Maximum of 10 files allowed

API Reference

File Upload is a compound component built on react-dropzone. The sections below summarize the API surface for each part.

FileUploadDropzone

The root context provider. Spread the return value of useFileUpload onto it and add isInvalid / disabled as needed.

Accepts all props returned by useFileUpload plus children. Forwards that state to all descendant components via context.

FileUploadTrigger

The dashed-border dropzone area. Renders a <label> wrapping a hidden <input type="file">.

Prop

Type

FileUploadDescription

Descriptive helper text displayed below the trigger (accepted types, size limits, etc.).

Accepts all standard <p> HTML attributes. The unstyled prop removes default CVA classes for full className override.

FileUploadMessage

Root-level validation / error message. Reads the dropzone's rootError from context and auto-renders when present.

Prop

Type

FileUploadFileList

Container (<ul>) for the list of uploaded file items. It has no accessible name by default; pass aria-labelledby pointing to a visible heading (e.g. "Uploaded Files").

Accepts all standard <ul> HTML attributes. The unstyled prop removes default CVA classes.

FileUploadFileListItem

Individual file row (<li>). Automatically derives isInvalid from file.status === 'error' and, in that state, sets aria-describedby to the item's FileUploadFileMessage.

Prop

Type

FileStatus

Prop

Type

FileUploadFileMessage

Per-item message inside FileUploadFileListItem. Shows the file's error automatically when its status is 'error'.

Accepts all standard <p> HTML attributes.

FileUploadRemoveFile

Wrapper for the remove button. Use asChild to render your own Button component inside it. Removes the file on click and moves focus (see Focus management); don't add your own onClick={onRemoveFile}.

Accepts all standard <button> HTML attributes plus asChild and unstyled.

FileUploadRetryFile

Wrapper for the retry button on failed uploads. Use asChild to render your own Button component inside it. Calls onRetry on click and sets aria-disabled when the file can't be retried.

Its default accessible name is the generic "retry", so pass a descriptive aria-label such as `Retry upload ${file.fileName}`.

Accepts all standard <button> HTML attributes plus asChild and unstyled.

useFileUpload

Hook that wires react-dropzone with Catalyst's file-status reducer. Pass its return value to FileUploadDropzone.

const dropzone = useFileUpload({
  onDropFile: async (file: File) => {
    return { status: 'success', result: uploadedUrl };
    // or on failure:
    return { status: 'error', error: 'Upload failed.' };
  },
  validation: {
    accept: { 'image/*': ['.png', '.jpg', '.jpeg'] },
    maxSize: 10 * 1024 * 1024,
    maxFiles: 10,
  },
});

Returns fileStatuses, isInvalid, disabled, getInputProps, and other dropzone state.

useFileUploadFileListContext

Access per-item context inside a FileUploadFileListItem for remove / retry callbacks.

Returns onRemoveFile, onRetry, canRetry, fileStatus, disabled, messageId, and dropzoneId (the file input ID).

Accessibility

File Upload is designed to meet WCAG 2.2 AA standards.

Keyboard Interactions

KeyAction
Tab / Shift+TabMoves focus to the file input, then through remove / retry buttons in the file list
Space or EnterOn the file input: opens the native file picker
Space or EnterOn a remove / retry button: activates it

Drag-and-drop is an optional shortcut; every file can also be selected with the keyboard through the native file picker.

Labels and descriptions

  • Pass aria-labelledby (e.g. the ID of the visible heading) to FileUploadTrigger to give the file input an accessible name. Any aria-describedby you pass (e.g. the ID of the "Drag files here…" instructions) is also forwarded to the input.
  • The file input is automatically described by FileUploadDescription. On a root-level error (a validation error or isInvalid set without failed uploads) the description is removed and the input gets aria-invalid="true" plus the FileUploadMessage ID in aria-describedby. Failed uploads add their own FileUploadFileMessage IDs. They keep the description only when there is no root-level error; when a root error is present, the description is hidden and FileUploadTrigger references the FileUploadMessage ID and the failed files' message IDs.
  • Each failed FileUploadFileListItem is described by its own FileUploadFileMessage.
  • Screen readers read the native file input as a button whose name includes the last selected file, for example "File Upload: photo.jpg". This comes from the browser and can't be changed by the component.
  • FileUploadFileList has no default name: give it one with aria-labelledby pointing to a visible heading.
  • Remove and retry buttons should include the file name in their accessible name (e.g. "Remove photo.jpg"), so they make sense out of context.

Focus management

  • When a file is removed via FileUploadRemoveFile, focus moves to a control in the next file item (or the previous one if it was last). If no files remain, focus returns to the file input, so it never drops to <body>.
  • FileUploadTrigger sets aria-disabled on the <label> and disabled on the input when the dropzone is disabled.

Status announcements

The component doesn't announce upload status on its own. The composition examples show the recommended pattern:

  • Render one role="status" / aria-atomic="true" live region that is always in the DOM, so it is registered before its content changes.
  • Announce every finished upload ("File uploaded successfully: photo.jpg." / "photo.jpg failed to upload."), combining files that finish together into one message.
  • For uploads that can take longer than about 3 seconds, also announce when the upload starts and its progress (see Slow Upload Announcements). Focus never moves.
  • FileUploadMessage is not a live region: validation errors are read when the input is focused. If you need them announced immediately, send them to the same live region.

Progress

  • Give each ProgressRoot an aria-label that includes the file name. For real progress, pass value (Radix sets aria-valuenow / aria-valuetext); for an indeterminate animation, describe the state in the label instead.
  • Give FileUploadFileMessage className="sr-only" when its text is redundant with visible UI, as shown in the default example.

Catalyst adds the visual styling and ARIA wiring; drag-and-drop and file selection come from react-dropzone.

Choosing the Right Component

Use File Upload when users need to select or drop one or more files with progress feedback. Reach for a different primitive when:

If you need…Use instead
A simple inline text or number inputTextField
Rich text or long-form inputTextArea
A date or calendar selectionCalendar

On this page