Skip to main content
0.77.1
View Zag.js on Github
Join the Discord server

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.

Drag your files here
Properties

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

This command will install the framework agnostic file upload logic and the reactive utilities for your framework of choice.

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-part attribute 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 [state, send] = useMachine( fileUpload.machine({ id: useId(), }), ) const api = fileUpload.connect(state, send, 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> ) }

Setting the accepted file types

Use the accept attribute to set the accepted file types.

const [state, send] = useMachine( fileUpload.machine({ accept: "image/*", }), )

Alternatively, you can provide an object with a MIME type and an array of file extensions.

const [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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.

const [state, send] = useMachine( fileUpload.machine({ validate(file) { if (file.size > 1024 * 1024 * 10) { return ["FILE_TOO_LARGE"] } return null }, }), )

Apply multiple validation errors:

const [state, send] = useMachine( fileUpload.machine({ validate(file) { const errors = [] if (file.size > 1024 * 1024 * 10) { errors.push("FILE_TOO_LARGE") // Default error enum } if (!file.name.endsWith(".pdf")) { errors.push("ONLY_PDF_ALLOWED") // Custom error } if (file.size < 1024) { errors.push("FILE_TOO_SMALL") // Default error enum } return errors.length > 0 ? errors : null }, }), )

Return null if no validation errors are detected.

Disabling drag and drop

To disable the drag and drop functionalty, set the allowDrop context property to false.

const [state, send] = 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 [state, send] = 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:

  • user for capturing media from the user-facing camera
  • environment for the outward-facing camera

This behavior only works on mobile devices. On desktop devices, it will open the file system like normal.

const [state, send] = 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 [state, send] = useMachine( fileUpload.machine({ accept: "image/*", }), ) const api = fileUpload.connect(state, send, normalizeProps) return ( <textarea onPaste={(event) => { if (event.clipboardData?.files) { api.setFiles(Array.from(event.clipboardData.files)) } }} /> ) }

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 input
  • idsPartial<{ 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 types
  • disabledbooleanWhether the file input is disabled
  • requiredbooleanWhether the file input is required
  • allowDropbooleanWhether to allow drag and drop in the dropzone element
  • maxFileSizenumberThe maximum file size in bytes
  • minFileSizenumberThe minimum file size in bytes
  • maxFilesnumberThe maximum number of files
  • validate(file: File) => FileError[]Function to validate a file
  • onFileChange(details: FileChangeDetails) => voidFunction called when the value changes, whether accepted or rejected
  • onFileAccept(details: FileAcceptDetails) => voidFunction called when the file is accepted
  • onFileReject(details: FileRejectDetails) => voidFunction called when the file is rejected
  • capture"user" | "environment"The default camera to use when capturing media
  • directorybooleanWhether to accept directories, only works in webkit browsers
  • invalidbooleanWhether the file input is invalid
  • localestringThe 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 element
  • focusedbooleanWhether the user is focused on the dropzone element
  • openFilePicker() => voidFunction to open the file dialog
  • deleteFile(file: File) => 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[]) => voidFunction to set the value
  • clearFiles() => voidFunction to clear the value
  • clearRejectedFiles() => voidFunction to clear the rejected files
  • getFileSize(file: File) => stringFunction to format the file size (e.g. 1.2MB)
  • createFileUrl(file: File, cb: (url: string) => void) => VoidFunctionFunction to get the preview url of a file. It returns a function to revoke the url.

Data Attributes

Root
data-scope
file-upload
data-part
root
data-disabled
Present when disabled
data-dragging
Present when in the dragging state
Dropzone
data-scope
file-upload
data-part
dropzone
data-invalid
Present when invalid
data-disabled
Present when disabled
data-dragging
Present when in the dragging state
Trigger
data-scope
file-upload
data-part
trigger
data-disabled
Present when disabled
data-invalid
Present when invalid
ItemGroup
data-scope
file-upload
data-part
item-group
data-disabled
Present when disabled
Item
data-scope
file-upload
data-part
item
data-disabled
Present when disabled
ItemName
data-scope
file-upload
data-part
item-name
data-disabled
Present when disabled
ItemSizeText
data-scope
file-upload
data-part
item-size-text
data-disabled
Present when disabled
ItemPreview
data-scope
file-upload
data-part
item-preview
data-disabled
Present when disabled
ItemPreviewImage
data-scope
file-upload
data-part
item-preview-image
data-disabled
Present when disabled
ItemDeleteTrigger
data-scope
file-upload
data-part
item-delete-trigger
data-disabled
Present when disabled
Label
data-scope
file-upload
data-part
label
data-disabled
Present when disabled
ClearTrigger
data-scope
file-upload
data-part
clear-trigger
data-disabled
Present when disabled

Edit this page on GitHub

Proudly made in🇳🇬by Segun Adebayo

Copyright © 2024
On this page