In this guide, I’ll be explaining how to upload images by dragging and dropping. This includes dragging images
- From OS to browser
- From browser to browser
I’ll be using Pure Javascript (no frameworks or libraries), and the code will be compatible with all modern browsers including IE 9+. Also, I haven’t used ES6 which means you won’t need a compiler like Babel to run the code.
Our Drag & Drop function will do 5 things.
- Listen for drag and drop
- Indicate when a file is hovering on the drop region
- Validate dropped images
- Preview images
- Upload images
However, relying completely on drag & drop is a bad idea because mobile users won’t like it. Also, most mobile browsers don’t even support the API. Here’s the browser support for Drag & Drop API from caniuse.com.
Therefore, we will also allow users to simply upload files by selecting them (Via <input type="file"
).
Here’s the final result of this tutorial:
A demo is available on JSFiddle
Let’s start with basic HTML.
<div id="drop-region"> <div class="drop-message"> Drag & Drop images or click to upload </div> <div id="image-preview"></div> </div>
Then, save the elements in Javascript variables.
var // where files are dropped + file selector is opened dropRegion = document.getElementById("drop-region"), // where images are previewed imagePreviewRegion = document.getElementById("image-preview");
File Selector
For file selecting, we have to use <input type="file">
here. However, those default file selectors look old-fashioned and hard to style with CSS. Therefore, using fake file input, we can make dropRegoin
opening the file selector when clicked.
// open file selector when clicked on the drop region var fakeInput = document.createElement("input"); fakeInput.type = "file"; fakeInput.accept = "image/*"; fakeInput.multiple = true; dropRegion.addEventListener('click', function() { fakeInput.click(); });
Note that the accept attribute (fakeInput.accept = "image/*"
) is used to limit the files to image files. You can also specify the mime type too. For example, if you only need users to select only GIF files, you can use image/gif
. Or, multiple values like image/png, image/jpeg
. The multiple
attribute allows users to select multiple images at once.
Now, when the dropRegion is clicked, the fakeInput will be clicked. So, the browser will open the OS file selector.
Let’s add the onChange
event to the file input.
fakeInput.addEventListener("change", function() { var files = fakeInput.files; handleFiles(file); });
fakeInput.files
is a FileList which we can use to preview and upload images. We will create the handleFiles()
function later.
Drag Events
The Drag & Drop API defines 8 events: 4 events for the draggable element and 4 events for the droppable element. We will only need the latter 4 when developing a drag & drop image uploading.
dragenter
: a dragged item enters a valid drop target.dragleave
: a dragged item leaves a valid drop target.dragover
: a dragged item is being dragged over a valid drop target. Triggered every few hundred milliseconds.drop
: an item is dropped on a valid drop target.
Adding Drag & Drop Functionality
When a file is dragged into the browser from the OS, the browser will try to open and display it by default. We have to prevent default behaviors and stop propagating to upper elements for all the events. This ensures any external event (especially an event of an outer element) doesn’t crash the functionality.
function preventDefault(e) { e.preventDefault(); e.stopPropagation(); } dropRegion.addEventListener('dragenter', preventDefault, false); dropRegion.addEventListener('dragleave', preventDefault, false); dropRegion.addEventListener('dragover', preventDefault, false); dropRegion.addEventListener('drop', preventDefault, false);
Then, we can handle the drop
event.
function handleDrop(e) { var data = e.dataTransfer, files = data.files; handleFiles(files) } dropRegion.addEventListener('drop', handleDrop, false);
e.dataTransfer
is a Data Transfer object which contains the dragged data. e.dataTransfer.files
contains the dragged local files as a FileList which is exactly same as the files
variable in the change
event handler of the file input.
Now it’s the time to create the handleFiles()
function which gets a File List and upload each item.
function handleFiles(files) { for (var i = 0, len = files.length; i < len; i++) { if (validateImage(files[i])) previewAnduploadImage(files[i]); } }
We can loop through the FileList (files
here) using a simple for loop. If each file is a valid image, we will preview and upload it.
Wait! There’s a problem. This only works if the files are dragged from the local file system. What if an image is dragged from another webpage? We will need to optimize our drop
handler for this (This is quite tricky).
Here’s the upgraded handleDrop
function.
function handleDrop(e) { var dt = e.dataTransfer, files = dt.files; if (files.length) { handleFiles(files); } else { // check for img var html = dt.getData('text/html'), match = html && /\bsrc="?([^"\s]+)"?\s*/.exec(html), url = match && match[1]; if (url) { uploadImageFromURL(url); return; } } function uploadImageFromURL(url) { var img = new Image; var c = document.createElement("canvas"); var ctx = c.getContext("2d"); img.onload = function() { c.width = this.naturalWidth; // update canvas size to match image c.height = this.naturalHeight; ctx.drawImage(this, 0, 0); // draw in image c.toBlob(function(blob) { // get content as PNG blob // call our main function handleFiles( [blob] ); }, "image/png"); }; img.onerror = function() { alert("Error in uploading"); } img.crossOrigin = ""; // if from different origin img.src = url; } }
Here if files are not selected, we will check for browser images. When you drag an image on the browser to another place, the image is dragged as HTML. So, we can get the HTML and check for src=""
attribute which is the image URL. Then, we can fetch the image with Image
object and convert it to a canvas. Finally, the canvas can be converted to a blob, and we can use our handleFiles
function as usual.
However, there some limitations to this implementation. This would not work if the image is from a server that blocks cross-domain requests. To solve that, you can use a proxy image server to fetch images. And, also dragging from chrome to firefox can also show errors. Even with those limitations, dragging from one page to another is a cool feature.
Validating Images
function validateImage(image) { // check the type var validTypes = ['image/jpeg', 'image/png', 'image/gif']; if (validTypes.indexOf( image.type ) === -1) { alert("Invalid File Type"); return false; } // check the size var maxSizeInBytes = 10e6; // 10MB if (image.size > maxSizeInBytes) { alert("File too large"); return false; } return true; }
This function validates two attributes.
- File type – In this example, I have allowed jpg, png, and gif files. However, you can add or remove any valid MIME type.
2. File size – I have set the maximum size to 10MB. It is a good practice to limit the file size to prevent malicious oversized uploading.
Validating on the client-side has one advantage. The user can know instantly whether their image is valid. If we completely rely on server-side validation, the user has to wait until the upload is finished and the server processes the response. This can be a burden for the server if the image files are very large.
(Validating file size from the client-side inadequate. You must validate it again on the server-side.)
Previewing and Uploading Images
1. Previewing
There are several ways to preview the image.
- Preview after uploading – Upload the image, get the image URL, and display it.
- Preview before uploading – We can preview the image before uploading. This is fast and efficient. There are two Javascript methods we can use.
For this example, I’ll choose 2.2: Preview the image before uploading using FileReader.readAsDataURL()
.
Let’s start
function previewAnduploadImage(image) { // container var imgView = document.createElement("div"); imgView.className = "image-view"; imagePreviewRegion.appendChild(imgView); // previewing image var img = document.createElement("img"); imgView.appendChild(img); // progress overlay var overlay = document.createElement("div"); overlay.className = "overlay"; imgView.appendChild(overlay); // ... }
We will read the image in the next step and display it on img
element. The overlay will be used to add a faded look to each image until uploaded. We will reduce the width of the overlay when the image is uploading.
Let’s read the image and preview.
// read the image... var reader = new FileReader(); reader.onload = function(e) { img.src = e.target.result; } reader.readAsDataURL(image);
Here we create a FileReader
object and set up the onload
event handler for it. Then, we read the image as a data URL. This is done asynchronously. After the reading is finished, the onload callback will be called. The src attribute of the img
element will be set to e.target.result
which is a base64 data URL.
2. Uploading
We can use FormData
interface to create form data to send them to the server. Then, we can use AJAX with XMLHttpRequest
to perform an asynchronous upload.
Creating form data:
// create FormData var formData = new FormData(); formData.append('image', image);
AJAX request:
var uploadLocation = 'UPLOAD_LOCATION'; var ajax = new XMLHttpRequest(); ajax.open("POST", uploadLocation, true); ajax.onreadystatechange = function(e) { if (ajax.readyState === 4) { if (ajax.status === 200) { // done! } else { // error! } } } ajax.upload.onprogress = function(e) { // change progress // (reduce the width of overlay) var perc = (e.loaded / e.total * 100) || 100, width = 100 - perc; overlay.style.width = width; } ajax.send(formData);
Here uploadLocation
should be set to the server URL of the upload handler. The onreadystatechange
event handler is called when the state is changed. ajax.readyState
is 4 when the request is completed. ajax.status
is the status code sent by the server. Usually, servers set the status code to 200 when the request is successful. ajax.upload.onprogress
event handler is called each time when the progress is updated. In this function, we calculate the percentage of the progress and reduce that from the overlay’s width.
And, the || 100
part is a simple bug fix. Sometimes, (e.loaded / e.total * 100)
can return NaN
. In that case, the default value, 100, will be used.
Here’s the complete function.
function previewAnduploadImage(image) { // container var imgView = document.createElement("div"); imgView.className = "image-view"; imagePreviewRegion.appendChild(imgView); // previewing image var img = document.createElement("img"); imgView.appendChild(img); // progress overlay var overlay = document.createElement("div"); overlay.className = "overlay"; imgView.appendChild(overlay); // read the image... var reader = new FileReader(); reader.onload = function(e) { img.src = e.target.result; } reader.readAsDataURL(image); // create FormData var formData = new FormData(); formData.append('image', image); // upload the image var uploadLocation = 'UPLOAD_LOCATION'; var ajax = new XMLHttpRequest(); ajax.open("POST", uploadLocation, true); ajax.onreadystatechange = function(e) { if (ajax.readyState === 4) { if (ajax.status === 200) { // done! } else { // error! } } } ajax.upload.onprogress = function(e) { // change progress // (reduce the width of overlay) var perc = (e.loaded / e.total * 100) || 100, width = 100 - perc; overlay.style.width = width; } ajax.send(formData); }
Finally, here’s the CSS I created. You can change it as you like.
#drop-region { background-color: #fff; border-radius:20px; box-shadow:0 0 35px rgba(0,0,0,0.05); width:400px; padding:60px 40px; text-align: center; cursor:pointer; transition:.3s; } #drop-region:hover { box-shadow:0 0 45px rgba(0,0,0,0.1); } #image-preview { margin-top:20px; } #image-preview .image-view { display: inline-block; position:relative; margin-right: 13px; margin-bottom: 13px; } #image-preview .image-view img { max-width: 100px; max-height: 100px; } #image-preview .overlay { position: absolute; width: 100%; height: 100%; top: 0; right: 0; z-index: 2; background: rgba(255,255,255,0.5); }
Additional Improvements
1. Detect drag & drop feature and change the message
It is not useful to show the message “Drag & Drop images or click to upload” on browsers which don’t support the Drag and Drop API. So, we can detect the feature and change the message.
Detect function is taken from Modernizr.
function detectDragDrop() { var div = document.createElement('div'); return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div) } // change the message var dragSupported = detectDragDrop(); if (!dragSupported) { document.getElementsByClassName("drop-message")[0].innerHTML = 'Click to upload'; }
2. Highlight on dragenter
We can highlight the dropRegion
when a file is dragging over using the drag events.
dropRegion.addEventListener('dragenter', highlight, false); dropRegion.addEventListener('dragover', highlight, false); dropRegion.addEventListener('dragleave', unhighlight, false); dropRegion.addEventListener('drop', unhighlight, false); function highlight() { dropRegion.classList.add('highlighted'); } function unhighlight() { dropRegion.classList.remove("highlighted"); }
Then, some CSS for .highlighted
will make the dropRegion more interesting. For example,
.highlighted { background-color:grey; }
Conclusion
In this article, I described how to upload a file using drag and drop. It is true that some effort is needed to create a drag and drop functionality. However, if you use a library like jquery the DOM manipulation would be much easier even the process is the same. If you need it, you can use ES6 and make syntax shorter. The fetch()
API can also be instead of XMLHttpRequest
. Finally, check out and play with the JSFiddle I created. I used the free service IMGBB for image uploading.
Supun Kavinda , commonly I used libraries for Drag & Drop, really helpful to know how to realize it on pure JS!
I’m really happy to hear that 🙂
Excellent post – many thanks. There is a small typo in the “Let’s add the onChange event to the file input.” handler:
fakeInput.addEventListener(“change”, function() {
var files = fakeInput.files;
handleFiles(file);
});
The “file” in “handleFiles(file);” is missing its final ‘s’.
Thank you for pure JS option. I like how simple/clean your implementation is.
How can I “attach” the name of the image in preview to an tag and push forward with other information in a form?
What is your name:
Drag & drop an image file in the box provided here.
Drag & Drop images or click to upload
Submit Form>
Sorry my JS skills are non-existent. Thank you for any advice/help.
Image of code… new to blogs as well I guess…
I tried this solution and working fine for the window google chrome and ubuntu google chrome also worked on firefox and opera browser. But it is not working on Mac Google chrome can you please help me so I can implement that solution
Hi – I know this is a old post, but I hope someone’s still monitoring… this is all great, except that I cannot for the life of me get the progressbar / overlay bit to work. It simply won’t show until the upload has completed, which is kind of redundant. I am wondering if it’s deprecated code, or what… maybe it’s just me being daft (quite likely). And ta for a great post, that aside.
to be clear,, the “ajax_upload_onprogress” is receiving the data ok – it’s the overlay that isn’t displaying, somehow. (Had to rewrite that call, as this site rejected the post with the dot notation!)