# File uploader

> A React file uploader with drag and drop, validation, image previews and uploads with progress, retry and cancel, or a plain picker. Every part is replaceable.

The slotsmith FileUploader is a React file uploader. Drop or browse files, check them against your limits, preview images, and upload them with progress, retry and cancellation. Leave out the `upload` function and it is a picker that never touches the network. Every part is a slot you can replace.

## Overview

The demo below uses the fallbacks only, and its uploads are simulated in the page: nothing leaves your browser. Drop up to four images or PDFs, or press <kbd>Enter</kbd> on the drop zone to browse. Tick the box to make the next uploads fail, then retry them.

```tsx
import { useState } from "react";
import { FileUploader, type UploadItem } from "slotsmith/file-uploader";
import { fakeUpload } from "../shared/fake-upload";

export default function Overview() {
  const [items, setItems] = useState<UploadItem[]>([]);
  const [fail, setFail] = useState(false);
  const done = items.filter((item) => item.status === "done").length;

  return (
    <div style={{ display: "grid", gap: "0.75rem" }}>
      <label style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
        <input type="checkbox" checked={fail} onChange={(event) => setFail(event.target.checked)} />
        Make the next uploads fail
      </label>
      {/* Up to four images or PDFs of 5 MB each, uploaded two at a time. */}
      <FileUploader
        multiple
        accept="image/*,.pdf"
        maxSize={5 * 1024 * 1024}
        maxFiles={4}
        concurrency={2}
        value={items}
        onValueChange={setItems}
        upload={(file, context) => fakeUpload(file, context, { fail })}
      />
      <p>
        {items.length === 0 ? "No files yet." : `${done} of ${items.length} ${items.length === 1 ? "file" : "files"} uploaded.`}
      </p>
    </div>
  );
}
```

Each feature has a guide with its own live demo: [the picker](https://slotsmith.dev/ar/components/file-uploader/guides/picker/) for forms that send files themselves, [uploads](https://slotsmith.dev/ar/components/file-uploader/guides/uploads/) with progress, cancel and retry, the [tile](https://slotsmith.dev/ar/components/file-uploader/guides/tile/) and [compact](https://slotsmith.dev/ar/components/file-uploader/guides/compact/) layouts, [editing a record](https://slotsmith.dev/ar/components/file-uploader/guides/editing-a-record/) that already has files, [validation](https://slotsmith.dev/ar/components/file-uploader/guides/validation/), and [virtual lists](https://slotsmith.dev/ar/components/file-uploader/guides/virtual/) for batches of hundreds.

## Install

```bash
npm i slotsmith
```

`VirtualFileUploader` from `slotsmith/virtual` also needs `@tanstack/react-virtual`.

```ts
import "slotsmith/file-uploader.css";
```

Import the stylesheet once, at your app's entry. `slotsmith/styles.css` holds every component's styles instead.

## Quick start

Render `FileUploader` and read the files from `onFilesChange`. With no `upload` prop nothing is sent. `accept` filters the file dialog and is checked again on drop, and `maxSize` is in bytes.

```tsx
import { useState } from "react";
import { FileUploader } from "slotsmith/file-uploader";
import "slotsmith/file-uploader.css";

export default function QuickStart() {
  // With no `upload` prop nothing is sent: you get the picked File objects.
  const [files, setFiles] = useState<File[]>([]);

  return (
    <div style={{ display: "grid", gap: "0.75rem" }}>
      <FileUploader multiple accept="image/*,.pdf" maxSize={5 * 1024 * 1024} onFilesChange={setFiles} />
      <p>{files.length === 0 ? "No files picked." : `Picked: ${files.map((file) => file.name).join(", ")}`}</p>
    </div>
  );
}
```

## Guides

- [The picker](https://slotsmith.dev/ar/components/file-uploader/guides/picker/): Use the slotsmith FileUploader as a file picker: leave out upload, read the File objects from onFilesChange, and send them with your form.
- [Uploads](https://slotsmith.dev/ar/components/file-uploader/guides/uploads/): Give the slotsmith FileUploader an upload function: files are queued with progress, cancel through an AbortSignal, and retry after a failure.
- [The tile layout](https://slotsmith.dev/ar/components/file-uploader/guides/tile/): The slotsmith FileUploader's tile variant: one square where the picked image replaces the drop zone, for avatars, logos and covers.
- [The compact layout](https://slotsmith.dev/ar/components/file-uploader/guides/compact/): The slotsmith FileUploader's compact variant: a Browse button and the limits on one line, with the files listed under it, for tight forms and toolbars.
- [Editing a record](https://slotsmith.dev/ar/components/file-uploader/guides/editing-a-record/): Show the files a record already has in the slotsmith FileUploader: pass their URLs as defaultValue, then save the URLs onUrlsChange gives back.
- [Validation](https://slotsmith.dev/ar/components/file-uploader/guides/validation/): Limit what the slotsmith FileUploader accepts with accept, maxSize, minSize, maxFiles and your own validate rule; refused files are explained and announced.
- [Virtual lists](https://slotsmith.dev/ar/components/file-uploader/guides/virtual/): Pass virtual to the slotsmith FileUploader for batches of hundreds of files: only the rows in view are rendered, and the rest works as before.

## Reference

The [API page](https://slotsmith.dev/ar/components/file-uploader/api/) is generated from the library's source, so it lists every slot, prop and label the installed version has. The file uploader has two label sections: the parts' text, and the [validation messages](https://slotsmith.dev/ar/components/file-uploader/api/#validation-labels) for refused files.

- [Slots](https://slotsmith.dev/ar/components/file-uploader/api/#slots): Every part you can replace, the props it receives and its fallback.
- [Props](https://slotsmith.dev/ar/components/file-uploader/api/#props): Every prop, grouped, with its type and default.
- [Labels & locales](https://slotsmith.dev/ar/components/file-uploader/api/#labels): Every string it shows or announces, and how to translate them.
- [Styling](https://slotsmith.dev/ar/components/file-uploader/api/#styling): Its class names and the tokens its stylesheet reads.
- [Adapters](https://slotsmith.dev/ar/components/file-uploader/adapters/): Ready slot maps for shadcn/ui, MUI, Chakra, Ant Design and Radix Themes.

`FileUploader` is also a set of compound parts, for a layout of your own. `FileUploader.Provider` runs the uploader and renders no markup; the others render the parts in it, and `useFileUploaderContext()` reads its state anywhere inside. The [validation guide's demo](https://slotsmith.dev/ar/components/file-uploader/guides/validation/) is built from them. The list is read from the installed library:

- `FileUploader.Provider`
- `FileUploader.Root`
- `FileUploader.Dropzone`
- `FileUploader.Compact`
- `FileUploader.List`
- `FileUploader.VirtualList`
- `FileUploader.Item` (`FileUploaderItemView`)
- `FileUploader.Actions`
- `FileUploader.Rejections`

## Adapters

The file uploader works with any design system. Each slot takes a component, so a library's own surface, buttons, avatar and progress bar can replace the fallbacks while the queue, the validation and the previews stay the same. The [Adapters page](https://slotsmith.dev/ar/components/file-uploader/adapters/) has ready slot maps for shadcn/ui, MUI, Chakra, Ant Design and Radix Themes, as examples of well-known libraries; the same approach works with your own.

## Accessibility

- **The drop zone** has `role="button"` and is in the tab order (`tabindex="0"`, or `-1` while [`disabled`](https://slotsmith.dev/ar/components/file-uploader/api/#prop-disabled), with `aria-disabled`). It is named by the [`dropzone`](https://slotsmith.dev/ar/components/file-uploader/api/#label-dropzone) label, "Add files" by default. A click, <kbd>Enter</kbd> or <kbd>Space</kbd> opens the file dialog. Dragging a file over it sets `data-dragging` and switches the title to the [`dropHere`](https://slotsmith.dev/ar/components/file-uploader/api/#label-dropHere) label.
- **The file input** is a real `<input type="file">`, visually hidden but left in the accessibility tree and named by the same label. It is out of the tab order, because the zone and the Browse button open it, but Chrome exposes it as a second "Add files" button to a screen reader's browse mode; this is a known library issue. Its `accept` and `multiple` follow your props.
- **The Browse button** is a `<button type="button">` inside the zone, so it never submits a form. A button inside a `role="button"` element is a nested interactive control, which some screen readers announce poorly; this is a known library issue.
- **Progress** is a `role="progressbar"` with `aria-valuenow` (the percentage, rounded), `aria-valuemin="0"` and `aria-valuemax="100"`, named "Uploading" and the file name by the [`progress`](https://slotsmith.dev/ar/components/file-uploader/api/#label-progress) label. See it in the [uploads guide](https://slotsmith.dev/ar/components/file-uploader/guides/uploads/).
- **Each item's actions** are buttons named by the [`remove`](https://slotsmith.dev/ar/components/file-uploader/api/#label-remove), [`retry`](https://slotsmith.dev/ar/components/file-uploader/api/#label-retry) and [`cancel`](https://slotsmith.dev/ar/components/file-uploader/api/#label-cancel) labels. The names do not include the file name, so a list of several files has several "Remove file" buttons; the button sits in the file's list item.
- **Image previews** are `<img>` elements with alt text from the [`preview`](https://slotsmith.dev/ar/components/file-uploader/api/#label-preview) label ("Preview of photo.png"). Other files show a decorative glyph, hidden from assistive technology.
- **Refused files** are listed in a `role="status"` region with `aria-live="polite"`, and stay until the Dismiss button, named by the [`dismiss`](https://slotsmith.dev/ar/components/file-uploader/api/#label-dismiss) label, clears them. The region appears with the first refusal, which some screen readers do not announce; later refusals are added to it and announced without interrupting. This is a known library issue. See the [validation guide](https://slotsmith.dev/ar/components/file-uploader/guides/validation/).
- **Status changes** of an item (Uploading, Uploaded, Failed) are shown as text in the item and are not announced on their own.

With focus on the drop zone, or in the compact layout on the Browse button:

| Keys | Action |
| --- | --- |
| Enter, Space | Open the file dialog. |
| Tab | Move to the Browse button, then to each item's Cancel, Retry and Remove buttons, then to Dismiss when files were refused. |

Every label can be translated or replaced; see [Labels & locales](https://slotsmith.dev/ar/components/file-uploader/api/#labels).

## Limitations

- One request per file. There are no chunked or resumable uploads, and a retry starts the file again from zero.
- Files only: a dropped folder is not read file by file, and there is no paste from the clipboard.
- `accept` checks the file name and the MIME type the browser reports, not the file's contents. Check files on the server too.
- Previews are object URLs for images only. There is no cropping, resizing or compression before upload.
- A drop that misses the zone opens the file in the browser tab, unless you set [`captureWindowDrops`](https://slotsmith.dev/ar/components/file-uploader/api/#prop-captureWindowDrops).
- Items restored from a URL have no size, so none is shown, and cannot be retried.

## FAQ

### How does the file uploader send files?

Through the upload function you pass: upload(file, { signal, onProgress }) returns a promise with the stored URL. Use fetch, axios or a presigned PUT. Pass signal on so Cancel stops the request, and call onProgress with a percentage. Uploads are queued, three at a time by default. See https://slotsmith.dev/ar/components/file-uploader/guides/uploads/

### Can I use the file uploader in a form without uploading?

Yes. Leave out upload and nothing is ever sent: the items stay ready and onFilesChange hands you the File objects, so the form sends them with the rest of its fields. See https://slotsmith.dev/ar/components/file-uploader/guides/picker/

### How do I show the files a record already has?

Pass their URLs as defaultValue. Each becomes a finished item, with a preview when the URL ends in an image extension, and can be removed like any other. onUrlsChange gives you the URLs to save. See https://slotsmith.dev/ar/components/file-uploader/guides/editing-a-record/

### In what unit are maxSize and minSize?

Bytes, so 5 MB is 5 * 1024 * 1024. The hint and the rejection messages show the limit formatted with 1024 per step (5 MB, 512 KB). maxFiles counts the items already in the list. See https://slotsmith.dev/ar/components/file-uploader/guides/validation/

### Does the file uploader work in Arabic and right to left?

Yes. A locale pack translates the parts' text and the rejection messages, two sections called fileUploader and fileValidation, and writes the sizes in that language's number format. The layout uses logical properties, so it mirrors inside a right-to-left ancestor. See https://slotsmith.dev/ar/components/file-uploader/api/#labels
