HTML5 Drag and Drop Avatar Changer with Resizing and Cropping

Avatar of Chris Coyier
Chris Coyier on (Updated on )

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.

View Demo

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?

  1. Let’s go with cropping.
  2. 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
  3. 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:

View 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).