DropZone

An area that takes dropped files and opens the file picker when pressed.

StableSince 1.0.0
On this page

Import

import { DropZone } from '@envsemble/envui';

Usage

A DropZone takes dropped files and opens the file picker when pressed.

  • label says what to add and description which files fit; together they name the zone.
  • onFilesAccepted receives the files it takes.
  • icon replaces the upload icon.
LogoUpload.tsx
import { useEffect, useRef, useState } from 'react';
import { Button, DropZone, Column, Graphic, Row, Text } from '@envsemble/envui';

const twoMegabytes = 2 * 1024 * 1024;

export default function LogoUpload() {
  const picker = useRef<HTMLInputElement>(null);
  const [uploading, setUploading] = useState<File | null>(null);
  const [logo, setLogo] = useState<File | null>(null);
  const [preview, setPreview] = useState<string>();
  const [error, setError] = useState<string>();

  // A URL for the chosen image, released when it changes.
  useEffect(() => {
    if (!logo) return setPreview(undefined);
    const url = URL.createObjectURL(logo);
    setPreview(url);
    return () => URL.revokeObjectURL(url);
  }, [logo]);

  // Uploading takes a moment; a timer stands in for it here.
  useEffect(() => {
    if (!uploading) return;
    const timer = window.setTimeout(() => {
      setLogo(uploading);
      setUploading(null);
    }, 1500);
    return () => window.clearTimeout(timer);
  }, [uploading]);

  return (
    // As wide as a form column.
    <Column gap={3} style={{ inlineSize: 'min(100%, 24rem)' }}>
      <DropZone
        hidden={logo != null}
        inputRef={picker}
        label={uploading ? `Uploading ${uploading.name}` : 'Drop a logo here, or browse'}
        description={uploading ? 'This takes a moment' : 'PNG, JPG or WebP up to 2 MB'}
        accept="image/png,image/jpeg,.webp"
        pending={uploading != null}
        error={error}
        onFilesAccepted={([file]) => {
          // The zone checks the type; the size is the application's to check.
          if (file.size > twoMegabytes) return setError('Choose an image up to 2 MB.');
          setError(undefined);
          setUploading(file);
        }}
        onFilesRejected={() => setError('Choose a PNG, JPG or WebP image.')}
      />
      {logo && (
        <Row wrap align="center" gap={4}>
          <Graphic src={preview} alt="" size="lg" />
          <Text render={<span />}>{logo.name}</Text>
          {/* Replace opens the zone's file picker while the zone is hidden. */}
          <Button variant="secondary" onClick={() => picker.current?.click()}>
            Replace
          </Button>
          <Button variant="text" onClick={() => setLogo(null)}>
            Remove
          </Button>
        </Row>
      )}
    </Column>
  );
}

Accept, several files and errors

accept filters dropped and chosen files, as the native attribute does, with MIME types such as image/png, groups such as image/* and extensions such as .webp; any file by default. Without multiple the zone takes the first file. Files that don't fit, and those after the first without multiple, go to onFilesRejected. Check size and contents yourself, and show the message in error, which takes the error color on the zone's outline. disabled turns the zone off; it still keeps the browser from opening dropped files.

<DropZone
  multiple
  accept="application/pdf,.pdf"
  label="Drop documents here, or browse"
  description="PDF up to 10 MB each"
  error={error}
  onFilesAccepted={(files) => setDocuments((current) => [...current, ...files])}
  onFilesRejected={() => setError('Only PDF files can be attached.')}
/>

Pending work

While pending, a spinner replaces the icon, the zone reports aria-busy and takes no new files. Change the label to say what is happening, and announce when the upload finishes, such as in a role="status" text.

<DropZone
  pending={uploading != null}
  label={uploading ? `Uploading ${uploading}` : 'Drop a report here, or browse'}
  description={uploading ? 'This takes a moment' : 'PDF up to 10 MB'}
  accept="application/pdf,.pdf"
  onFilesAccepted={upload}
/>

Sizes

size is md, the tall area with a large icon and the default, or sm, one row with a small icon, for attachments in a dense form or a row.

In a form

With name, a native form submits the last files the zone took, dropped ones too, and a reset clears them; new FormData(form).getAll('attachments') reads them, with an empty entry when there are none. files hands the zone the files the application keeps, such as after a Remove, and replaces the ones it took. form names a form the zone sits outside of, and inputRef reaches the native file input, to open the picker from a Replace button. ref reaches the zone itself: unlike a field's input, the file input holds no value a form library could register.

<form onSubmit={send}>
  <TextField label="Subject" name="subject" />
  <DropZone
    size="sm"
    multiple
    name="attachments"
    label="Attach files"
    description="Sent with the message"
    onFilesAccepted={setAttachments}
  />
  <Button type="submit">Send</Button>
</form>

Composition

The zone's frame is Field's root and error. Its parts are the zone, env-drop-zone, the target people press, env-drop-zone-target, with its icon, env-drop-zone-icon, its label, env-drop-zone-label, and its description, env-drop-zone-description; then the error, env-field-error. List the chosen files beside it, such as in a List with Replace and Remove, and keep links and other controls out of the zone.

Accessibility

You provide

  • A label that says what to add.
  • Checks of size and contents, and the error message.
  • An announcement when an upload finishes, and the list of chosen files.

envUI provides

  • The zone as a button named by its label and described by its description and error.
  • aria-busy while pending.
  • A native file input for the picker and for forms.

Props

DropZone

A drop target for files that also opens the file picker; the application shows what was chosen.

PropTypeDescription
labelrequired
ReactNode
What to drop or choose, such as "Drop a logo here, or browse". With the description, it names the zone, as its visible text does.
description
ReactNode
Which files fit, such as "PNG, JPG or WebP up to 2 MB"; it is read as part of the name.
icon
ReactNode
Replaces the upload icon.
acceptDefault any file
string
The file types the zone takes, as in the native accept attribute: MIME types such as image/png, groups such as image/*, and extensions such as .webp.
multipleDefault false
boolean
Take several files at once; otherwise the zone takes the first file.
onFilesAcceptedrequired
(files: File[]) => void
Receives the dropped or chosen files that the zone takes.
onFilesRejected
(files: File[]) => void
Receives the files that do not match accept, and those after the first without multiple.
disabledDefault false
boolean
The zone can't be pressed or focused and takes no files; it still keeps the browser from opening dropped files.
pendingDefault false
boolean
The application is working on the files: a spinner replaces the icon, the zone reports that it is busy and takes no new files.
error
ReactNode
A message linked to the zone, whose outline takes the error color.
sizeDefault 'md'
'sm''md'
md is the tall area with a large icon; sm is one row with a small icon, for a form or a sidebar.
name
string
Submits files with a native form under this name: the last files the zone took, until the form resets, or files.
files
readonly File[]
The files a named zone submits when the application keeps them, such as after a Remove action; they replace the files the zone took, also after the form resets.
form
string
The id of a form the zone belongs to when it is outside of it.
inputRef
Ref<HTMLInputElement>
The native file input that opens the file picker, for example from a Replace button. The zone holds no value a form library could register, as a field's input does, so ref reaches the zone itself, the drop target, and this ref the picker.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Part names

Each part's class and state attributes are public and versioned, for an application's CSS to style.

env-drop-zone
The area that takes dropped files.
env-drop-zone-description
The sentence about the accepted files.
env-drop-zone-icon
The icon.
env-drop-zone-label
The label.
env-drop-zone-target
The target that takes the drop and opens the file picker.
env-drop-zone-text
The column with the label and the description.
data-drag-over
Present
data-invalid
Present
data-pending
Present
data-size
md · sm

Changelog

1.0.0

onFiles is onFilesAccepted and onReject is onFilesRejected; they run for dropped and browsed files alike. The zone is named by what it shows, its label and its description, and described by its error. Its frame is Field's root and error.

FileDropzone is DropZone, with the env-drop-zone-* classes; the page is drop-zone.

1.0.0-prerelease.6

Colors under the pointer and while pressed change with --env-motion-ease-enter, rather than easing in slowly, so hover and press show from the first frame.