In any app that has user avatars, users should be able to change those avatars. Anything to make that easier is desirable. Many apps start with a user’s Twitter avatar, Facebook avatar, or Gravatar. That’s a smart move. Avatars give users a sense of ownership over a virtual space so any way to get them to have their desired avatar is good for engagement.
Let’s create a page where a user can update their avatar with as little friction as possible: they just drop an image anywhere on the page and it’s done.

The Workhorse of Drag and Drop
Perhaps the most important bit we’ll deal with is the drop event. This is where we get access to the file and can do what we need to do with it. We’ll be using jQuery to help us with events and whatnot.
// Required for drag and drop file access
jQuery.event.props.push('dataTransfer');
$("body").on('drop', function(event) {
// Or else the browser will open the file
event.preventDefault();
// Do something with the file(s)
var files = event.dataTransfer.files;
}
Interestingly enough, as written, the above won’t work. One more bit is required, and that is to prevent the default of the dragover
event as well. That’s fine, as we will be using that event to make some kind of UI change to emphasize “dropability.”
$("body").on("dragover", function(event) {
// Do something to UI to make page look droppable
// Required for drop to work
return false;
});
And of course remove that UI change if the user doesn’t perform the drop:
$("body").on("dragleave", function(event) {
// Remove UI change
});
Handling the Dropped File
Drag and drop can do multiple files. We’re only dealing with one file here, so let’s just use the first one.
$("body").on('drop', function(event) {
event.preventDefault();
var file = event.dataTransfer.files[0];
if (file.type.match('image.*')) {
// Deal with file
} else {
// However you want to handle error that dropped file wasn't an image
}
}
Perhaps if you were really nice, you’d loop over all the files and find the first image rather than rejecting based on the first file.
Resizing the Avatar
There are server side ways to resize images, but that requires a round trip (slow) and the transfer of potentially enormous files (slow). We can resize the avatar to the size we want for our app right on the client side. This is wicked fast.
You can do this by creating a <canvas>
, drawing the image to the canvas, then exporting the canvas as a Data URI. Andrea Giammarchi has an excellent script for this. You would just include this script before all this custom code we’re writing.
Squaring Avatars
In our example, all our avatars are squares. Squaring is a little tricky. Do you allow rectangles and just center them? Do you apply whitespace around edges so rectangles are really squares? Do you crop the image so it’s a square? If you do crop, from what original point do you do the cropping? Do you crop before or after the resizing?
- Let’s go with cropping.
- Let’s not bother the user about it at all. There are ways to build UI cropping tool for users to pick their own crop, but let’s not make an extra step for them and just do it automatically
- Let’s crop from the top/left.
All this canvas stuff was a bit over my head. Fortunately Ralph Holzmann was able to jump in an help me alter Andrea’s original script to handle cropping.
Crop / Resize In Action
With those parts ready to go, we can crop and resize by calling our new script that does both:
var fileTracker = new FileReader;
fileTracker.onload = function() {
Resample(
this.result,
256,
256,
placeNewAvatar
);
}
fileTracker.readAsDataURL(file);
placeNewAvatar
is a custom callback function that we’ll provide that receives the newly resized data URI we can place on the page.
function placeNewAvatar(data) {
$("#profile-avatar").attr("src", data);
}
Uploading and Saving Avatar
You’ll probably want to save your resized avatar, not the original. You know, keep things fast and storage low.
It would be silly to trigger a page load to upload the file, since we’re already using fancy drag and drop. So we’re looking at Ajaxing the file to the server. Perhaps you’re server is totally fine accepting and saving that Data URI. In that case, just send ‘er up however. Maybe like
$.post({
url: "/your/app/save/image/whatever",
data: data
});
But if you’re using some kind of asset host, they will probably want a real file, not a data URI. And they’ll want you to POST
them multipart/form-data
not just a string. So you’ll need to change that data URI into a file (or “Blob”).
function dataURItoBlob(dataURI) {
var binary = atob(dataURI.split(',')[1]);
var array = [];
for (var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
}
You also can’t use jQuery to Ajax the file anymore, because it’s Ajax methods won’t let you pass FormData()
in my experience. So you’ll have to do it manually, which is fine since drag and drop more new-fangled than Ajax anyway.
var xhr = new XMLHttpRequest();
var fd = new FormData();
fd.append('file', resampledFile);
xhr.open('POST', "/your/app/save/image/whatever", true);
xhr.send(fd);
Relevant CSS Bits
If you’re going to watch for the drop event on the body, you should make sure the body is at least as tall as the page. Otherwise there might be some space toward the bottom that won’t take the event.
html, body {
height: 100%;
}
Also, the drag event isn’t only fired by files that you drag in from outside the browser window, it can be fired by dragging an image already placed on the page. To prevent this, I wrap the image in a div and apply a pseudo element over the entire div. This prevents the dragging of that image.
.profile-avatar-wrap {
position: relative;
}
.profile-avatar-wrap:after {
/* Drag Prevention */
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
This works with the file input as well
As a fallback, you could include a file input as well. How exactly you want to handle that is up to you. For example, inject it on a feature detection fail, or just supply both on the page.
<input type="file" id="uploader">
Everything would be pretty much the same, only access to the file would happen on:
$("#uploader").on('change', function(event) {
var file = event.target.files[0];
});
This is particularly relevant for mobile where drag and drop is rather irrelevant.
Not Quite Done
Here’s the demo:
I’m not even quite sure the browser support. Here’s CanIUse for drag and drop and canvas.
I’m sure I haven’t handled everything quite perfectly here. For one thing, it doesn’t seem to work in Opera. The drag and drop stuff seems to work (it asks you if you want to upload the file) but never quite processes.
It does work in Chrome/Safari/Firefox.
For another thing, I handled the “Drop File Anywhere” thing by adding a pseudo element to the body. Sometimes it gets a little “flashy”. It’s an improvement for when I tried doing it with a div, but not great. I also tried only doing and UI action when the dragleave
event originated on the body itself.
$("body").on("dragleave", function(event) {
if (event.currentTarget == $("body")[0]) {
$("body").removeClassClass("droppable");
}
// Required for drop to work
return false;
});
But no dice.
And finally, the code shown in this article isn’t organized. In real life, you would organize all this. Here is the organized code. If you can fix stuff or make it better, I tossed it on GitHub so feel free to submit a pull request (how).
How secure is this kind of upload? Are there security holes to worry about?
You resize/crop on client side and then send it to server side. Without validation on server side anyone could upload anything, any image, any size etc. IMHO this post is just a proof it can be done on client side (and can be used for saving upload time for big images as it was mentioned in the post) but you shouldn’t rely on uploaded stuff and you cannot trust anything sent to “/your/app/save/image/whatever”.
Awesome sauce! I’ll definitely need to use this in the near future.
A minor bug on Chrome 23 on Mac: after using the file upload to change the avatar, I then drag/drop a new image. As I’m dragging over the drop zone, the drop zone flickers on/off repeatedly. Only happens the first time after using the file upload, though. No idea what the fix might be, but maybe somewhere here does.
And by “somewhere here does” I mean “someone here does”…
I found the same bug, however Chrome 24 on Mac, not sure whats causing it?
The moment the dragover state is triggered on the body, an :after element is added, which then causes the body to lose the dragover state.
Epilepsy ensues.
Right. I mention that in the article as a problem. It’s even worse when you use a DIV or something in my experience.
The remaining issues are:
1) Why the F does a pseudo element trigger a dragleave?
2) This happens even when you check the event.currentTarget and make sure it’s the body (weird)
3) is unbinding / rebinding the answer? Usually never is…
One thing that I’ve actually used is just to do something less visually intensive to highlight the drop area. So even if it flickers it’s not so epilepsy causing.
1 & 2. Maybe it works like mouseover and mouseout.
This flickering may or may not be related (though I want to believe it is), but half the time when I drop an image, the “drop” event is not firing, and I am caught loading the image as though I had dragged a bookmark onto the page.
To combat random flickering in the past, I’ve set the dragleave event to not directly call the leaving action, but instead use a setTimeout with a 200ms delay that calls the leaving action. Then in the dragenter and dragover events, clear that timeout before you do anything else. I picked this technique up from a drag and drop tutorial somewhere and it’s served me well.
For the issue of the :after pseudo element specifically, maybe put a pointer-events:none on that bad boy?
Both great ideas. I’m pushing them both.
I’m fairly sure Opera doesnt support the FormData() function yet, which prevents the upload code from happening. Not surprisingly I don’t think any IE (maybe IE10 does) supports the new XMLHttpRequest 2 that you are using on the upload either. Remy Sharp seems to mention that as well in his article: http://html5doctor.com/drag-and-drop-to-server/
Thanks for the great writeup!
Do you have any kind of stat regarding popularity of drag and drop vs. a regular file chooser? I always have my browser in full screen, so the file picker is always easier for me.
With IE8+ support it would actually make sense since this is core functionality.
Nevertheless very interesting article and it’s insane we (the server) have to resize a 9MP image to 100x100px.
I have been trying to add this to a profile page that I am working on. I think I did everything right. I uploaded the files to my test server the paths are right.
When i drag and drop the image. It highlights the avatar area or when I click on the choose file button it allows me to choose the file. However it does not load up the image when I do either.
Could there be a step that I am missing?
I am using latest version of Chrome
It’s not working on Safari for me or my brother. I’m on PC, he’s on Mac. I’m v. 5.1.7
For only Drag Drop of image files from OS to Browser, I have done the following an year back
Demo Link
Would like you to review it. Also, I used DataURI conversion by base64 encoding an image, which is visible in the src of images in your demo as well. I think its a bad bad way to store such huge encoded strings. What other options are possible?
work great
thnx chris :D
I want to implement this in a page with multiple avatars that could be changed, what do I need to modify to be able to drag, drop and change only one image, at this point it’s changing all avatars in my page.