{"slug":"file-upload","title":"File Upload","description":"Using the file-upload machine in your project.","contentType":"component","framework":"react","content":"A file upload component lets users select and manage files.\n\nThe native input file element is quite difficult to style and doesn't provide a\ndrag-n-drop version.\n\n> The file upload component doesn't handle the actual file uploading process. It\n> only handles the UI and the state of the file upload.\n\n## Resources\n\n\n[Latest version: v1.35.3](https://www.npmjs.com/package/@zag-js/file-upload)\n[Logic Visualizer](https://zag-visualizer.vercel.app/file-upload)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/file-upload)\n\n\n\n**Features**\n\n- Supports a button to open the file dialog\n- Supports drag and drop to upload files\n- Set the maximum number of files that can be uploaded\n- Set the maximum size of the files that can be uploaded\n- Set the accepted file types\n\n## Installation\n\nInstall the file upload package:\n\n```bash\nnpm install @zag-js/file-upload @zag-js/react\n# or\nyarn add @zag-js/file-upload @zag-js/react\n```\n\n## Anatomy\n\nTo set up the file upload correctly, you'll need to understand its anatomy and\nhow we name its parts.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nImport the file upload package:\n\n```jsx\nimport * as fileUpload from \"@zag-js/file-upload\"\n```\n\nThe file upload package exports two key functions:\n\n- `machine` - Behavior logic.\n- `connect` - Maps behavior to JSX props and event handlers.\n\nThen use the framework integration helpers:\n\n```jsx\nimport * as fileUpload from \"@zag-js/file-upload\"\nimport { normalizeProps, useMachine } from \"@zag-js/react\"\nimport { useId } from \"react\"\n\nexport function FileUpload() {\n  const service = useMachine(fileUpload.machine, {\n    id: useId(),\n  })\n\n  const api = fileUpload.connect(service, normalizeProps)\n\n  return (\n    <div {...api.getRootProps()}>\n      <div {...api.getDropzoneProps()}>\n        <input {...api.getHiddenInputProps()} />\n        <span>Drag your file(s) here</span>\n      </div>\n\n      <button {...api.getTriggerProps()}>Choose file(s)</button>\n\n      <ul {...api.getItemGroupProps()}>\n        {api.acceptedFiles.map((file) => (\n          <li key={file.name} {...api.getItemProps({ file })}>\n            <div {...api.getItemNameProps({ file })}>{file.name}</div>\n            <button {...api.getItemDeleteTriggerProps({ file })}>Delete</button>\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n```\n\n### Setting the accepted file types\n\nUse the `accept` attribute to set the accepted file types.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  accept: \"image/*\",\n})\n```\n\nAlternatively, you can provide an object with a MIME type and an array of file\nextensions.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  accept: {\n    \"image/png\": [\".png\"],\n    \"text/html\": [\".html\", \".htm\"],\n  },\n})\n```\n\n### Setting the maximum number of files\n\nUse `maxFiles` to set how many files can be selected.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  maxFiles: 5,\n})\n```\n\n### Setting the minimum size per file\n\nUse `minFileSize` to reject files smaller than a minimum size.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  minFileSize: 1024, // 1KB\n})\n```\n\n### Setting the maximum size per file\n\nUse the `maxFileSize` attribute to set the maximum size per file that can be\nuploaded.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  maxFileSize: 1024 * 1024 * 10, // 10MB\n})\n```\n\n### Setting initial files\n\nUse `defaultAcceptedFiles` to prefill the accepted files list.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  defaultAcceptedFiles: [\n    new File([\"hello\"], \"hello.txt\", { type: \"text/plain\" }),\n  ],\n})\n```\n\n### Controlled files\n\nUse `acceptedFiles` and `onFileChange` to control accepted files externally.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  acceptedFiles,\n  onFileChange(details) {\n    setAcceptedFiles(details.acceptedFiles)\n  },\n})\n```\n\n### Listening to file changes\n\nWhen files are uploaded, the `onFileChange` callback is invoked with the details\nof the accepted and rejected files.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  onFileChange(details) {\n    // details => { acceptedFiles: File[], rejectedFiles: { file: File, errors: FileError[] }[] }\n    console.log(details.acceptedFiles)\n    console.log(details.rejectedFiles)\n  },\n})\n```\n\n### Listening to accept and reject events\n\nUse `onFileAccept` and `onFileReject` for separated success/failure flows.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  onFileAccept(details) {\n    console.log(\"Accepted:\", details.files)\n  },\n  onFileReject(details) {\n    console.log(\"Rejected:\", details.files)\n  },\n})\n```\n\n### Usage within a form\n\nTo use file upload in a form, set `name` and render `api.getHiddenInputProps()`.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  name: \"avatar\",\n})\n```\n\n```jsx\n<input {...api.getHiddenInputProps()} />\n```\n\n### Displaying image preview\n\nTo display a preview of the uploaded image, use the built-in FileReader API to\nread the file and set the `src` attribute of an image element.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  onFileChange(details) {\n    const reader = new FileReader()\n    reader.onload = (event) => {\n      const image = event.target.result\n      // set the image as the src of an image element\n    }\n    reader.readAsDataURL(details.acceptedFiles[0])\n  },\n})\n```\n\n### Applying custom validation\n\nTo apply custom validation, set `validate` to a function that returns an **array\nof error strings**.\n\nThe returned array can contain any string as an error message. While zagjs\nsupports default errors such as `TOO_MANY_FILES`, `FILE_INVALID_TYPE`,\n`FILE_TOO_LARGE`, or `FILE_TOO_SMALL`, you can return any string that represents\nyour custom validation errors.\n\n> Return `null` if no validation errors are detected.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  validate(file, details) {\n    // Check if file size exceeds 10MB\n    if (file.size > 1024 * 1024 * 10) {\n      return [\"FILE_TOO_LARGE\"]\n    }\n    // details => { acceptedFiles, rejectedFiles }\n    console.log(details.acceptedFiles.length)\n    return null\n  },\n})\n```\n\nApply multiple validation errors:\n\n```js\nconst service = useMachine(fileUpload.machine, {\n  validate(file, details) {\n    const errors = []\n\n    // Check file size\n    if (file.size > 10 * 1024 * 1024) {\n      errors.push(\"FILE_TOO_LARGE\") // Default error enum\n    }\n\n    // Ensure file is a PDF\n    if (!file.name.endsWith(\".pdf\")) {\n      errors.push(\"ONLY_PDF_ALLOWED\") // Custom error\n    }\n\n    // Custom check: Reject duplicate files\n    const isDuplicate = details.acceptedFiles.some(\n      (acceptedFile) => acceptedFile.name === file.name,\n    )\n    if (isDuplicate) {\n      errors.push(\"FILE_EXISTS\")\n    }\n\n    return errors.length > 0 ? errors : null\n  },\n})\n```\n\n### Disabling drag and drop\n\nSet `allowDrop` to `false` to disable drag and drop.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  allowDrop: false,\n})\n```\n\n### Preventing document-level drop\n\nSet `preventDocumentDrop` to `false` if you do not want this component to block\nfile drops outside the dropzone.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  preventDocumentDrop: false,\n})\n```\n\n### Read-only mode\n\nSet `readOnly` to prevent adding or removing files.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  readOnly: true,\n})\n```\n\n### Allowing directory selection\n\nSet the `directory` property to `true` to enable selecting directories instead\nof files.\n\nThis maps to the native input `webkitdirectory` HTML attribute and allows users\nto select directories and their contents.\n\n> Please note that support for this feature varies from browser to browser.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  directory: true,\n})\n```\n\n### Supporting media capture on mobile devices\n\nSet the `capture` property to specify the media capture mechanism to capture\nmedia on the spot. The value can be:\n\n- `user` for capturing media from the user-facing camera\n- `environment` for the outward-facing camera\n\n> This behavior only works on mobile devices. On desktop devices, it will open\n> the file system like normal.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  capture: \"user\",\n})\n```\n\n### Pasting files from clipboard\n\nAfter a user copies an image, to allow pasting the files from the clipboard, you\ncan listen for the paste event and use the `api.setFiles` method to set the\nfiles.\n\nHere's an example of how to do this in React.\n\n```jsx\nfunction Demo() {\n  const service = useMachine(fileUpload.machine, {\n    accept: \"image/*\",\n  })\n\n  const api = fileUpload.connect(service, normalizeProps)\n\n  return (\n    <textarea\n      onPaste={(event) => {\n        if (event.clipboardData?.files) {\n          api.setFiles(Array.from(event.clipboardData.files))\n        }\n      }}\n    />\n  )\n}\n```\n\n### Transforming files before acceptance\n\nUse the `transformFiles` callback to process files before they're added to\n`acceptedFiles`. This is useful for scenarios like image cropping, compression,\nor format conversion.\n\nThe `transformFiles` function receives the selected files and should return a\npromise that resolves with the transformed files.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  accept: \"image/*\",\n  transformFiles: async (files) => {\n    return Promise.all(\n      files.map(async (file) => {\n        // Compress or transform the file\n        const transformedBlob = await processImage(file)\n        return new File([transformedBlob], file.name, { type: file.type })\n      }),\n    )\n  },\n})\n```\n\nWhile files are being transformed, the `api.transforming` boolean is `true`,\nallowing you to show loading states in your UI.\n\n### Customizing accessibility labels\n\nUse `translations` to customize dropzone and item action labels.\n\n```jsx\nconst service = useMachine(fileUpload.machine, {\n  translations: {\n    dropzone: \"Drop files here\",\n    deleteFile: (file) => `Remove ${file.name}`,\n  },\n})\n```\n\n## Styling guide\n\nEach part includes a `data-part` attribute you can target in CSS.\n\n```css\n[data-part=\"root\"] {\n  /* styles for root element*/\n}\n\n[data-part=\"dropzone\"] {\n  /* styles for root element*/\n}\n\n[data-part=\"trigger\"] {\n  /* styles for file picker trigger */\n}\n\n[data-part=\"label\"] {\n  /* styles for the input's label */\n}\n```\n\n### Dragging State\n\nWhen the user drags a file over the file upload, the `data-dragging` attribute\nis added to the `root` and `dropzone` parts.\n\n```css\n[data-part=\"root\"][data-dragging] {\n  /* styles for when the user is dragging a file over the file upload */\n}\n\n[data-part=\"dropzone\"][data-dragging] {\n  /* styles for when the user is dragging a file over the file upload */\n}\n```\n\n### Disabled State\n\nWhen the file upload is disabled, the `data-disabled` attribute is added to the\ncomponent parts.\n\n```css\n[data-part=\"root\"][data-disabled] {\n  /* styles for when the file upload is disabled */\n}\n\n[data-part=\"dropzone\"][data-disabled] {\n  /* styles for when the file upload is disabled */\n}\n\n[data-part=\"trigger\"][data-disabled] {\n  /* styles for when the file upload is disabled */\n}\n\n[data-part=\"label\"][data-disabled] {\n  /* styles for when the file upload is disabled */\n}\n```\n\n## Methods and Properties\n\n### Machine Context\n\nThe file upload machine exposes the following context properties:\n\n**`name`**\nType: `string`\nDescription: The name of the underlying file input\n\n**`ids`**\nType: `Partial<{ 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; itemDeleteTrigger: (id: string) => string; }>`\nDescription: The ids of the elements. Useful for composition.\n\n**`translations`**\nType: `IntlTranslations`\nDescription: The localized messages to use.\n\n**`accept`**\nType: `Record<string, string[]> | FileMimeType[]`\nDescription: The accept file types\n\n**`disabled`**\nType: `boolean`\nDescription: Whether the file input is disabled\n\n**`required`**\nType: `boolean`\nDescription: Whether the file input is required\n\n**`allowDrop`**\nType: `boolean`\nDescription: Whether to allow drag and drop in the dropzone element\n\n**`maxFileSize`**\nType: `number`\nDescription: The maximum file size in bytes\n\n**`minFileSize`**\nType: `number`\nDescription: The minimum file size in bytes\n\n**`maxFiles`**\nType: `number`\nDescription: The maximum number of files\n\n**`preventDocumentDrop`**\nType: `boolean`\nDescription: Whether to prevent the drop event on the document\n\n**`validate`**\nType: `(file: File, details: FileValidateDetails) => FileError[]`\nDescription: Function to validate a file\n\n**`defaultAcceptedFiles`**\nType: `File[]`\nDescription: The default accepted files when rendered.\nUse when you don't need to control the accepted files of the input.\n\n**`acceptedFiles`**\nType: `File[]`\nDescription: The controlled accepted files\n\n**`onFileChange`**\nType: `(details: FileChangeDetails) => void`\nDescription: Function called when the value changes, whether accepted or rejected\n\n**`onFileAccept`**\nType: `(details: FileAcceptDetails) => void`\nDescription: Function called when the file is accepted\n\n**`onFileReject`**\nType: `(details: FileRejectDetails) => void`\nDescription: Function called when the file is rejected\n\n**`capture`**\nType: `\"user\" | \"environment\"`\nDescription: The default camera to use when capturing media\n\n**`directory`**\nType: `boolean`\nDescription: Whether to accept directories, only works in webkit browsers\n\n**`invalid`**\nType: `boolean`\nDescription: Whether the file input is invalid\n\n**`readOnly`**\nType: `boolean`\nDescription: Whether the file input is read-only\n\n**`transformFiles`**\nType: `(files: File[]) => Promise<File[]>`\nDescription: Function to transform the accepted files to apply transformations\n\n**`locale`**\nType: `string`\nDescription: The current locale. Based on the BCP 47 definition.\n\n**`dir`**\nType: `\"ltr\" | \"rtl\"`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `() => ShadowRoot | Node | Document`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n### Machine API\n\nThe file upload `api` exposes the following methods:\n\n**`dragging`**\nType: `boolean`\nDescription: Whether the user is dragging something over the root element\n\n**`focused`**\nType: `boolean`\nDescription: Whether the user is focused on the dropzone element\n\n**`disabled`**\nType: `boolean`\nDescription: Whether the file input is disabled\n\n**`readOnly`**\nType: `boolean`\nDescription: Whether the file input is in read-only mode\n\n**`transforming`**\nType: `boolean`\nDescription: Whether files are currently being transformed via `transformFiles`\n\n**`maxFilesReached`**\nType: `boolean`\nDescription: Whether the maximum number of files has been reached\n\n**`remainingFiles`**\nType: `number`\nDescription: The number of files that can still be added\n\n**`openFilePicker`**\nType: `VoidFunction`\nDescription: Function to open the file dialog\n\n**`deleteFile`**\nType: `(file: File, type?: ItemType) => void`\nDescription: Function to delete the file from the list\n\n**`acceptedFiles`**\nType: `File[]`\nDescription: The accepted files that have been dropped or selected\n\n**`rejectedFiles`**\nType: `FileRejection[]`\nDescription: The files that have been rejected\n\n**`setFiles`**\nType: `(files: File[]) => void`\nDescription: Sets the accepted files\n\n**`clearFiles`**\nType: `VoidFunction`\nDescription: Clears the accepted files\n\n**`clearRejectedFiles`**\nType: `VoidFunction`\nDescription: Clears the rejected files\n\n**`getFileSize`**\nType: `(file: File) => string`\nDescription: Returns the formatted file size (e.g. 1.2MB)\n\n**`createFileUrl`**\nType: `(file: File, cb: (url: string) => void) => VoidFunction`\nDescription: Returns the preview url of a file.\nReturns a function to revoke the url.\n\n**`setClipboardFiles`**\nType: `(dt: DataTransfer) => boolean`\nDescription: Sets the clipboard files\nReturns `true` if the clipboard data contains files, `false` otherwise.\n\n### Data Attributes\n\n**`Root`**\n\n**`data-scope`**: file-upload\n**`data-part`**: root\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-dragging`**: Present when in the dragging state\n\n**`Dropzone`**\n\n**`data-scope`**: file-upload\n**`data-part`**: dropzone\n**`data-invalid`**: Present when invalid\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-dragging`**: Present when in the dragging state\n\n**`Trigger`**\n\n**`data-scope`**: file-upload\n**`data-part`**: trigger\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-invalid`**: Present when invalid\n\n**`ItemGroup`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item-group\n**`data-disabled`**: Present when disabled\n**`data-type`**: The type of the item\n\n**`Item`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item\n**`data-disabled`**: Present when disabled\n**`data-type`**: The type of the item\n\n**`ItemName`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item-name\n**`data-disabled`**: Present when disabled\n**`data-type`**: The type of the item\n\n**`ItemSizeText`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item-size-text\n**`data-disabled`**: Present when disabled\n**`data-type`**: The type of the item\n\n**`ItemPreview`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item-preview\n**`data-disabled`**: Present when disabled\n**`data-type`**: The type of the item\n\n**`ItemPreviewImage`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item-preview-image\n**`data-disabled`**: Present when disabled\n**`data-type`**: The type of the item\n\n**`ItemDeleteTrigger`**\n\n**`data-scope`**: file-upload\n**`data-part`**: item-delete-trigger\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only\n**`data-type`**: The type of the item\n\n**`Label`**\n\n**`data-scope`**: file-upload\n**`data-part`**: label\n**`data-disabled`**: Present when disabled\n**`data-required`**: Present when required\n\n**`ClearTrigger`**\n\n**`data-scope`**: file-upload\n**`data-part`**: clear-trigger\n**`data-disabled`**: Present when disabled\n**`data-readonly`**: Present when read-only","package":"@zag-js/file-upload","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/file-upload.mdx"}