Skip to main content

File Upload

View as Markdown

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

Drag files here or choose from your device
Images and PDFs up to 5 files.
    No files selected yet.
<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>
typo3 ui:add fileUpload

Read more about installing Components and Primitives.

Wrap the file upload in a Field to add helper text and error text, and to mark it required.

Upload your resume
PDF only, one file maximum.
    No files selected yet.
We use this for your application review.

Picked files are listed from an itemTemplate: author the item once and FileUpload clones it for every accepted or rejected file. A rejected item shows why in its itemError part. Drop a file over 1.5 MB, a non-image or a third file to see a rejection.

Drop up to two images
Large files will appear in a rejected list below.
Accepted files
    No files selected yet.
Rejected files
    No rejected files.

Add several itemPreview parts to the item template, each with a match MIME type pattern such as image/*. The first part that matches the file shows, the others hide: an image preview and a generic fallback for everything else.

Drag files here or choose from your device
Images and PDFs up to 5 files.
    No files selected yet.

FileUpload translates three labels: dropzone (the aria-label of the dropzone), itemPreview (the alt text of a preview image) and deleteFile (the aria-label of a delete button). The last two take a %fileName% placeholder, which the browser replaces with the name of the file:

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

Write %fileName%, not {fileName}: Fluid reads a curly-brace expression inside a string as a variable and strips it. A client script passes a function of the File instead of a text. Leave out itemPreview or deleteFile to keep the built-in text. To translate per locale, read the texts from your own locallang.xlf with f:translate.

Set directory to pick a whole folder in browsers that support webkitdirectory.

Select a directory
Useful for importing grouped assets or media folders.
    No files selected yet.

The itemTemplate takes any markup. Leave out parts you can do without (this demo has no itemSizeText) and add your own, like a running "3 / 5 files selected" counter. Rejected files can have a template of their own: add a second itemTemplate with type="{f:constant(name: 'Jramke\FluidPrimitives\Enum\FileUploadItemType::Rejected')}", here one without a preview. Without it, rejected items use the accepted template.

Your own elements are ui:refs with context="fileUpload", in the root's slot or in the item template. A subclass of FileUpload fills them in render(), which runs after every change. fileValue() maps the data-value of a rendered item back to its File, which is how the demo shows each photo's modified date.

0 / 5 files selected
Drop photos here or choose from your device
Up to 5 images. File size is intentionally not shown here.
    No photos selected yet.
Rejected
    Nothing was rejected.

itemDeleteTrigger deletes right away. To ask first, pair the file upload with a Dialog: render both with autoMount="{false}" and a fixed rootId, and listen for the click on the item group in the capture phase. Call event.stopImmediatePropagation() there to keep the built-in delete from running, find the file with fileValue() and call api.deleteFile() once the dialog is confirmed.

Items are cloned again on every change, so the listener belongs on the item group.

Drop documents here or choose from your device
Removing a file asks for confirmation first.
    No documents selected yet.

FileUpload renders a native <input type="file">, so Extbase's #[FileUpload] property attribute works as it does with f:form.upload, without special controller code.

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_photos/',
    )]
    protected ObjectStorage $photos;
}

Use ui:form.root and set maxFiles on FileUpload to match the property: 1 for a single FileReference, more for an ObjectStorage<FileReference>. For a multi-file property, add the [] suffix to name yourself, as for a CheckboxGroup bound to an array property:

<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="photos">
        <ui:fileUpload.root name="photos[]" accept="image/jpeg,image/png" maxFiles="10">
            <ui:fileUpload.label>Photos</ui:fileUpload.label>
            <ui:fileUpload.dropzone>
                <ui:fileUpload.trigger>Choose photos</ui:fileUpload.trigger>
            </ui:fileUpload.dropzone>
            <ui:fileUpload.itemGroup>
                <ui:fileUpload.emptyState>No photos selected yet.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </ui:fileUpload.root>
    </ui:field.root>
</ui:form.root>

The form posts with extbase.post(), which sends multipart/form-data, so the <form> needs no enctype.

accept only filters the file picker in the browser, #[FileUpload] validates the upload itself. List the same MIME types and extensions in accept, in #[FileUpload] and in the allowed key of the TCA type=file column.

In an edit form, render the stored files as items with type="existing" inside the same itemGroup as the newly picked files. Pair each delete trigger with ui:fileUploadDeleteCheckbox, which renders the signed @delete token Extbase uses to remove the file reference on submit. Give each existing item a value, such as the uid of its file reference, so their ids stay unique.

Keep the itemTemplate next to the existing items: newly picked files are cloned from it, and picking one throws without it.

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

            <primitives:fileUpload.itemTemplate>
                <!-- the item markup for newly picked files -->
            </primitives:fileUpload.itemTemplate>

            <ui:fileUpload.itemGroup>
                <f:for each="{conference.photos}" as="fileReference">
                    <ui:fileUpload.fileItem type="existing" value="{fileReference.uid}">
                        <f:image image="{fileReference}" width="40" height="40" />
                        <ui:fileUpload.fileName
                            >{fileReference.originalResource.name}</ui:fileUpload.fileName
                        >
                        <ui:fileUploadDeleteCheckbox
                            fileReference="{fileReference}"
                            class="hidden"
                        />
                        <ui:fileUpload.fileDeleteTrigger />
                    </ui:fileUpload.fileItem>
                </f:for>
                <ui:fileUpload.emptyState>No photos selected yet.</ui:fileUpload.emptyState>
            </ui:fileUpload.itemGroup>
        </ui:fileUpload.root>
    </ui:field.root>
</ui:form.root>

A click on the delete trigger checks the checkbox next to it and hides the item right away, like removing a newly picked file. The FileReference is deleted when the form is submitted. Put the checkbox next to fileDeleteTrigger, a <button> cannot contain an <input>, and hide it with class="hidden".

ui:fileUploadDeleteCheckbox needs an objectName on the enclosing ui:form.root that matches the argument name of the controller action. It reads property from the surrounding ui:field.root unless you pass it.

existingFilesCount counts the slots the existing items use, so maxFiles and the TOO_MANY_FILES rejection include them.

The following tables cover the available props of the Fluid Primitives. For a full list of available client side props and methods, see the Zag.js Machine API.

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

NameDescriptionRequiredDefault
namestring
The name of the underlying file input. Inherited from a surrounding field when available.
No
-
disabledboolean
Whether the file upload is disabled. Inherited from a surrounding field when available.
No
-
invalidboolean
Whether the file upload is invalid. Inherited from a surrounding field when available.
No
-
requiredboolean
Whether the file upload is required. Inherited from a surrounding field when available.
No
-
readOnlyboolean
Whether the file upload is read-only. Inherited from a surrounding field when available.
No
-
acceptmixed
The accepted file types, e.g. `image/*` or `{ 'image/png': ['.png'] }`.
No
-
allowDropboolean
Whether to allow drag and drop in the dropzone element.
No
true
maxFilesinteger
The maximum number of files that can be selected.
No
1
existingFilesCountinteger
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.
No
0
maxFileSizeinteger
The maximum file size in bytes.
No
-
minFileSizeinteger
The minimum file size in bytes.
No
-
preventDocumentDropboolean
Whether to prevent a file drop outside of the dropzone from navigating the document.
No
true
captureEnum\FileUploadCapture
The camera to use to capture media on mobile devices.
No
-
directoryboolean
Whether to accept whole directories instead of files. Only works in webkit based browsers.
No
false
translationsarray
Localized file upload labels. `itemPreview` and `deleteFile` take a `%fileName%` placeholder, and are functions of the `File` in a client script. Use `f:translate` for per-template localization overrides when needed.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
rootIdstring
The root ID of the component, used for hydration and identification.
No
-
idsarray
The IDs of of the component parts for composition.
No
[]
autoMountboolean
Whether the client initializes the component on its own. Set it to false to mount it yourself with `mount()`
No
true
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-root<uid>
data-disabledPresent when disabled
data-readonlyPresent when read-only
data-draggingPresent when in the dragging state

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-label<uid>
data-disabledPresent when disabled
data-invalidPresent when invalid
data-requiredPresent when required

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-dropzone<uid>
data-invalidPresent when invalid
data-disabledPresent when disabled
data-readonlyPresent when read-only
data-draggingPresent when in the dragging state

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-trigger<uid>
data-disabledPresent when disabled
data-readonlyPresent when read-only
data-invalidPresent when invalid

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

NameDescriptionRequiredDefault
typeEnum\FileUploadItemType
Which file list this group renders, `accepted` or `rejected`.
No
Accepted
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item-group<uid>
data-disabledPresent when disabled
data-typeThe type of the item

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

NameDescriptionRequiredDefault
typeEnum\FileUploadItemType
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.
No
Accepted
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

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.

NameDescriptionRequiredDefault
typeEnum\FileUploadItemType
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().
No
Accepted

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.

NameDescriptionRequiredDefault
typestring
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.
No
'accepted'
valuestring
A unique identifier for this item, e.g. a persisted file reference's own `uid`. Not needed for `accepted`/`rejected` items populated from `itemTemplate` - the client re-stamps those with the picked file's own value on mount. Required when authoring more than one `type=" existing"` item directly, so the client can tell their `item`/`itemName`/`itemSizeText`/`itemDeleteTrigger`/`itemError` parts apart.
No
-
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item<uid>
data-disabledPresent when disabled
data-typeThe type of the item

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item-name<uid>
data-disabledPresent when disabled
data-typeThe type of the item

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item-size-text<uid>
data-disabledPresent when disabled
data-typeThe type of the item

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

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

NameDescriptionRequiredDefault
matchstring
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.
No
'.*'
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item-preview<uid>
data-disabledPresent when disabled
data-typeThe type of the item

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item-preview-image<uid>
data-disabledPresent when disabled
data-typeThe type of the item

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-item-delete-trigger<uid>
data-disabledPresent when disabled
data-readonlyPresent when read-only
data-typeThe type of the item

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

NameDescriptionRequiredDefault
attributesarray
Additional attributes that should be rendered on the component where ui:attributes is used.
No
[]
asChildboolean
If true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props.
No
-
classstring
The CSS class(es) to be applied to the component.
No
-

Rendered data attributes

AttributeDescription
data-file-upload-clear-trigger<uid>
data-disabledPresent when disabled
data-readonlyPresent when read-only
NameTypeDescription
draggingbooleanWhether the user is dragging something over the root element
focusedbooleanWhether the user is focused on the dropzone element
disabledbooleanWhether the file input is disabled
readOnlybooleanWhether the file input is in read-only mode
transformingbooleanWhether files are currently being transformed via `transformFiles`
maxFilesReachedbooleanWhether the maximum number of files has been reached
remainingFilesnumberThe number of files that can still be added
openFilePickerVoidFunctionFunction to open the file dialog
deleteFile(file: File, type?: ItemType | undefined) => voidFunction to delete the file from the list
acceptedFilesFile[]The accepted files that have been dropped or selected
rejectedFilesFileRejection[]The files that have been rejected
setFiles(files: File[]) => voidSets the accepted files
clearFilesVoidFunctionClears the accepted files
clearRejectedFilesVoidFunctionClears the rejected files
getFileSize(file: File) => stringReturns the formatted file size (e.g. 1.2MB)
createFileUrl(file: File, cb: (url: string) => void) => VoidFunctionReturns the preview url of a file. Returns a function to revoke the url.
setClipboardFiles(dt: DataTransfer | null) => booleanSets the clipboard files Returns `true` if the clipboard data contains files, `false` otherwise.
getRootState() => RootStateReturns the state of the root
getDropzoneState() => DropzoneStateReturns the state of the dropzone
getTriggerState() => TriggerStateReturns the state of the trigger
getItemGroupState(props: ItemGroupProps) => ItemGroupStateReturns the state of the item group
getItemState(props: ItemProps) => ItemStateReturns the state of the item