Grow your CSS skills. Land your dream job.

HTML5 Drag and Drop Avatar Changer with Resizing and Cropping

Published by Chris Coyier

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

Comments

  1. How secure is this kind of upload? Are there security holes to worry about?

    • Jacek JÄ™drzejewski
      Permalink to comment#

      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”.

  2. Permalink to comment#

    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.

  3. Permalink to comment#

    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.

    • Permalink to comment#

      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.

  4. 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.

  5. Jason
    Permalink to comment#

    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.

  6. Mike M
    Permalink to comment#

    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?

  7. Noah Gelman
    Permalink to comment#

    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

  8. 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?

  9. work great
    thnx chris :D

Leave a Comment

Current day month ye@r *

*May or may not contain any actual "CSS" or "Tricks".