I realised (when looking myself) that there are a lot of demos and tutorials that show you how to drag-and-drop a file into the browser and then render it on the page. They’re often labelled as “drag-and-drop and upload”, but they actually don’t upload. This tutorial will take you that final step.
I’ll walk you through an example that you can play with: drag a file into the web page, and it’ll be uploaded to the server instantly.
Tasks #
Let’s break this process down into specific tasks:
- Capture the drop event and read its data.
- Post that binary data to the server.
- Provide feedback on the progress of the upload.
- Optionally render a preview of what’s being uploaded and its status.
To achieve all of this, we need the following HTML5 and non-HTML5 APIs:
Keep in mind that not all browsers support all of this technology today, but they’re getting close. I just wanted to create a clear and complete tutorial on how to go the whole hog and upload that dropped file.
The end result: we’re able to drop the file anywhere in the browser, we get a preview and progress of the upload, and it’s a slick experience.

Drag and Drop #
Just as a forewarning, drag-and-drop has been known to be a bit of a killjoy. In fact, it’s a nightmare, but I won’t repeat what’s been saidbefore.
Our example is going to allow you to drag-and-drop a file anywhere within the browser. To achieve that, we need to attach our event listeners to the
body
or
documentElement
(i.e., the root HTML node). By listening on the
documentElement
, this code could be anywhere in the page, as
documentElement
will always exist. You can only listen on
body
once the
<body>
element has been encountered.
Here’s the pattern of code we need to capture the drop event on the entire document:
var doc = document.documentElement; doc.ondragover = function () { this.className = 'hover';return false; }; doc.ondragend = function () { this.className = ''; returnfalse; }; doc.ondrop = function (event) { event.preventDefault && event.preventDefault(); this.className = ''; // now do something with: var files = event.dataTransfer.files; return false; };
I’m using the
hover
class so that I can toggle a tip explaining to the user that the file can be dropped on the page.
Inside the
drop
event, we can access the dropped files via
event.dataTransfer.files
.
An Alternative to Drag and Drop #
Annoyingly, as I write this, I realise this combination of API requirements is currently only met by Chrome and Firefox. So, taking the test from Modernizr, we can test for support and provide our alternative:
var dndSupported = function () { var div = document.createElement('div'); return ('draggable' in div) || ('ondragstart' in div &&'ondrop' in div); }; if (!dndSupported()) { // take alternative route }
Instead of drag-and-drop, we can insert a file input element (I’ve given it an
id
of “upload”), and when its value is changed, the file can be scooped in:
document.getElementById('upload').onchange = function (event){ // `this` refers to the element the event fired upon var files = this.files; };
You can see the equivalent is simply the
files
property of the
HTMLInputElement
object. This will give us access to the same file as the
event.dataTransfer.files
from the drop event.
Automatically Uploading the File #
This is the neat bit, and it’s painfully simple. We use a feature of the XHR2 spec:
FormData
. Once we create an instance of
FormData
, we can append items to it:
var formData = new FormData(); for (var i = 0; i < files.length; i++) { formData.append('file', files[i]); } // now post a new XHR request var xhr = new XMLHttpRequest(); xhr.open('POST', '/upload'); xhr.onload = function () { if (xhr.status === 200) { console.log('all done: ' + xhr.status); } else { console.log('Something went terribly wrong...'); } }; xhr.send(formData);
FormData
Yeah, that’s it.†
Let’s look at a couple of key things that are going on the above code.
formData.append('file', files[i]);
We’re sending named parameters to our server, specifically an array of values called
file
. Obviously, you can call it what you want, but the
file
is the name your server will be looking for when it saves the uploaded file (or files).
xhr.onload = function () { if (xhr.status === 200) { console.log('all done: ' + xhr.status); } else { console.log('Something went terribly wrong...'); } };
If you’re familiar with XHR, you’ll notice we aren’t listening to
onreadystatechange
, only
onload
— which is (in effect) a convenience function to let you know when the
readyState
is 4 (i.e., loaded!). You should still check and respond appropriately to the status code of the request to ensure it’s 200 OK, rather than a 500 (internal server error) or 404 (not found) or anything else.
xhr.send(formData);
The nice trick here is that XHR has automatically set the encoding of the posted data to
multipart/form-data
. This encoding allows your server to read and save the files. It’s like the encoding used when you send an attachment in an email.
Providing Feedback to the User #
XHR2 now (flippin’ finally) comes with a
progress
event. So if you’re sending or retrieving a large file via XHR, you can tell the user how far along you are.
It’s pretty simple. If you want to track the progress of the XHR request, listen to the
progress
event. One gotcha that caught me out for some time: I needed to track the progress of the upload, not the download. To do this properly, you need to listen for progress on theXHR upload object, as so:
xhr.upload.onprogress = function (event) { if (event.lengthComputable) { var complete = (event.loaded / event.total * 100 | 0); progress.value = progress.innerHTML = complete; } }; xhr.onload = function () { // just in case we get stuck around 99% progress.value = progress.innerHTML = 100; };
progress
eventNote that instead of
xhr.onprogress
, we use
xhr.upload.onprogess
. When this event fires, we check that the event supports calculating the amount of data uploaded (the
lengthComputable
part), and then calculate the amount completed.
In my HTML, I’m using a
<progress>
element (similar to the
<meter>
element, but more semantically appropriate as we’re presenting the progress of a task) to show the user how much of their file has been uploaded:
<progress id="progress" min="0" max="100"value="0">0</progress>
Note that I’m using
innerHTML
for browsers that don’t yet support
<progress>
. Then with a dash of CSS-generated content, both the
innerHTML
and
value
of the progress can be the same (perhaps an over optimisation, but I was rather proud of myself at the time!).
And Finally, Rendering a Preview #
As a nice addition, we’ll give the user a preview of what we’re uploading for them. It requires the File API — specifically the
FileReader
API.
As the drag-and-drop operation (or the
input[type=file]
) contains a file object, we can hand this over to the
FileReader
to get the contents of the file. If it’s something like an image, we can get the Base64 data and give the user a preview. Or if it’s text, we could render it in a
<div>
. The MIME type for the file is available as the
type
property on the file object.
So let’s assume we only accept images. Here’s the preview part that runs after the file has been received by the browser:
var acceptedTypes = { 'image/png': true, 'image/jpeg': true, 'image/gif': true }; if (acceptedTypes[file.type] === true) { var reader = new FileReader(); reader.onload = function (event) { var image = new Image(); image.src = event.target.result; image.width = 100; // a fake resize document.body.appendChild(image); }; reader.readAsDataURL(file); }
We create the
FileReader
and give it a file to read. In the above example, I’ve used
readAsDataURL
, but you can also read files in plain text and binary formats (as per the spec).
When the reader has read the file and the data is available, it fires the
load
event, which in turn creates our new image.
The result of the file read action is in the
event.target.result
property. In the case of the
readAsDataURL
, the value is along the lines of
data:image/png;base64,ABC...
.
Using All the Tools in the Shed #
This example may be a little contrived for the article that I wanted to write. I’ve used many different technologies to do something that’s a fairly common pattern on the web. In fact, it was because I had trouble finding a definitive source on uploading to the actual server (except, of course, as I wrote this article I found this example from back in late 2010!).
Hopefully, you’ll see how each bit of tech can work together to create an application, but equally I hope that you can see where this would work if none or just some of the technology wasn’t available. Make sure you detect functionality. Make sure you provide fallbacks. Make sure you develop responsibly.
Here’s the final drag-and-drop and upload demo for you to play with.