Components

File Upload

People add files by dropping them on the drop zone or browsing. Each file gets a row with its name, size and progress; it can be cancelled while it uploads and removed when it's done. In Arabic the rows mirror and progress fills from the right.

Preview

Mirrored is the same English content laid out right to left: it separates a mirroring bug from a translation one.

Installation

SoonInstalling from the registry is coming soon. Until then, these are the commands you will run.
npx ritla add file-upload

Also works with shadcn: npx shadcn@latest add @ritla/file-upload

Dependencies it brings: @base-ui/react, @phosphor-icons/react

Installing by hand copies the component's source into your project. The source comes with a Ritla UI license, so the CLI is the short way to the same files.

Usage

import { FileUpload, FileUploadItem, FileUploadList } from "@/components/ui/file-upload";
<FileUpload
  label="Receipts"
  accept=".pdf,.jpg"
  maxSize={10_000_000}
  helperText="PDF or JPG, up to 10 MB each"
  onFilesAdd={(accepted, rejected) => startUploads(accepted, rejected)}
>
  <FileUploadList>
    {files.map((file) => (
      <FileUploadItem
        key={file.id}
        name={file.name}
        size={file.size}
        status={file.status}
        progress={file.progress}
        onCancel={() => cancel(file.id)}
        onRemove={() => remove(file.id)}
      />
    ))}
  </FileUploadList>
</FileUpload>

Examples

Row states

While uploading the X cancels (status="uploading", progress 0 to 100, or null when unknown). When done the Trash removes and the bar is full. On failure the reason shows under the name, with Try again (onRetry). disabled dims the zone and the rows.

Get the code ›

One file, and the camera on phones

multiple={false} takes one file, and capture="environment" opens the back camera on phones, where there is no dragging. The file name stays as its owner wrote it, in any language.

Get the code ›

API reference

FileUpload

PropTypeDefaultDescription
labelReactNodeNoneThe label above the drop zone, named after the document you want ("Receipts").
requiredbooleanfalseThe asterisk after the label.
helperTextReactNodeNoneUnder the zone: the allowed types, size and count, before anyone tries.
errorReactNodeNoneAn error for the whole upload ("Add at least one receipt"), in red in place of the helper text.
acceptstringNoneAllowed types as in <input accept>: .pdf,.jpg or image/*. Every file is checked, dropped or picked.
maxSizenumberNoneLargest file, in bytes.
maxFilesnumberNoneMost files in the list (failed rows don't count).
multiplebooleantrueMore than one file at a time.
capture"environment" | "user"NoneOpens the camera on phones instead of the file picker.
disabledbooleanfalseState = Disabled.
namestringNoneThe file input's name, for a form that posts files itself.
onFilesAdd(accepted: File[], rejected: FileRejection[]) => voidNonePicked or dropped files after the checks: start uploading accepted and show each rejected file as a row with its reason (type, size or count). Nothing uploads by itself.
dropTextReactNodeNoneThe zone's line. Default: Figma's text in the page language.
browseLabelReactNodeNoneThe button's label. Default: "Browse files" (Arabic «اختيار ملفات», or «اختيار ملف» for one file).

FileUploadItem

PropTypeDefaultDescription
namestringNoneThe file's own name, never translated or transliterated.
sizenumberNoneSize in bytes, shown in the page's language and numerals.
status"uploading" | "done" | "error""uploading"Sets the row's button: the X cancels while uploading, the Trash removes after.
progressnumber | nullnullProgress 0 to 100 while uploading; null for the striped bar when it isn't known.
errorReactNodeNoneWhy it failed, in plain words ("The file is larger than 10 MB").
onCancel / onRemove() => voidNoneThe row's button. After a removal, focus moves to the next row's button, or the previous one, or Browse.
onRetry() => voidNoneShows Try again on a failed row.
disabledbooleanfalseState = Disabled.
showFileSize / showIconbooleantrueFigma's Show file size and Show icon.

Accessibility

Keyboard

Keys are written for left-to-right. In right-to-left the arrow keys swap: the one pointing toward the reading direction moves forward.

KeyAction
TabTo Browse, then to each row's button.
EnterOn Browse, opens the file dialog; on a row's button, cancels or removes.
SpaceThe same as Enter.

ARIA

  • The drop zone is only a shortcut: Browse is a real button that opens a native file input, so everything works with a keyboard, a screen reader and on phones.
  • The whole upload is a group named by its label; Browse is read with the label and described by the helper text, so a screen reader hears the types and limits before choosing.
  • Each row's button carries the file name: "Cancel upload of hotel-invoice.pdf", "Remove taxi-receipt.jpg".
  • A polite live region announces when a file is done or failed, not every percent. Each bar is role="progressbar", named by its file.

Arabic and RTL notes

Mirrors

  • The rows: the type icon at the start (the right), then the name and size, the button at the end (the left).
  • The progress bar fills from the inline start, so from the right.

Does not mirror

  • The type icon doesn't mirror, and its extension stays Latin (PDF, JPG).
  • The file name stays as written, isolated with dir="auto"; it is never translated or transliterated.
  • The cloud, Trash and X icons don't mirror.
Numerals
The size follows numerals, with its unit in the page language: "2.3 MB" in English and "2.3 ميغابايت" or "٢٫٣ ميغابايت" in Arabic.
Arabic typography
The name is body-16 Medium and the size body-12, with the taller Arabic line height; a long name is cut with an ellipsis rather than wrapping.