# File Upload

**A file picker with drag and drop support, file lists, and validation hooks.**

**Reference:** [View Source](https://github.com/jramke/fluid-primitives/tree/main/Resources/Private/Primitives/FileUpload) · [Zag.js Docs](https://zagjs.com/components/react/file-upload)

**FileUpload.html**

```html
<ui:fileUpload.root name="attachments[]" maxFiles="5" accept="image/*,.pdf">
    <ui:fileUpload.label>Attachments</ui:fileUpload.label>
    <ui:fileUpload.dropzone>
        <div class="grid gap-1">
            <div class="text-sm font-medium">Drag files here or choose from your device</div>
            <div class="text-muted-foreground text-sm">Images and PDFs up to 5 files.</div>
        </div>
        <ui:fileUpload.trigger>Choose files</ui:fileUpload.trigger>
    </ui:fileUpload.dropzone>

    <f:comment>
        Accepted/rejected files only ever exist as browser File objects, so the item markup is
        authored once as a ui:template and cloned/populated client-side per file.
    </f:comment>
    <primitives:fileUpload.itemTemplate>
        <ui:fileUpload.fileItem>
            <ui:fileUpload.itemPreview match="image/*">
                <ui:fileUpload.itemPreviewImage />
            </ui:fileUpload.itemPreview>
            <ui:fileUpload.itemPreview match=".*">
                <ui:fileUpload.filePreviewFallback />
            </ui:fileUpload.itemPreview>

            <div class="grid min-w-0 flex-1 gap-0.5">
                <ui:fileUpload.fileName />
                <ui:fileUpload.fileMeta />
                <ui:fileUpload.fileError />
            </div>

            <ui:fileUpload.fileDeleteTrigger />
        </ui:fileUpload.fileItem>
    </primitives:fileUpload.itemTemplate>

    <ui:fileUpload.itemGroup>
        <ui:fileUpload.emptyState>No files selected yet.</ui:fileUpload.emptyState>
    </ui:fileUpload.itemGroup>
</ui:fileUpload.root>
```

**FileUpload.ts**

```ts
import { mountAll } from 'fluid-primitives';
import { FileUpload } from 'fluid-primitives/file-upload';

mountAll('fileUpload', ({ props }) => {
    const fileUpload = new FileUpload(props);
    fileUpload.init();
    return fileUpload;
});

```

## Features

- Supports button-triggered file selection and drag-and-drop uploads
- Works with the `Field` primitive for form labels, descriptions, and validation
- Supports accepted file types, file count limits, size limits, and directory selection
- Exposes accepted and rejected files through the client-side API
- Supports clearing files and deleting individual accepted files
- Lets already-persisted files sit in the same list as newly-picked ones, so an edit form needs only one file list
- Renders accepted/rejected file items from a Fluid-authored `itemTemplate`, since the actual `File` objects only ever exist in the browser - optionally a completely separate one for rejected items
- Supports multiple preview variants per item, for example an image preview with a generic file-extension fallback
- Exports `fileValue()`, the identity `FileUpload` stamps onto every rendered item's `data-value`, so userland code can resolve a clicked/found item back to the real `File` it represents
- Replaces TYPO3's `f:form.upload` and integrates with Extbase's `#[FileUpload]` attribute out of the box

## Installation

```bash
typo3 ui:add file-upload
```

Or copy the files manually from GitHub (https://github.com/jramke/fluid-primitives.com/tree/main/packages/docs/Resources/Private/Registry/FileUpload) into your project.

Read more about installing [Components and Primitives](/docs/core-concepts/primitives.md).

## Examples

### With Field

Use file upload inside `Field` for form semantics and validation messaging.

**FileUpload.html**

```html
<ui:field.root name="resume" required="{true}">
    <ui:fileUpload.root accept=".pdf">
        <ui:fileUpload.label>Resume</ui:fileUpload.label>
        <ui:fileUpload.dropzone>
            <div class="grid gap-1">
                <div class="text-sm font-medium">Upload your resume</div>
                <div class="text-muted-foreground text-sm">PDF only, one file maximum.</div>
            </div>
            <ui:fileUpload.trigger>Choose PDF</ui:fileUpload.trigger>
        </ui:fileUpload.dropzone>

        <primitives:fileUpload.itemTemplate>
            <ui:fileUpload.fileItem>
                <ui:fileUpload.itemPreview match="image/*">
                    <ui:fileUpload.itemPreviewImage />
                </ui:fileUpload.itemPreview>
                <ui:fileUpload.itemPreview match=".*">
                    <ui:fileUpload.filePreviewFallback />
                </ui:fileUpload.itemPreview>

                <div class="grid min-w-0 flex-1 gap-0.5">
                    <ui:fileUpload.fileName />
                    <ui:fileUpload.fileMeta />
                    <ui:fileUpload.fileError />
                </div>

                <ui:fileUpload.fileDeleteTrigger />
            </ui:fileUpload.fileItem>
        </primitives:fileUpload.itemTemplate>

        <ui:fileUpload.itemGroup>
            <ui:fileUpload.emptyState>No files selected yet.</ui:fileUpload.emptyState>
        </ui:fileUpload.itemGroup>
    </ui:fileUpload.root>
    <ui:field.description>We use this for your application review.</ui:field.description>
    <ui:field.error />
</ui:field.root>
```

### Accepted and Rejected Files

Accepted and rejected files only ever exist as browser `File` objects, so they can never be part of the server-rendered markup. The `item` part is instead authored once inside an `itemTemplate` part inside `ui:fileUpload.root`, and the primitive clones/populates it for every accepted or rejected file - the same [`ui:template`](/docs/core-concepts/hydration.md) pattern the Combobox uses for asynchronously loaded results, just wrapped in a part so neither the stencil's internal name nor its `context` argument are something you need to type yourself. Rejected items use the `itemError` part to show why they were rejected, and can use an entirely different template (via `itemTemplate`'s `type` prop, see [Custom Item Layout](#custom-item-layout) below) when a rejected file shouldn't look like an accepted one at all.

**FileUpload.html**

```html
<ui:fileUpload.root name="portfolio[]" accept="image/*" maxFileSize="1500000" maxFiles="2">
    <ui:fileUpload.label>Portfolio Images</ui:fileUpload.label>
    <ui:fileUpload.dropzone>
        <div class="grid gap-1">
            <div class="text-sm font-medium">Drop up to two images</div>
            <div class="text-muted-foreground text-sm">Large files will appear in a rejected list below.</div>
        </div>
        <div class="flex items-center gap-2">
            <ui:fileUpload.trigger>Select images</ui:fileUpload.trigger>
            <ui:fileUpload.clearTrigger />
        </div>
    </ui:fileUpload.dropzone>

    <f:comment>
        One item template shared by both file lists below - the same markup renders an accepted or
        a rejected item, styled by its `data-type` attribute.
    </f:comment>
    <primitives:fileUpload.itemTemplate>
        <ui:fileUpload.fileItem>
            <ui:fileUpload.itemPreview match="image/*">
                <ui:fileUpload.itemPreviewImage />
            </ui:fileUpload.itemPreview>
            <ui:fileUpload.itemPreview match=".*">
                <ui:fileUpload.filePreviewFallback />
            </ui:fileUpload.itemPreview>

            <div class="grid min-w-0 flex-1 gap-0.5">
                <ui:fileUpload.fileName />
                <ui:fileUpload.fileMeta />
                <ui:fileUpload.fileError />
            </div>

            <ui:fileUpload.fileDeleteTrigger />
        </ui:fileUpload.fileItem>
    </primitives:fileUpload.itemTemplate>

    <div class="grid gap-4 md:grid-cols-2">
        <div class="grid gap-2">
            <div class="text-sm font-medium">Accepted files</div>
            <ui:fileUpload.itemGroup>
                <ui:fileUpload.emptyState>No files selected yet.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </div>
        <div class="grid gap-2">
            <div class="text-sm font-medium">Rejected files</div>
            <ui:fileUpload.itemGroup type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}">
                <ui:fileUpload.emptyState type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}">No rejected files.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </div>
    </div>
</ui:fileUpload.root>
```

### Preview Variants

Declare multiple `itemPreview` parts inside the item template, each with a `match` MIME type pattern (e.g. `image/*`). The primitive shows the first matching variant for a given file and hides the rest, which makes an image preview plus a generic fallback icon possible without a custom render callback.

**FileUpload.html**

```html
<ui:fileUpload.root name="attachments[]" maxFiles="5" accept="image/*,.pdf">
    <ui:fileUpload.label>Attachments</ui:fileUpload.label>
    <ui:fileUpload.dropzone>
        <div class="grid gap-1">
            <div class="text-sm font-medium">Drag files here or choose from your device</div>
            <div class="text-muted-foreground text-sm">Images and PDFs up to 5 files.</div>
        </div>
        <ui:fileUpload.trigger>Choose files</ui:fileUpload.trigger>
    </ui:fileUpload.dropzone>

    <f:comment>
        Accepted/rejected files only ever exist as browser File objects, so the item markup is
        authored once as a ui:template and cloned/populated client-side per file.
    </f:comment>
    <primitives:fileUpload.itemTemplate>
        <ui:fileUpload.fileItem>
            <ui:fileUpload.itemPreview match="image/*">
                <ui:fileUpload.itemPreviewImage />
            </ui:fileUpload.itemPreview>
            <ui:fileUpload.itemPreview match=".*">
                <ui:fileUpload.filePreviewFallback />
            </ui:fileUpload.itemPreview>

            <div class="grid min-w-0 flex-1 gap-0.5">
                <ui:fileUpload.fileName />
                <ui:fileUpload.fileMeta />
                <ui:fileUpload.fileError />
            </div>

            <ui:fileUpload.fileDeleteTrigger />
        </ui:fileUpload.fileItem>
    </primitives:fileUpload.itemTemplate>

    <ui:fileUpload.itemGroup>
        <ui:fileUpload.emptyState>No files selected yet.</ui:fileUpload.emptyState>
    </ui:fileUpload.itemGroup>
</ui:fileUpload.root>
```

### Translations

`FileUpload` translates three labels: `dropzone` (the dropzone's `aria-label`), `itemPreview` (an accepted item's preview `alt` text), and `deleteFile` (an item's delete button `aria-label`). The last two are functions in zag-js (`(file: File) => string`), since they interpolate the file's name - but Fluid has no callbacks to hand over, and the `File` only exists in the browser anyway. So they're translated as plain strings containing a literal `%fileName%` placeholder, which the primitive substitutes with the real file name client-side:

```html
<ui:fileUpload.root
    translations="{itemPreview: 'Vorschau von %fileName%', deleteFile: 'Datei %fileName% entfernen'}"
></ui:fileUpload.root>
```

Use `%fileName%`, not `{fileName}` - Fluid's own inline array/object syntax already treats a bare `{...}` inside a string as a nested variable expression, so a curly-brace placeholder would silently get stripped from an override written this way. Omit `itemPreview`/`deleteFile` to keep the built-in translation, or set an entry to `{false}` to omit that `aria-label`/`alt` entirely. Per-locale overrides can also live in your own `locallang.xlf` and be read with `f:translate` instead.

### Directory Upload

Enable directory selection in browsers that support `webkitdirectory`.

**FileUpload.html**

```html
<ui:fileUpload.root name="assets[]" directory="{true}" maxFiles="20">
    <ui:fileUpload.label>Asset Folder</ui:fileUpload.label>
    <ui:fileUpload.dropzone>
        <div class="grid gap-1">
            <div class="text-sm font-medium">Select a directory</div>
            <div class="text-muted-foreground text-sm">Useful for importing grouped assets or media folders.</div>
        </div>
        <ui:fileUpload.trigger>Choose folder</ui:fileUpload.trigger>
    </ui:fileUpload.dropzone>

    <primitives:fileUpload.itemTemplate>
        <ui:fileUpload.fileItem>
            <ui:fileUpload.itemPreview match="image/*">
                <ui:fileUpload.itemPreviewImage />
            </ui:fileUpload.itemPreview>
            <ui:fileUpload.itemPreview match=".*">
                <ui:fileUpload.filePreviewFallback />
            </ui:fileUpload.itemPreview>

            <div class="grid min-w-0 flex-1 gap-0.5">
                <ui:fileUpload.fileName />
                <ui:fileUpload.fileMeta />
                <ui:fileUpload.fileError />
            </div>

            <ui:fileUpload.fileDeleteTrigger />
        </ui:fileUpload.fileItem>
    </primitives:fileUpload.itemTemplate>

    <ui:fileUpload.itemGroup>
        <ui:fileUpload.emptyState>No files selected yet.</ui:fileUpload.emptyState>
    </ui:fileUpload.itemGroup>
</ui:fileUpload.root>
```

### Custom Item Layout

Nothing about `itemTemplate` requires reusing the default part layout - author whatever markup you want inside it, drop parts you don't need (here, `itemSizeText` is left out entirely), and read the client-side API directly for anything that isn't a part at all, like a running "x / 5 files selected" count.

Rejected items can use a second `itemTemplate`, given `type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}"`, instead of reusing the accepted one - handy when a rejected file should look nothing like an accepted one, e.g. no preview at all. `FileUpload` falls back to the accepted `itemTemplate` for rejected items when no dedicated rejected one is given, so this is opt-in.

The counter is plain userland JS: a `ui:ref` written directly in the root's slot content (`context="fileUpload"`, since it isn't part of the primitive's own template) is read back via `FileUpload`'s own `getElement()`, in a small subclass that updates it after every render.

Item-level data with no corresponding part works the same way, but each rendered item first has to be found again. `itemModifiedDate` is a plain `ui:ref` inside the item template - `context="fileUpload"` is required here too (and not optional the way it might look from the counter above): ambient `ui:ref` resolution only reliably works for a primitive's own template body, or for hand-authored content with no other part rendered as a sibling first - `itemModifiedDate` sits right next to `itemPreview`/`fileName`, so it needs the explicit argument. The subclass's `render()` override then re-finds every rendered `item` via `getElements('item')`, reads each one's own `itemModifiedDate` child with `getElement('itemModifiedDate', itemEl)` - passing that specific item as the scope is what lets one `getElement()` call resolve correctly per file, the same way it already does for any other item-level part - matches its `data-value` back to a `File` via the exported `fileValue()` helper (the exact identity `FileUpload` itself stamps onto that attribute), and fills in the date. The same pattern works for any other per-item data a template needs.

**FileUpload.html**

```html
<ui:fileUpload.root name="photos[]" accept="image/*" maxFiles="5" controlled="{true}" rootId="custom-layout">
    <div class="flex items-center justify-between gap-4">
        <ui:fileUpload.label>Event Photos</ui:fileUpload.label>
        <div {ui:ref(name: 'filesCounter', context: 'fileUpload')} class="text-muted-foreground text-xs">0 / 5 files selected</div>
    </div>
    <ui:fileUpload.dropzone>
        <div class="grid gap-1">
            <div class="text-sm font-medium">Drop photos here or choose from your device</div>
            <div class="text-muted-foreground text-sm">Up to 5 images. File size is intentionally not shown here.</div>
        </div>
        <ui:fileUpload.trigger>Choose photos</ui:fileUpload.trigger>
    </ui:fileUpload.dropzone>

    <f:comment>
        Accepted items get their own compact layout - a larger preview, a bold name and a text
        "Remove" trigger instead of the default icon button, and deliberately no file size.
    </f:comment>
    <f:comment>
        `itemModifiedDate` has no corresponding part anywhere in FileUpload - it's a plain ui:ref,
        populated entirely from userland JS (see CustomLayout.entry.ts), to test whether a consumer
        can show item-level data the primitive itself knows nothing about.
    </f:comment>
    <primitives:fileUpload.itemTemplate>
        <primitives:fileUpload.item class="flex items-center gap-3 rounded-lg border bg-background p-2 shadow-xs">
            <ui:fileUpload.itemPreview match="image/*" class="size-14">
                <ui:fileUpload.itemPreviewImage />
            </ui:fileUpload.itemPreview>
            <ui:fileUpload.itemPreview match=".*" class="size-14">
                <ui:fileUpload.filePreviewFallback />
            </ui:fileUpload.itemPreview>

            <div class="grid min-w-0 flex-1 gap-0.5">
                <ui:fileUpload.fileName class="truncate text-sm font-semibold" />
                <div {ui:ref(name: 'itemModifiedDate', context: 'fileUpload')} class="text-muted-foreground text-[11px]"></div>
            </div>

            <primitives:fileUpload.itemDeleteTrigger class="button button-ghost text-muted-foreground h-8 px-2.5 text-xs">Remove</primitives:fileUpload.itemDeleteTrigger>
        </primitives:fileUpload.item>
    </primitives:fileUpload.itemTemplate>

    <f:comment>
        Rejected items use a completely different template - no preview at all, just the file name
        and why it was rejected - and are listed in their own group below the accepted photos.
    </f:comment>
    <primitives:fileUpload.itemTemplate type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}">
        <primitives:fileUpload.item class="border-destructive/30 bg-destructive/5 grid gap-0.5 rounded-lg border px-3 py-2">
            <div class="flex items-center justify-between gap-3">
                <ui:fileUpload.fileName class="truncate text-sm font-medium" />
                <primitives:fileUpload.itemDeleteTrigger class="button button-ghost text-muted-foreground h-7 px-2 text-xs">Dismiss</primitives:fileUpload.itemDeleteTrigger>
            </div>
            <ui:fileUpload.fileError />
        </primitives:fileUpload.item>
    </primitives:fileUpload.itemTemplate>

    <ui:fileUpload.itemGroup>
        <ui:fileUpload.emptyState>No photos selected yet.</ui:fileUpload.emptyState>
    </ui:fileUpload.itemGroup>

    <div class="grid gap-2">
        <div class="text-sm font-medium">Rejected</div>
        <ui:fileUpload.itemGroup type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}">
            <ui:fileUpload.emptyState type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}">Nothing was rejected.</ui:fileUpload.emptyState>
        </ui:fileUpload.itemGroup>
    </div>
</ui:fileUpload.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/FileUpload/Examples/CustomLayout.entry.ts" />

```

**CustomLayout.entry.ts**

```ts
import { mount } from 'fluid-primitives';
import { FileUpload, fileValue } from 'fluid-primitives/file-upload';

function findFileByValue(files: File[], value: string): File | undefined {
    return files.find(file => fileValue(file) === value);
}

class FileUploadWithCounter extends FileUpload {
    render() {
        super.render();
        this.updateFilesCounter();
        this.updateItemModifiedDates();
    }

    private updateFilesCounter() {
        const counterEl = this.getElement<HTMLElement>('filesCounter');
        if (!counterEl) return;

        counterEl.textContent = `${this.api.acceptedFiles.length} / ${this.machine.prop('maxFiles')} files selected`;
    }

    /**
     * `itemModifiedDate` isn't a part FileUpload knows about at all - it's a plain ui:ref inside
     * the item template (see CustomLayout.html), populated entirely here. render() already reruns
     * on every accepted/rejected-files change (see Component.init()), which is also exactly when
     * new item clones need this filled in, so no separate wiring is needed beyond this loop.
     * getElement(part, itemEl) - scoped to one specific item - finds it despite there being one
     * per rendered file: Template's clone-time restamping (see ComponentHydrator.restampValue())
     * gives every ref'd element with an id a unique one per file, the same way itemName/
     * itemSizeText/itemPreview already get theirs, so no special handling is needed here either.
     */
    private updateItemModifiedDates() {
        this.getElements<HTMLElement>('item').forEach(itemEl => {
            const dateEl = this.getElement<HTMLElement>('itemModifiedDate', itemEl);
            const value = itemEl.dataset.value;
            if (!dateEl || !value) return;

            const file = findFileByValue(this.api.acceptedFiles, value);
            if (!file) return;

            dateEl.textContent = `Modified ${new Date(file.lastModified).toLocaleDateString()}`;
        });
    }
}

mount('fileUpload', 'custom-layout', ({ props }) => {
    const fileUpload = new FileUploadWithCounter(props);
    fileUpload.init();
    return fileUpload;
});

```

### Confirm File Deletion

`itemDeleteTrigger` always deletes immediately once clicked - there's no built-in confirmation step, since what "confirm" means (a dialog, an undo toast, nothing) is entirely up to the consumer. This example pairs `FileUpload` with [`Dialog`](/docs/components/dialog.md) to ask before a file is actually removed: both are mounted as independent, hydration-controlled instances (`controlled="{true}"` + a fixed `rootId`, the same pattern [Combobox's custom filter example](/docs/components/combobox.md#custom-filter-api) uses), and a capturing-phase click listener on the item group intercepts the delete trigger's own click before it reaches FileUpload's built-in handler - `event.stopImmediatePropagation()` keeps the immediate delete from ever running. The file itself is recovered from the clicked item's `data-value` via the exported `fileValue()` helper (see [Custom Item Layout](#custom-item-layout) above), and only deleted (via `api.deleteFile()`) once the dialog is confirmed.

Delegation is needed here because FileUpload re-clones its item markup on every accepted/rejected-files change, so there's no stable per-item element to attach a listener to directly.

**FileUpload.html**

```html
<ui:fileUpload.root name="documents[]" accept=".pdf,.doc,.docx" maxFiles="5" controlled="{true}" rootId="delete-confirm">
    <ui:fileUpload.label>Documents</ui:fileUpload.label>
    <ui:fileUpload.dropzone>
        <div class="grid gap-1">
            <div class="text-sm font-medium">Drop documents here or choose from your device</div>
            <div class="text-muted-foreground text-sm">Removing a file asks for confirmation first.</div>
        </div>
        <ui:fileUpload.trigger>Choose files</ui:fileUpload.trigger>
    </ui:fileUpload.dropzone>

    <primitives:fileUpload.itemTemplate>
        <ui:fileUpload.fileItem>
            <ui:fileUpload.itemPreview match=".*">
                <ui:fileUpload.filePreviewFallback />
            </ui:fileUpload.itemPreview>

            <div class="grid min-w-0 flex-1 gap-0.5">
                <ui:fileUpload.fileName />
                <ui:fileUpload.fileMeta />
            </div>

            <ui:fileUpload.fileDeleteTrigger />
        </ui:fileUpload.fileItem>
    </primitives:fileUpload.itemTemplate>

    <ui:fileUpload.itemGroup>
        <ui:fileUpload.emptyState>No documents selected yet.</ui:fileUpload.emptyState>
    </ui:fileUpload.itemGroup>
</ui:fileUpload.root>

<f:comment>
    A second, independently-controlled Dialog instance, opened programmatically from the entry file
    below instead of its own ui:dialog.trigger - clicking an item's delete button never deletes
    immediately, it opens this confirmation first.
</f:comment>
<ui:dialog.root controlled="{true}" rootId="delete-confirm" role="alertdialog">
    <ui:dialog.content>
        <ui:dialog.header>
            <ui:dialog.title>Remove file?</ui:dialog.title>
            <ui:dialog.description>
                <span {ui:ref(name: 'pendingFileName', context: 'dialog')}>This file</span> will be removed from the upload. This can't be undone.
            </ui:dialog.description>
        </ui:dialog.header>
        <ui:dialog.footer>
            <ui:dialog.close asChild="{true}">
                <ui:button variant="secondary">Cancel</ui:button>
            </ui:dialog.close>
            <button type="button" {ui:ref(name: 'confirmTrigger', context: 'dialog')} class="button button-default h-8 px-3.5 py-2.5">Remove file</button>
        </ui:dialog.footer>
    </ui:dialog.content>
</ui:dialog.root>

<vite:asset entry="EXT:docs/Resources/Private/Components/ui/FileUpload/Examples/DeleteConfirmation.entry.ts" />

```

**FileUpload.ts**

```ts
import { mountAll } from 'fluid-primitives';
import { FileUpload } from 'fluid-primitives/file-upload';

mountAll('fileUpload', ({ props }) => {
    const fileUpload = new FileUpload(props);
    fileUpload.init();
    return fileUpload;
});

```

## Using with Extbase

`FileUpload` produces a plain, native `<input type="file">` under the hood, so it works with Extbase's [`#[FileUpload]` property attribute](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ExtensionArchitecture/Extbase/Domain/FileUpload.html) without any special controller code - it replaces `f:form.upload` one-to-one.

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Conference extends AbstractEntity
{
    #[FileUpload(
        validation: [
            'required' => false,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '10K', 'maximum' => '2M'],
            'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
            'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
        ],
        uploadFolder: '1:/user_upload/conference_logos/',
    )]
    protected ?FileReference $logo = null;

    /**
     * @var ObjectStorage<FileReference>
     */
    #[FileUpload(
        validation: [
            'fileSize' => ['minimum' => '10K', 'maximum' => '10M'],
            'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
            'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
        ],
        uploadFolder: '1:/user_upload/conference_impressions/',
    )]
    protected ObjectStorage $impressions;

    // constructor, initializeObject(), getters and setters...
}
```

Use `ui:form.root`, and set `maxFiles` on `FileUpload` to match the property's cardinality - `1` for a single `FileReference`, greater than `1` for an `ObjectStorage<FileReference>`. For a multi-file property, write the `[]` suffix on `name` yourself, the same way you would for a [`CheckboxGroup`](/docs/components/checkbox-group.md) field bound to an array property - the primitive never appends it for you:

```html
<ui:form.root action="update" objectName="conference" object="{conference}">
    <ui:field.root name="logo">
        <ui:fileUpload.root name="logo" accept="image/jpeg,image/png" maxFiles="1">
            <ui:fileUpload.label>Logo</ui:fileUpload.label>
            <ui:fileUpload.dropzone>
                <ui:fileUpload.trigger>Choose logo</ui:fileUpload.trigger>
            </ui:fileUpload.dropzone>
            <ui:fileUpload.itemGroup>
                <ui:fileUpload.emptyState>No logo selected yet.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </ui:fileUpload.root>
    </ui:field.root>

    <ui:field.root name="impressions">
        <ui:fileUpload.root name="impressions[]" accept="image/jpeg,image/png" maxFiles="10">
            <ui:fileUpload.label>Impressions</ui:fileUpload.label>
            <ui:fileUpload.dropzone>
                <ui:fileUpload.trigger>Choose images</ui:fileUpload.trigger>
            </ui:fileUpload.dropzone>
            <ui:fileUpload.itemGroup>
                <ui:fileUpload.emptyState>No images selected yet.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </ui:fileUpload.root>
    </ui:field.root>
</ui:form.root>
```

No `enctype` is needed on the form: `Form` never submits natively - it always posts a `FormData` body via `fetch` (see the [Forms guide](/docs/core-concepts/forms.md)), and `fetch` always sends a `FormData` body as `multipart/form-data` regardless of any `enctype` attribute, since it isn't reading the `<form>` element at all.

<div class="relative w-full rounded-lg border px-4 py-3 text-sm grid has-[&gt;svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[&gt;svg]:gap-x-3 gap-y-0.5 items-start [&amp;&gt;svg]:size-4 [&amp;&gt;svg]:translate-y-0.5 [&amp;&gt;svg]:text-current text-warning bg-warning/5 border-warning/30 [&amp;&gt;svg]:text-warning *:[&amp;[data-scope=alert][data-part=content]]:text-warning/90 not-prose" role="alert" id="alert:«ft9UmRRmob»" data-scope="alert" data-part="root" >

<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5"> <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path> <path d="M12 9v4"></path> <path d="M12 17h.01"></path> </svg>

<div class="col-start-2 min-h-4 font-medium tracking-tight" id="alert:«ft9UmRRmob»:title" data-scope="alert" data-part="title" >

<h3>Keep validation in sync</h3>

</div>

<div class="text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&amp;_p]:leading-relaxed" id="alert:«ft9UmRRmob»:content" data-scope="alert" data-part="content" >

<p>Keep the allowed MIME types/extensions in #[FileUpload] in sync with the TCA type=file 'allowed' key on the same column, and pass the same list to FileUpload's accept prop so the browser's file picker pre-filters accordingly. accept is a client-side hint only, never a security control.</p>

</div>

</div>

### Editing: Mixing Already-Uploaded Files With New Ones

`FileUpload`'s own file list only ever holds files newly picked in the browser - it has no concept of an already-persisted `FileReference` from a previous request. For an edit form, render existing files as ordinary `item`s directly inside `ui:fileUpload.itemGroup`, alongside the newly-picked ones, instead of showing them in a second, separate list. Give each one `type="existing"` and pair its delete trigger with `ui:fileUploadDeleteCheckbox`, which renders TYPO3's HMAC-signed `@delete` token so Extbase removes the file reference on submit:

Existing items still sit inside the same `itemTemplate`-driven `itemGroup` as newly-picked files (see [Accepted and Rejected Files](#accepted-and-rejected-files) above), so don't drop the item template when adding them - without it, the `itemGroup` renders the existing files fine, but selecting a _new_ file has nothing to clone and populate, and throws:

```html
<ui:form.root action="update" objectName="conference" object="{conference}">
    <ui:field.root name="impressions[]">
        <ui:fileUpload.root
            accept="image/jpeg,image/png"
            maxFiles="10"
            existingFilesCount="{conference.impressions -> f:count()}"
        >
            <ui:fileUpload.label>Impressions</ui:fileUpload.label>
            <ui:fileUpload.dropzone>
                <ui:fileUpload.trigger>Choose images</ui:fileUpload.trigger>
            </ui:fileUpload.dropzone>

            <primitives:fileUpload.itemTemplate>
                <ui:fileUpload.fileItem>
                    <primitives:fileUpload.itemPreview
                        match="image/*"
                        class="bg-muted flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md border"
                    >
                        <primitives:fileUpload.itemPreviewImage class="size-full object-cover" />
                    </primitives:fileUpload.itemPreview>
                    <primitives:fileUpload.itemPreview
                        match=".*"
                        class="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md border"
                    >
                        <ui:fileUpload.filePreviewFallback />
                    </primitives:fileUpload.itemPreview>
                    <div class="grid min-w-0 flex-1 gap-0.5">
                        <ui:fileUpload.fileName />
                        <ui:fileUpload.fileMeta />
                        <ui:fileUpload.fileError />
                    </div>
                    <ui:fileUpload.fileDeleteTrigger />
                </ui:fileUpload.fileItem>
            </primitives:fileUpload.itemTemplate>

            <ui:fileUpload.itemGroup>
                <f:for each="{conference.impressions}" as="fileReference">
                    <ui:fileUpload.fileItem type="existing">
                        <f:image
                            image="{fileReference}"
                            width="40"
                            height="40"
                            class="size-10 shrink-0 rounded-md border object-cover"
                        />
                        <div class="grid min-w-0 flex-1 gap-0.5">
                            <ui:fileUpload.fileName
                                >{fileReference.originalResource.name}</ui:fileUpload.fileName
                            >
                        </div>
                        <ui:fileUploadDeleteCheckbox
                            fileReference="{fileReference}"
                            class="hidden"
                        />
                        <ui:fileUpload.fileDeleteTrigger />
                    </ui:fileUpload.fileItem>
                </f:for>
                <ui:fileUpload.emptyState>No images selected yet.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </ui:fileUpload.root>
    </ui:field.root>
</ui:form.root>
```

Clicking that delete trigger checks the sibling checkbox and hides the item immediately, exactly like removing a newly-picked file - the actual `FileReference` is only deleted once the form is submitted. The checkbox must be a sibling of `fileDeleteTrigger`, never nested inside it: a native `<button>` isn't allowed to contain interactive descendants like `<input>`, so `class="hidden"` (rather than nesting) is how it's kept out of view.

`ui:fileUploadDeleteCheckbox` requires the enclosing `ui:form.root` to have an `objectName` matching the controller action's argument name, and reads `property` from the surrounding `ui:field.root` when not given explicitly.

`existingFilesCount` tells the machine how many slots the existing items above already use, so `maxFiles` and the rejected-as-`TOO_MANY_FILES` behavior account for them too. It's deliberately a plain count, not the existing files themselves: whatever the machine holds as an accepted file also ends up in the hidden input's native `FileList` and gets resubmitted on the next form post - passing the existing `FileReference`s in as reconstructed `File` placeholders would silently overwrite each one with garbage content built from just its name and size.

## API Reference

### fileUpload.root

Contains every part of the file upload. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | No | `-` | The name of the underlying file input. Inherited from a surrounding field when available. |
| `disabled` | `boolean` | No | `-` | Whether the file upload is disabled. Inherited from a surrounding field when available. |
| `invalid` | `boolean` | No | `-` | Whether the file upload is invalid. Inherited from a surrounding field when available. |
| `required` | `boolean` | No | `-` | Whether the file upload is required. Inherited from a surrounding field when available. |
| `readOnly` | `boolean` | No | `-` | Whether the file upload is read-only. Inherited from a surrounding field when available. |
| `accept` | `mixed` | No | `-` | The accepted file types, e.g. `image/*` or `{ 'image/png': ['.png'] }`. |
| `allowDrop` | `boolean` | No | `true` | Whether to allow drag and drop in the dropzone element. |
| `maxFiles` | `integer` | No | `1` | The maximum number of files that can be selected. |
| `existingFilesCount` | `integer` | No | `0` | The number of already-persisted files shown outside the machine's own file list (e.g. as `type="existing"` items), so it can be subtracted from `maxFiles` and the remaining-slots count also accounts for them. Deliberately a plain count, not a list of `File` objects: whatever ends up in the machine's own accepted files also ends up in the hidden input's `FileList` and gets resubmitted on the next form post, which would silently overwrite an already-stored file with placeholder content reconstructed from just its name/size. |
| `maxFileSize` | `integer` | No | `-` | The maximum file size in bytes. |
| `minFileSize` | `integer` | No | `-` | The minimum file size in bytes. |
| `preventDocumentDrop` | `boolean` | No | `true` | Whether to prevent a file drop outside of the dropzone from navigating the document. |
| `capture` | `string` | No | `-` | The camera to use to capture media on mobile devices. Either `user` or `environment`. |
| `directory` | `boolean` | No | `false` | Whether to accept whole directories instead of files. Only works in webkit based browsers. |
| `translations` | `array` | No | `-` | Localized file upload labels. Set entries to `` to omit the corresponding `aria-label`. Use `f:translate` for per-template localization overrides when needed. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `rootId` | `string` | No | `-` | The root ID of the component, used for hydration and identification. |
| `ids` | `array` | No | `[]` | The IDs of of the component parts for composition. |
| `controlled` | `boolean` | No | `false` | If true, the component is meant to be initialized manually inside another component |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | root |
| `data-disabled` | Present when disabled |
| `data-readonly` | Present when read-only |
| `data-dragging` | Present when in the dragging state |

### fileUpload.label

The label for the file input. Renders a `<label>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | label |
| `data-disabled` | Present when disabled |
| `data-required` | Present when required |

### fileUpload.dropzone

The drag-and-drop target and click-to-open surface. Renders a `<div>` element with `role="button"`.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | dropzone |
| `data-invalid` | Present when invalid |
| `data-disabled` | Present when disabled |
| `data-readonly` | Present when read-only |
| `data-dragging` | Present when in the dragging state |

### fileUpload.hiddenInput

The native file input used for form submission. Renders a visually hidden `<input type="file">` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### fileUpload.trigger

Opens the file picker dialog. Renders a `<button>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | trigger |
| `data-disabled` | Present when disabled |
| `data-readonly` | Present when read-only |
| `data-invalid` | Present when invalid |

### fileUpload.itemGroup

Groups accepted or rejected file items, selected via the `type` prop (`Jramke\FluidPrimitives\Enum\FileUploadItemType`). Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `type` | `Enum\FileUploadItemType` | No | `Accepted` | Which file list this group renders, `accepted` or `rejected`. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item-group |
| `data-disabled` | Present when disabled |
| `data-type` | The type of the item |

### fileUpload.emptyState

Shown while an `itemGroup` has no items yet. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `type` | `Enum\FileUploadItemType` | No | `Accepted` | Which file list this empty state belongs to - must match the enclosing item group's own `type`, to keep ids unique when both groups render in the same root. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### fileUpload.itemTemplate

Wraps the item markup for accepted files, or rejected files when `type` is set to `Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected` (falls back to the accepted template when no dedicated rejected one is given). Renders a `<template>` element - never visible itself, cloned per file.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `type` | `Enum\FileUploadItemType` | No | `Accepted` | Which files this template is for. `accepted` (the default) also doubles as the fallback rejected items use when no dedicated `rejected` template is given - see FileUpload.ts's resolveItemTemplatePart(). |

### fileUpload.item

One file. Accepted/rejected items are only ever rendered from an `itemTemplate`; an already-persisted file can be authored directly with `type="existing"`. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `type` | `string` | No | `'accepted'` | The item's kind, e.g. `accepted`, `rejected`, or a custom value such as `existing` for an already-persisted file authored directly rather than populated from a `ui:template` stencil. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item |
| `data-disabled` | Present when disabled |
| `data-type` | The type of the item |

### fileUpload.itemName

Displays the file's name. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item-name |
| `data-disabled` | Present when disabled |
| `data-type` | The type of the item |

### fileUpload.itemSizeText

Displays the file's formatted size. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item-size-text |
| `data-disabled` | Present when disabled |
| `data-type` | The type of the item |

### fileUpload.itemError

Displays a rejected file's validation errors, hidden by default. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### fileUpload.itemPreview

A preview variant for an item, shown when its `match` MIME type pattern matches the file. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `match` | `string` | No | `'.*'` | A file MIME type pattern (e.g. `image/*`) this preview variant should be used for. The first `itemPreview` whose `match` matches the file wins; use `.*` as a catch-all fallback. |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item-preview |
| `data-disabled` | Present when disabled |
| `data-type` | The type of the item |

### fileUpload.itemPreviewImage

Displays an image preview, only rendered for image files. Renders an `<img>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item-preview-image |
| `data-disabled` | Present when disabled |
| `data-type` | The type of the item |

### fileUpload.itemPreviewFallback

Displays a generic fallback (e.g. the file extension) inside a non-image `itemPreview` variant. Renders a `<div>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

### fileUpload.itemDeleteTrigger

Removes a single accepted or rejected file. Renders a `<button>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | item-delete-trigger |
| `data-disabled` | Present when disabled |
| `data-readonly` | Present when read-only |
| `data-type` | The type of the item |

### fileUpload.clearTrigger

Clears every accepted file. Renders a `<button>` element.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | No | `-` | If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. |
| `class` | `string` | No | `-` | The CSS class(es) to be applied to the component. |
| `attributes` | `array` | No | `[]` | Additional attributes that should be rendered on the component where ui:attributes is used. |

#### Rendered data attributes

| Attribute | Description |
| --- | --- |
| `data-scope` | file-upload |
| `data-part` | clear-trigger |
| `data-disabled` | Present when disabled |
| `data-readonly` | Present when read-only |

### Machine JavaScript API

| Name | Type | Description |
| --- | --- | --- |
| `dragging` | `boolean` | Whether the user is dragging something over the root element |
| `focused` | `boolean` | Whether the user is focused on the dropzone element |
| `disabled` | `boolean` | Whether the file input is disabled |
| `readOnly` | `boolean` | Whether the file input is in read-only mode |
| `transforming` | `boolean` | Whether files are currently being transformed via `transformFiles` |
| `maxFilesReached` | `boolean` | Whether the maximum number of files has been reached |
| `remainingFiles` | `number` | The number of files that can still be added |
| `openFilePicker` | `VoidFunction` | Function to open the file dialog |
| `deleteFile` | `(file: File, type?: ItemType \| undefined) => void` | Function to delete the file from the list |
| `acceptedFiles` | `File[]` | The accepted files that have been dropped or selected |
| `rejectedFiles` | `FileRejection[]` | The files that have been rejected |
| `setFiles` | `(files: File[]) => void` | Sets the accepted files |
| `clearFiles` | `VoidFunction` | Clears the accepted files |
| `clearRejectedFiles` | `VoidFunction` | Clears the rejected files |
| `getFileSize` | `(file: File) => string` | Returns the formatted file size (e.g. 1.2MB) |
| `createFileUrl` | `(file: File, cb: (url: string) => void) => VoidFunction` | Returns the preview url of a file. Returns a function to revoke the url. |
| `setClipboardFiles` | `(dt: DataTransfer \| null) => boolean` | Sets the clipboard files Returns `true` if the clipboard data contains files, `false` otherwise. |

## Anatomy

```html
<primitives:fileUpload.root>
    <primitives:fileUpload.label />
    <primitives:fileUpload.dropzone>
        <primitives:fileUpload.trigger />
    </primitives:fileUpload.dropzone>
    <primitives:fileUpload.hiddenInput />

    <primitives:fileUpload.itemTemplate>
        <primitives:fileUpload.item>
            <primitives:fileUpload.itemPreview match="image/*">
                <primitives:fileUpload.itemPreviewImage />
            </primitives:fileUpload.itemPreview>
            <primitives:fileUpload.itemPreview match=".*">
                <primitives:fileUpload.itemPreviewFallback />
            </primitives:fileUpload.itemPreview>
            <primitives:fileUpload.itemName />
            <primitives:fileUpload.itemSizeText />
            <primitives:fileUpload.itemError />
            <primitives:fileUpload.itemDeleteTrigger />
        </primitives:fileUpload.item>
    </primitives:fileUpload.itemTemplate>

    <f:comment>Optional - falls back to the itemTemplate above when omitted.</f:comment>
    <primitives:fileUpload.itemTemplate
        type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}"
    >
        <primitives:fileUpload.item>
            <primitives:fileUpload.itemName />
            <primitives:fileUpload.itemError />
            <primitives:fileUpload.itemDeleteTrigger />
        </primitives:fileUpload.item>
    </primitives:fileUpload.itemTemplate>

    <primitives:fileUpload.itemGroup>
        <primitives:fileUpload.emptyState />
    </primitives:fileUpload.itemGroup>
    <primitives:fileUpload.clearTrigger />
</primitives:fileUpload.root>
```
