Skip to main content

DropZone

An area that takes files dropped onto it, and hands them over as Files.

Source Style
Loading...
function App() {
  const [files, setFiles] = useState<File[]>([])
  const [rejected, setRejected] = useState<File[]>([])

  return (
    <div>
      <DropZone.Root
        className={dropZoneTheme.root}
        accept="audio/*"
        multiple
        onDrop={(files) => {
          setRejected([])
          setFiles(files)
        }}
        onReject={(files) => setRejected(files)}
      >
        Drop an audio file here
      </DropZone.Root>
      <ul>
        {files.map((file) => (
          <li key={file.name}>
            {file.name} ({file.type || 'unknown type'})
          </li>
        ))}
      </ul>
      {rejected.length > 0 && (
        <p>Not audio: {rejected.map((file) => file.name).join(', ')}</p>
      )}
    </div>
  )
}

Import​

import { DropZone } from '@tremolo-ui/react'

What it handles for you​

Leaving a child does not end the drag. dragenter and dragleave fire for descendants too, so moving between two children of the zone leaves it before it enters. Counting the pairs is what keeps [data-dragover] from flickering off in the middle of the element.

The drop is cancelled even when it is refused. A drop nothing handles makes the browser leave the page and open the file, which loses whatever the app was holding. The zone swallows it whether or not it takes the file.

A drag that ends elsewhere still clears the state. Dropped on another element, cancelled with Escape, taken out of the window — none of those send a dragleave, and the element would stay marked as a drop target for good.

Refusing before the drop​

accept is checked twice. On the drop it decides which files go to onDrop and which go to onReject. While the drag is still in the air it sets [data-invalid], so the zone can show it will not take what is being carried before the person lets go.

What can be told apart before the drop is limited. The browser reports the type of what is being dragged but withholds the name, so a rule written as an extension (.wav) cannot be decided yet — it is not counted against the drag, and it is decided on the drop where the name is. Rules written as MIME types (audio/*, audio/wav) work either way.

Making any element a drop target​

DropZone.Root draws a <div>. When the element already exists — a waveform display, a track lane, a canvas — the useDropZone hook does the same job without wrapping it:

const { refCallback, over, invalid } = useDropZone({
accept: 'audio/*',
onDrop: (files) => load(files[0]),
})

return <canvas ref={refCallback} data-dragover={over ? '' : undefined} />

It takes the same options, minus children, and returns the state that DropZone.Root renders its data attributes from.

PropTypeDefault
acceptstring—
multiplebooleanfalse
disabledbooleanfalse
onDrop(files: File[], event: DragEvent) => void—
onReject(files: File[], event: DragEvent) => void—

Full reference: UseDropZoneOptions

Picking a file instead​

To open a file picker from a button, see FileInput, which reports the same File[].

API Reference​

Each part lists the props of its own. It also takes the attributes of the element it renders, such as className and style.

In Svelte and Vue the props are the same, written their way — class rather than className, bind:value or v-model, snippets or slots for content. See Svelte and Vue.

State is on the element as a data attribute, so it is a selector away — see Styling. An attribute is only there while its state is on.

Root​

The area itself. It holds the settings and reports what was dropped.

PropTypeDefault
acceptstring—
multiplebooleanfalse
disabledbooleanfalse
onDrop(files: File[], event: DragEvent) => void—
onReject(files: File[], event: DragEvent) => void—
children*ReactNode—

Full reference: DropZoneProps

Data attributeValues
[data-dragover]Present while files are being dragged over the zone
[data-invalid]Present while what is being dragged matches no accept rule that can be decided yet
[data-disabled]Present when disabled is set