Skip to content

FileDropField

FileDropField is an input that accepts drag-and-drop files such as images or PDFs.
Figma logo
Breakdown of a file input pointing out its input, drop zone, plus icon, and drop helper text. See the following table for more information.
Anatomy of a FileInput
Anatomy of the file input component
ItemNameDescription
ADrop zoneA <div> that wraps the rest of the content.
B<Plus />Icon used to help convey that the user can upload a file
C<input />An <input type=“file”> styled as a <Button>
DTextA <span> used to provide additional direction
Breakdown of the file input showing its dimensions
Blueprint of a FileInput
Breakdown of the FileListItem pointing out its container, content slot, and remove button. See the following table for more information.
Anatomy of a FileListItem
Anatomy of the FileListItem component
ItemNameDescription
AContainerThe <FileListItem /> is rendered as a <li>
BContent slotSlot used to render a text span or a <Link /> to preview file and an <Error /> icon when invalid is true.
C<IconButton />An <IconButton /> is used to allow the user to remove a file from the list
D<Close />Icon used for the <IconButton />
Breakdown of the FileListItem showing its dimensions.
Blueprint of a FileListItem

Only image files at 500mb or less

    Show code
    import {
      Actions,
      FileDropField,
      FileList,
      FileListItem,
      Flex,
      Form,
      SubmitButton,
    } from '@gusto/workbench';
    import { Document, Plus } from '@gusto/workbench-icons';
    import React, { useState } from 'react';
    
    export const Basic = () => {
      // More info on the File type: https://developer.mozilla.org/en-US/docs/Web/API/File
      const [file, setFile] = useState<File | null>(null);
    
      const validateFile = (validatedFile: File) => {
        /**
         * We‘re validating the file MIME type and size. We're being flexible in order to
         * allow for the backend to transform files as needed and to improve UX. For
         * example, accepting HEIF files without forcing users to convert.
         *
         * Size validation is base 10 and not base 2 (1000 vs. 1024).
         *
         * - [File.type](https://developer.mozilla.org/en-US/docs/Web/API/File/type)
         * - [File.size](https://developer.mozilla.org/en-US/docs/Web/API/File/size)
         * - [Base 10 size insights](https://randomascii.wordpress.com/2016/02/13/base-ten-for-almost-everything/)
         */
        return (
          validatedFile.type.startsWith('image/') &&
          validatedFile.size <= 500_000_000
        );
      };
    
      const invalid = file != null && !validateFile(file);
    
      return (
        <Form
          onSubmit={e => {
            e.preventDefault();
    
            alert(JSON.stringify(file, null, 2));
          }}
        >
          <Flex flexDirection="column" rowGap={5} maxWidth={480}>
            <Flex flexDirection="column" rowGap={3}>
              <FileDropField
                invalid={invalid}
                validationText={
                  invalid
                    ? 'Only image files 500mb or less are accepted'
                    : undefined
                }
                label="Add W-2s"
                helperText="Only image files at 500mb or less"
                before={<Plus />}
                name="files"
                onChange={e => {
                  setFile(e.currentTarget.files?.item(0) ?? null);
                }}
              >
                Upload files
              </FileDropField>
              <FileList>
                {file ? (
                  <FileListItem
                    aria-label={file.name}
                    fileTypeIcon={<Document />}
                    invalid={invalid}
                    onRemove={() => setFile(null)}
                  >
                    {/*
                     * Here we‘re using `pretty-bytes` to format the file size:
                     * https://github.com/sindresorhus/pretty-bytes
                     */}
                    {file.name}
                  </FileListItem>
                ) : null}
              </FileList>
            </Flex>
    
            <Actions>
              <SubmitButton>Submit</SubmitButton>
            </Actions>
          </Flex>
        </Form>
      );
    };
    

    Only image files at 500mb or less

      Show code
      import {
        Actions,
        FileDropField,
        FileList,
        FileListItem,
        Flex,
        Form,
        Link,
        SubmitButton,
        visuallyHidden,
      } from '@gusto/workbench';
      import { Document, Plus } from '@gusto/workbench-icons';
      import prettyBytes from 'pretty-bytes';
      import React, { useState } from 'react';
      
      export const MultipleWithPreview = () => {
        // More info on the File type: https://developer.mozilla.org/en-US/docs/Web/API/File
        const [files, setFiles] = useState(new Map<File, URL>());
      
        const validateFile = (validatedFile: File) => {
          /**
           * We‘re validating the file MIME type and size. We're being flexible in order to
           * allow for the backend to transform files as needed and to improve UX. For
           * example, accepting HEIF files without forcing users to convert.
           *
           * Size validation is base 10 and not base 2 (1000 vs. 1024).
           *
           * - [File.type](https://developer.mozilla.org/en-US/docs/Web/API/File/type)
           * - [File.size](https://developer.mozilla.org/en-US/docs/Web/API/File/size)
           * - [Base 10 size insights](https://randomascii.wordpress.com/2016/02/13/base-ten-for-almost-everything/)
           */
          return (
            validatedFile.type.startsWith('image/') &&
            validatedFile.size <= 500_000_000
          );
        };
      
        const invalid = Array.from(files.keys()).some(file => !validateFile(file));
      
        const addFiles = (addedFiles: FileList | null) => {
          if (addedFiles == null) {
            return;
          }
      
          setFiles(previousFiles => {
            const newEntries = Array.from(addedFiles).map(file => {
              const previewURL = new URL(URL.createObjectURL(file));
      
              return [file, previewURL] as const;
            });
      
            // New entries are placed before previous entries
            return new Map([...newEntries, ...previousFiles]);
          });
        };
      
        const removeFile = (removedFile: File) => {
          setFiles(previousFiles => {
            const newFiles = new Map(previousFiles);
      
            const objectURL = previousFiles.get(removedFile);
            if (objectURL != null) {
              // It's best practice to revoke these when they're no longer
              // being used to avoid memory leaks. ObjectURLStore is a safe
              // abstraction around a Map<File, URL>() which is performing this
              // for us:
              //
              // https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#memory_management
              URL.revokeObjectURL(objectURL.href);
            }
      
            newFiles.delete(removedFile);
      
            return newFiles;
          });
        };
      
        return (
          <Form
            onSubmit={e => {
              e.preventDefault();
      
              alert(JSON.stringify(files, null, 2));
            }}
          >
            <Flex flexDirection="column" rowGap={5} maxWidth={480}>
              <Flex flexDirection="column" rowGap={3}>
                <FileDropField
                  invalid={invalid}
                  validationText={
                    invalid
                      ? 'Only image files 500mb or less are accepted'
                      : undefined
                  }
                  label="Add W-2s"
                  helperText="Only image files at 500mb or less"
                  before={<Plus />}
                  name="files"
                  multiple
                  onChange={e => {
                    addFiles(e.currentTarget.files);
                  }}
                >
                  Upload files
                </FileDropField>
                <FileList>
                  {Array.from(files.entries()).map(([file, objectURL]) => {
                    const fileInvalid = !validateFile(file);
      
                    return (
                      <FileListItem
                        key={objectURL.href}
                        aria-label={file.name}
                        fileTypeIcon={<Document color="kale.500" />}
                        invalid={fileInvalid}
                        onRemove={() => removeFile(file)}
                      >
                        <Link
                          target="_blank"
                          href={objectURL.href}
                          color={fileInvalid ? 'error' : undefined}
                        >
                          {/*
                           * Here we‘re using `pretty-bytes` to format the file size:
                           * https://github.com/sindresorhus/pretty-bytes
                           */}
                          {file.name} ({prettyBytes(file.size)})
                          <span className={visuallyHidden}>(opens in a new tab)</span>
                        </Link>
                      </FileListItem>
                    );
                  })}
                </FileList>
              </Flex>
      
              <Actions>
                <SubmitButton>Submit</SubmitButton>
              </Actions>
            </Flex>
          </Form>
        );
      };
      
      React props
      NameTypeDefaultDescription
      id  stringThe ID of the input element.
      invalid  booleanIf true, the field will be shown in an invalid state.
      label  RequiredstringLabel associated with the input.
      optionalText  stringText used to indicate that a field is not required.
      helperText  ReactNodeA brief description or hint for the input; can be used to link to an external resource.
      validationText  ReactNodeValidation message associated with the input.
      children  ReactNodeThe content of the component.
      before  ReactNodeContent shown before the children.
      after  ReactNodeContent shown after children.
      dropHelperText  stringor drop filesText guiding users to drop files onto the target area.
      onChange  funcCallback triggered when a user makes a new selection.
      React props
      NameTypeDefaultDescription
      readOnly  booleanIf true, this list will be immutable and the remove buttons will be hidden. This can be used in cases where the corresponding input element isdisabled.
      children  ReactNodeThe content of the FileList.
      React props
      NameTypeDefaultDescription
      aria-label  stringThe accessible name for this item. Most commonly the file name.
      invalid  booleanIf true this item will be marked as invalid.
      fileTypeIcon  ReactNodeIcon used to indicate the type of the File being uploaded.
      children  ReactNodeThe content of the FileListItem.
      onRemove  ReactNodeCallback triggered when the remove button is clicked.
      getInvalidDescription  function() => 'Invalid'Callback which can be provided to modify the accessible description of the item when it is in the invalid state.

      The following testing snippet(s) offer suggestions for testing the component using React Testing Library with occasional help from Jest.

      Setup

      Each test code block depends on a stateful FileUpload component which manages the FileDropField state.

      const FileUpload = (props: FileDropFieldProps) => {
      const [files, setFiles] = useState<File[]>([]);
      return (
      <div>
      <FileDropField
      {...props}
      onChange={e => {
      const addedFiles = Array.from(e.currentTarget.files ?? []);
      setFiles(prevFiles => {
      return props.multiple ? [...addedFiles, ...prevFiles] : addedFiles.slice(0, 1);
      });
      props.onChange?.(e);
      }}
      />
      <FileList aria-label="Files" readOnly={props.readOnly || props.disabled}>
      {files.map((file, index) => (
      <FileListItem
      key={[file.name, file.type, file.size, file.lastModified, index].join('')}
      aria-label={file.name}
      onRemove={() =>
      setFiles(prevFiles => {
      return prevFiles.filter(prevFile => prevFile !== file);
      })
      }
      >
      {file.name}
      </FileListItem>
      ))}
      </FileList>
      </div>
      );
      };

      Example

      render(
      <FileUpload multiple label="Add employee tax documents">
      Upload
      </FileUpload>,
      );
      const fileInput = screen.getByLabelText('Add employee tax documents') as HTMLInputElement;
      const fileList = screen.getByRole('list', { name: 'Files' });
      expect(fileList).toBeEmptyDOMElement();
      const files = [
      new File(['HannahArdent2022W2'], 'HannahArdent2022W2.pdf', {
      type: 'application/pdf',
      }),
      new File(['IsaiahBerlin2022W2'], 'IsaiahBerlin2022W2.pdf', {
      type: 'application/pdf',
      }),
      ];
      await act(async () => userEvent.upload(fileInput, files));
      let fileItems = within(fileList).getAllByRole('listitem');
      expect(fileItems).toHaveLength(2);
      expect(fileItems[0]).toHaveAccessibleName(files[0].name);
      expect(fileItems[1]).toHaveAccessibleName(files[1].name);
      1. The focus indicators meet the minimum requirements for focus appearance and focus visibility. For more information, see footnote 1,For more information, see footnote 2
      2. The drop zone changes to a hover style when a customer drags and holds a file over it to give them visual feedback and confirmation that that is the right place to drop the file. For more information, see footnote 3
      3. The touch-target areas for the input and options meet the minimum requirement for target size. For more information, see footnote 4
      4. Use of the accept attribute is discouraged because it isn’t obvious what the restriction is and can be too narrow. Instead allow customers to upload as flexibly as possible and have the backend determine whether or not this is ok. For example, validate that the mime type starts with image rather than image/png, image/jpeg, ... . This enables cases such as direct upload from an iOS device live photo HEIF without user confusion and frustration. If you must place these restrictions, do so after the files have been selected so that we can offer more direct feedback to the effect of “Only image files are allowed” or “Image files must be 500mb or less.” For more information, see footnote 5