DropZone
An area that takes files dropped onto it, and hands them over as Files.
- React
- Svelte
- Vue
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> ) }
<script lang="ts">
import { DropZone } from '@tremolo-ui/svelte'
// Copy this file from the Styling page into your own project.
import dropZoneTheme from './DropZone.module.css'
let files: File[] = $state([])
let rejected: File[] = $state([])
</script>
<DropZone.Root
class={dropZoneTheme.root}
accept="audio/*"
multiple
onDrop={(dropped) => {
rejected = []
files = dropped
}}
onReject={(dropped) => (rejected = dropped)}
>
Drop an audio file here
</DropZone.Root>
<ul>
{#each files as file}
<li>{file.name} ({file.type || 'unknown type'})</li>
{/each}
</ul>
{#if rejected.length > 0}
<p>Not audio: {rejected.map((file) => file.name).join(', ')}</p>
{/if}
<script setup lang="ts">
import { ref } from 'vue'
import { DropZone } from '@tremolo-ui/vue'
// Copy this file from the Styling page into your own project.
import dropZoneTheme from './DropZone.module.css'
const files = ref<File[]>([])
const rejected = ref<File[]>([])
function onDrop(dropped: File[]) {
rejected.value = []
files.value = dropped
}
</script>
<template>
<DropZone
:class="dropZoneTheme.root"
accept="audio/*"
multiple
@drop="onDrop"
@reject="(dropped) => (rejected = dropped)"
>
Drop an audio file here
</DropZone>
<ul>
<li v-for="(file, i) in files" :key="i">
{{ file.name }} ({{ file.type || 'unknown type' }})
</li>
</ul>
<p v-if="rejected.length > 0">
Not audio: {{ rejected.map((file) => file.name).join(', ') }}
</p>
</template>
Import
- React
- Svelte
- Vue
import { DropZone } from '@tremolo-ui/react'
import { DropZone } from '@tremolo-ui/svelte'
import { DropZone } from '@tremolo-ui/vue'
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.
| Prop | Type | Default |
|---|---|---|
accept | string | — |
multiple | boolean | false |
disabled | boolean | false |
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.
| Prop | Type | Default |
|---|---|---|
accept | string | — |
multiple | boolean | false |
disabled | boolean | false |
onDrop | (files: File[], event: DragEvent) => void | — |
onReject | (files: File[], event: DragEvent) => void | — |
children* | ReactNode | — |
Full reference: DropZoneProps
| Data attribute | Values |
|---|---|
[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 |