File Upload
File upload component is used to upload multiple files.
The native input file element is quite difficult to style and doesn't provide a drag-n-drop version.
The file upload component doesn't handle the actual file uploading process. It only handles the UI and the state of the file upload.
Features
- Supports a button to open the file dialog
- Supports drag and drop to upload files
- Set the maximum number of files that can be uploaded
- Set the maximum size of the files that can be uploaded
- Set the accepted file types
Installation
To use the file upload machine in your project, run the following command in your command line:
npm install @zag-js/file-upload @zag-js/react # or yarn add @zag-js/file-upload @zag-js/react
npm install @zag-js/file-upload @zag-js/solid # or yarn add @zag-js/file-upload @zag-js/solid
npm install @zag-js/file-upload @zag-js/vue # or yarn add @zag-js/file-upload @zag-js/vue
npm install @zag-js/file-upload @zag-js/svelte # or yarn add @zag-js/file-upload @zag-js/svelte
Anatomy
To set up the file upload correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the file upload package into your project
import * as fileUpload from "@zag-js/file-upload"
The file upload package exports two key functions:
machine— The state machine logic for the file upload widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
Next, import the required hooks and functions for your framework and use the file upload machine in your project 🔥
import * as fileUpload from "@zag-js/file-upload" import { normalizeProps, useMachine } from "@zag-js/react" import { useId } from "react" export function FileUpload() { const service = useMachine(fileUpload.machine, { id: useId(), }) const api = fileUpload.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <div {...api.getDropzoneProps()}> <input {...api.getHiddenInputProps()} /> <span>Drag your file(s) here</span> </div> <button {...api.getTriggerProps()}>Choose file(s)</button> <ul {...api.getItemGroupProps()}> {api.acceptedFiles.map((file) => ( <li key={file.name} {...api.getItemProps({ file })}> <div {...api.getItemNameProps({ file })}>{file.name}</div> <button {...api.getItemDeleteTriggerProps({ file })}>Delete</button> </li> ))} </ul> </div> ) }
import * as fileUpload from "@zag-js/file-upload" import { normalizeProps, useMachine } from "@zag-js/solid" import { createUniqueId, createMemo, Index } from "solid-js" export function FileUpload() { const service = useMachine(fileUpload.machine, { id: createUniqueId(), }) const api = createMemo(() => fileUpload.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <div {...api().getDropzoneProps()}> <input {...api().getHiddenInputProps()} /> <span>Drag your file(s) here</span> </div> <button {...api().getTriggerProps()}>Choose file(s)</button> <ul {...api().getItemGroupProps()}> <Index each={api().acceptedFiles}> {(file) => ( <li {...api().getItemProps({ file: file() })}> <div {...api().getItemNameProps({ file: file() })}> {file().name} </div> <button {...api().getItemDeleteTriggerProps({ file: file() })}> Delete </button> </li> )} </Index> </ul> </div> ) }
<script setup> import * as fileUpload from "@zag-js/file-upload" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(fileUpload.machine, { id: "1" }) const api = computed(() => fileUpload.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <div v-bind="api.getDropzoneProps()"> <input v-bind="api.getHiddenInputProps()" /> Drag your files here </div> <button v-bind="api.getTriggerProps()">Choose Files...</button> <ul v-bind="api.getItemGroupProps()"> <li v-for="file in api.acceptedFiles" :key="file.name" v-bind="api.getItemProps({ file })" > <div v-bind="api.getItemNameProps({ file })">{{ file.name }}</div> <button v-bind="api.getItemDeleteTriggerProps({ file })">Delete</button> </li> </ul> </div> </template>
<script lang="ts"> import * as fileUpload from "@zag-js/file-upload" import { normalizeProps, useMachine } from "@zag-js/svelte" const id = $props.id() const service = useMachine(fileUpload.machine, ({ id })) const api = $derived(fileUpload.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <div {...api.getDropzoneProps()}> <input {...api.getHiddenInputProps()} /> <span>Drag your file(s) here</span> </div> <button {...api.getTriggerProps()}>Choose file(s)</button> <ul {...api.getItemGroupProps()}> {#each api.acceptedFiles as file} <li {...api.getItemProps({ file })}> <div {...api.getItemNameProps({ file })}>{file.name}</div> <button {...api.getItemDeleteTriggerProps({ file })}>Delete</button> </li> {/each} </ul> </div>
Setting the accepted file types
Use the accept attribute to set the accepted file types.
const service = useMachine(fileUpload.machine, { accept: "image/*", })
Alternatively, you can provide an object with a MIME type and an array of file extensions.
const service = useMachine(fileUpload.machine, { accept: { "image/png": [".png"], "text/html": [".html", ".htm"], }, })
Setting the maximum number of files
Use the maxFiles attribute to set the maximum number of files that can be
uploaded. This will set the multiple attribute on the underlying input
element.
const service = useMachine(fileUpload.machine, { maxFiles: 5, })
Setting the maximum size per file
Use the maxFileSize attribute to set the maximum size per file that can be
uploaded.
const service = useMachine(fileUpload.machine, { maxFileSize: 1024 * 1024 * 10, // 10MB })
Listening to file changes
When files are uploaded, the onFileChange callback is invoked with the details
of the accepted and rejected files.
const service = useMachine(fileUpload.machine, { onFileChange(details) { // details => { acceptedFiles: File[], rejectedFiles: { file: File, errors: [] }[] } console.log(details.acceptedFiles) console.log(details.rejectedFiles) }, })
Usage within a form
To use the file upload within a form, set the name attribute in the machine's
context, and ensure you render the input element api.getHiddenInputProps()
const service = useMachine(fileUpload.machine, { name: "avatar", })
Displaying image preview
To display a preview of the uploaded image, use the built-in FileReader API to
read the file and set the src attribute of an image element.
const service = useMachine(fileUpload.machine, { onFileChange(details) { const reader = new FileReader() reader.onload = (event) => { const image = event.target.result // set the image as the src of an image element } reader.readAsDataURL(details.acceptedFiles[0]) }, })
Applying custom validation
To apply custom validation, set the validate attribute to a function that
returns an array of error strings.
The returned array can contain any string as an error message. While zagjs
supports default errors such as TOO_MANY_FILES, FILE_INVALID_TYPE,
FILE_TOO_LARGE, or FILE_TOO_SMALL, you can return any string that represents
your custom validation errors.
Return
nullif no validation errors are detected.
const service = useMachine(fileUpload.machine, { validate(file) { // Check if file size exceeds 10MB if (file.size > 1024 * 1024 * 10) { return ["FILE_TOO_LARGE"] } return null }, })
Apply multiple validation errors:
const service = useMachine(fileUpload.machine, { validate(file) { const errors = [] // Check file size if (file.size > 10 * 1024 * 1024) { errors.push("FILE_TOO_LARGE") // Default error enum } // Ensure file is a PDF if (!file.name.endsWith(".pdf")) { errors.push("ONLY_PDF_ALLOWED") // Custom error } // Custom check: Reject duplicate files const isDuplicate = details.acceptedFiles.some( (acceptedFile) => acceptedFile.name === file.name, ) if (isDuplicate) { errors.push("FILE_EXISTS") } return errors.length > 0 ? errors : null }, })
Disabling drag and drop
To disable the drag and drop functionality, set the allowDrop context property
to false.
const service = useMachine(fileUpload.machine, { allowDrop: false, })
Allowing directory selection
Set the directory property to true to enable selecting directories instead
of files.
This maps to the native input webkitdirectory HTML attribute and allows users
to select directories and their contents.
Please note that support for this feature varies from browser to browser.
const service = useMachine(fileUpload.machine, { directory: true, })
Supporting media capture on mobile devices
Set the capture property to specify the media capture mechanism to capture
media on the spot. The value can be:
userfor capturing media from the user-facing cameraenvironmentfor the outward-facing camera
This behavior only works on mobile devices. On desktop devices, it will open the file system like normal.
const service = useMachine(fileUpload.machine, { capture: "user", })
Pasting files from clipboard
After a user copies an image, to allow pasting the files from the clipboard, you
can listen for the paste event and use the api.setFiles method to set the
files.
Here's an example of how to do this in React.
function Demo() { const service = useMachine(fileUpload.machine, { accept: "image/*", }) const api = fileUpload.connect(service, normalizeProps) return ( <textarea onPaste={(event) => { if (event.clipboardData?.files) { api.setFiles(Array.from(event.clipboardData.files)) } }} /> ) }
Transforming files before acceptance
Use the transformFiles callback to process files before they're added to
acceptedFiles. This is useful for scenarios like image cropping, compression,
or format conversion.
The transformFiles function receives the selected files and should return a
promise that resolves with the transformed files.
const service = useMachine(fileUpload.machine, { accept: "image/*", transformFiles: async (files) => { return Promise.all( files.map(async (file) => { // Compress or transform the file const transformedBlob = await processImage(file) return new File([transformedBlob], file.name, { type: file.type }) }), ) }, })
While files are being transformed, the api.transforming boolean is true,
allowing you to show loading states in your UI.
Styling guide
Earlier, we mentioned that each file upload part has a data-part attribute
added to them to select and style them in the DOM.
[data-part="root"] { /* styles for root element*/ } [data-part="dropzone"] { /* styles for root element*/ } [data-part="trigger"] { /* styles for file picker trigger */ } [data-part="label"] { /* styles for the input's label */ }
Dragging State
When the user drags a file over the file upload, the data-dragging attribute
is added to the root and dropzone parts.
[data-part="root"][data-dragging] { /* styles for when the user is dragging a file over the file upload */ } [data-part="dropzone"][data-dragging] { /* styles for when the user is dragging a file over the file upload */ }
Disabled State
When the file upload is disabled, the data-disabled attribute is added to the
component parts.
[data-part="root"][data-disabled] { /* styles for when the file upload is disabled */ } [data-part="dropzone"][data-disabled] { /* styles for when the file upload is disabled */ } [data-part="trigger"][data-disabled] { /* styles for when the file upload is disabled */ } [data-part="label"][data-disabled] { /* styles for when the file upload is disabled */ }
Methods and Properties
Machine Context
The file upload machine exposes the following context properties:
namestringThe name of the underlying file inputidsPartial<{ root: string; dropzone: string; hiddenInput: string; trigger: string; label: string; item: (id: string) => string; itemName: (id: string) => string; itemSizeText: (id: string) => string; itemPreview: (id: string) => string; }>The ids of the elements. Useful for composition.translationsIntlTranslationsThe localized messages to use.acceptRecord<string, string[]> | FileMimeType[]The accept file typesdisabledbooleanWhether the file input is disabledrequiredbooleanWhether the file input is requiredallowDropbooleanWhether to allow drag and drop in the dropzone elementmaxFileSizenumberThe maximum file size in bytesminFileSizenumberThe minimum file size in bytesmaxFilesnumberThe maximum number of filespreventDocumentDropbooleanWhether to prevent the drop event on the documentvalidate(file: File, details: FileValidateDetails) => FileError[]Function to validate a filedefaultAcceptedFilesFile[]The default accepted files when rendered. Use when you don't need to control the accepted files of the input.acceptedFilesFile[]The controlled accepted filesonFileChange(details: FileChangeDetails) => voidFunction called when the value changes, whether accepted or rejectedonFileAccept(details: FileAcceptDetails) => voidFunction called when the file is acceptedonFileReject(details: FileRejectDetails) => voidFunction called when the file is rejectedcapture"user" | "environment"The default camera to use when capturing mediadirectorybooleanWhether to accept directories, only works in webkit browsersinvalidbooleanWhether the file input is invalidtransformFiles(files: File[]) => Promise<File[]>Function to transform the accepted files to apply transformationslocalestringThe current locale. Based on the BCP 47 definition.dir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The file upload api exposes the following methods:
draggingbooleanWhether the user is dragging something over the root elementfocusedbooleanWhether the user is focused on the dropzone elementdisabledbooleanWhether the file input is disabledtransformingbooleanWhether files are currently being transformed via `transformFiles`openFilePickerVoidFunctionFunction to open the file dialogdeleteFile(file: File, type?: ItemType) => voidFunction to delete the file from the listacceptedFilesFile[]The accepted files that have been dropped or selectedrejectedFilesFileRejection[]The files that have been rejectedsetFiles(files: File[]) => voidSets the accepted filesclearFilesVoidFunctionClears the accepted filesclearRejectedFilesVoidFunctionClears the rejected filesgetFileSize(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) => booleanSets the clipboard files Returns `true` if the clipboard data contains files, `false` otherwise.