File upload with drag and drop in vanilla JavaScript
By Flavio Copes
Learn how to build file upload with drag and drop in vanilla JavaScript using the ondragover, ondragleave, and ondrop events, then POST the files as FormData.
To implement file upload with drag and drop in vanilla JavaScript you need three events: dragover, dragleave and drop. You highlight the drop zone while the user drags, read the files from the drop event, and POST them to your server with fetch() and FormData. No library needed.
I wrote about the Drag and Drop API concepts in the past.
Now I want to show how I implemented a simple file upload with drag and drop on a site I’m building.
The drop zone
Identify an HTML element you want people to drag their files to:
<div
id="dropzone"
ondragover="dragOverHandler(event)"
ondragleave="dragLeaveHandler(event)"
ondrop="dropHandler(event)"
>
...
</div>
ondragover is fired when people are dragging a file on the element. We can use this to add some style, for example a dashed border.
ondragleave is the opposite, when we exit the drop area.
I used this JS to add a dragging_over class to the element, and style it with CSS:
function dragOverHandler(event) {
event.preventDefault()
const dropzone = document.querySelector('#dropzone')
dropzone.classList.add('dragging_over')
}
function dragLeaveHandler(event) {
event.preventDefault()
const dropzone = document.querySelector('#dropzone')
dropzone.classList.remove('dragging_over')
}
#dropzone.dragging_over {
border: 2px dashed #fff;
background-color: #666;
}
Notice the event.preventDefault() call in the dragover handler. That one is not optional.
By default the browser does not allow dropping, and if you forget to prevent the default behavior, the drop event never fires. The browser handles the file itself instead, usually by opening it and navigating away from your page. If your drop zone “doesn’t work”, check this first.
Handling the drop
ondrop is fired when the file (or multiple files!) is dropped on the area.
That’s where the action happens.
I gather the files, check they’re images (I only want images in this example), and POST the data to /api/upload:
async function dropHandler(event) {
event.preventDefault()
const endpoint = `/api/upload`
if (event.dataTransfer.items) {
const formData = new FormData()
formData.append('action', 'upload')
for (let item of event.dataTransfer.items) {
if (item.kind === 'file') {
const file = item.getAsFile()
if (file) {
//I only want images
if (!file.type.match('image.*')) {
alert('only images supported')
return
}
formData.append('files', file)
}
}
}
try {
const response = await fetch(endpoint, {
method: 'POST',
body: formData,
})
if (response.ok) {
console.log('File upload successful')
} else {
console.error('File upload failed', response)
}
} catch (error) {
console.error('Error uploading file', error)
}
}
}
A few things to notice here.
We loop over event.dataTransfer.items because the user can drop several files at once. Each item has a kind property, and we only care about the ones where it’s file (dragging text from another window also fires drop).
item.getAsFile() gives us a File object. We check its type property to filter out anything that’s not an image, then append it to the FormData object under the files key.
When you pass a FormData object as the fetch() body, the browser sets the multipart/form-data content type on its own, boundary included. Don’t set the Content-Type header yourself, or the upload breaks server-side.
The server side
How to handle that server-side depends on your server.
With Astro I got the data using:
const formData = await Astro.request.formData()
console.log(formData.getAll('files'))
getAll('files') returns an array with every file we appended, because we used the same key for all of them.