惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
L
LINUX DO - 热门话题
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
爱范儿
爱范儿
D
DataBreaches.Net
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
S
SegmentFault 最新的问题
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
P
Proofpoint News Feed
The Hacker News
The Hacker News
T
ThreatConnect
N
News and Events Feed by Topic
T
Threatpost
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - Franky
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
F
Future of Privacy Forum
F
Full Disclosure
Cyberwarzone
Cyberwarzone
J
Java Code Geeks
李成银的技术随笔
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
博客园_首页
Scott Helme
Scott Helme
Google DeepMind News
Google DeepMind News
美团技术团队
Malwarebytes
Malwarebytes
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
Recent Announcements
Recent Announcements
C
CXSECURITY Database RSS Feed - CXSecurity.com

CSS-Tricks

Revealing Text With CSS letter-spacing | CSS-Tricks Technical Writing in the AI Age | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks The State of CSS Centering in 2026 | CSS-Tricks Stack Overflow: When We Stop Asking | CSS-Tricks Cross-Document View Transitions: The Gotchas Nobody Mentions | CSS-Tricks What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More | CSS-Tricks Computing and Displaying Discounted Prices in CSS | CSS-Tricks rotateX() | CSS-Tricks rotateY() | CSS-Tricks rotateZ() | CSS-Tricks rotate() | CSS-Tricks Soon We Can Finally Banish JavaScript to the ShadowRealm | CSS-Tricks Using CSS corner-shape For Folded Corners | CSS-Tricks A Scrollytelling Gift for Mum on Mother’s Day 2026 | CSS-Tricks Google’s Prompt API | CSS-Tricks Making Zigzag CSS Layouts With a Grid + Transform Trick | CSS-Tricks Fixed-Height Cards: More Fragile Than They Look | CSS-Tricks What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More | CSS-Tricks The Importance of Native Randomness in CSS | CSS-Tricks contrast() | CSS-Tricks contrast-color() | CSS-Tricks Let’s Use the Nonexistent ::nth-letter Selector Now | CSS-Tricks Quick Hit #126 Recreating Apple’s Vision Pro Animation in CSS | CSS-Tricks Quick Hit #125 Enhancing Astro With a Markdown Component | CSS-Tricks Quick Hit #124 Markdown + Astro = ❤️ | CSS-Tricks Quick Hit #123 What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More | CSS-Tricks A Well-Designed JavaScript Module System is Your First Architecture Decision | CSS-Tricks hypot() | CSS-Tricks The Radio State Machine | CSS-Tricks 7 View Transitions Recipes to Try | CSS-Tricks Quick Hit #122 Quick Hit #121 Selecting a Date Range in CSS | CSS-Tricks saturate() | CSS-Tricks justify-self | CSS-Tricks Quick Hit #120 Alternatives to the !important Keyword | CSS-Tricks Quick Hit #119 New CSS Multi-Column Layout Features in Chrome | CSS-Tricks Quick Hit #118 Making Complex CSS Shapes Using shape() | CSS-Tricks Quick Hit #117 Front-End Fools: Top 10 April Fools’ UI Pranks of All Time | CSS-Tricks Sniffing Out the CSS Olfactive API | CSS-Tricks What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More | CSS-Tricks Quick Hit #116 Form Automation Tips for Happier User and Clients | CSS-Tricks Quick Hit #115 Generative UI Notes | CSS-Tricks Quick Hit #114 Quick Hit #113 Experimenting With Scroll-Driven corner-shape Animations | CSS-Tricks Quick Hit #112 JavaScript for Everyone: Destructuring | CSS-Tricks Quick Hit #111 Quick Hit #110 What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More | CSS-Tricks 4 Reasons That Make Tailwind Great for Building Layouts | CSS-Tricks Quick Hit #109 Quick Hit #108 Abusing Customizable Selects | CSS-Tricks Quick Hit #107 The Value of z-index | CSS-Tricks Quick Hit #106 The Different Ways to Select <html> in CSS Quick Hit #105 Popover API or Dialog API: Which to Choose? Quick Hit #104 What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More Yet Another Way to Center an (Absolute) Element An Exploit ... in CSS?! Quick Hit #103 A Complete Guide to Bookmarklets Quick Hit #102 Loading Smarter: SVG vs. Raster Loaders in Modern Web Design Potentially Coming to a Browser :near() You Quick Hit #101 Distinguishing "Components" and "Utilities" in Tailwind Quick Hit #100 Spiral Scrollytelling in CSS With sibling-index() Interop 2026 Quick Hit #99 What’s !important #5: Lazy-loading iframes, Repeating corner-shape Backgrounds, and More Quick Hit #98 Making a Responsive Pyramidal Grid With Modern CSS Approximating contrast-color() With Other CSS Features Quick Hit #97 Trying to Make the Perfect Pie Chart in CSS Quick Hit #96 Quick Hit #95 CSS Bar Charts Using Modern Functions Quick Hit #94 No Hassle Visual Code Theming: Publishing an Extension Quick Hit #93
Drag and Drop File Uploading
CSS-Tricks · 2015-11-28 · via CSS-Tricks

I work on an RSS reader app called Readerrr (editor’s note: link removed as site seems dead). I wanted to enrich the feed import experience by making allowing for drag and drop file upload alongside the traditional file input. Sometimes drag and drop is a more comfortable way to select a file, isn’t it?

View Demo

Markup

This markup doesn’t have anything specifically to do with drag and drop. It’s just a normal, functional <form>, albeit with some extra HTML elements for potential states.

<form class="box" method="post" action="" enctype="multipart/form-data">
  <div class="box__input">
    <input class="box__file" type="file" name="files[]" id="file" data-multiple-caption="{count} files selected" multiple />
    <label for="file"><strong>Choose a file</strong><span class="box__dragndrop"> or drag it here</span>.</label>
    <button class="box__button" type="submit">Upload</button>
  </div>
  <div class="box__uploading">Uploading…</div>
  <div class="box__success">Done!</div>
  <div class="box__error">Error! <span></span>.</div>
</form>

We’ll hide those states until we need them:

.box__dragndrop,
.box__uploading,
.box__success,
.box__error {
  display: none;
}

A little explanation:

  • Regarding states: .box__uploading element will be visible during the Ajax process of file upload (and the others will still be hidden). Then .box__success or .box__error will be shown depending on what happens.
  • input[type="file"] and label are the functional parts of the form. I wrote about styling these together in my post about customizing file inputs. In that post I also described the purpose of [data-multiple-caption] attribute. The input and label also serve as an alternative for selecting files in the standard way (or the only way if drag and drop isn’t supported).
  • .box__dragndrop will be shown if a browser supports drag and drop file upload functionality.

Feature detection

We can’t 100% rely on browsers supporting drag and drop. We should provide a fallback solution. And so: feature detection. Drag & drop file upload relies on a number of different JavaScript API’s, so we’ll need to check on all of them.

First, drag & drop events themselves. Modernizr is a library you can trust all about feature detection. This test is from there:

var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)

Next we need to check the FormData interface, which is for forming a programmatic object of the selected file(s) so they can be sent to the server via Ajax:

return 'FormData' in window;

Last, we need the DataTransfer object. This one is a bit tricky because there is no bullet-proof way to detect the availability of the object before user’s first interaction with the drag & drop interface. Not all browsers expose the object.

Ideally we’d like to avoid UX like…

  • “Drag and drop files here!”
  • [User drags and drops files]
  • “Oops just kidding drag and drop isn’t supported.”

The trick here is to check the availability of FileReader API right when the document loads. The idea behind this is that browsers that support FileReader support DataTransfer too:

'FileReader' in window

Combining the code above into self-invoking anonymous function…

var isAdvancedUpload = function() {
  var div = document.createElement('div');
  return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window;
}();

… will enable us to make an effective feature support detection:

if (isAdvancedUpload) {
  // ...
}

With this working feature detection, now we can let the users know they can drag & drop their files into our form (or not). We can style the form by adding a class to it in the case of support:

var $form = $('.box');

if (isAdvancedUpload) {
  $form.addClass('has-advanced-upload');
}
.box.has-advanced-upload {
  background-color: white;
  outline: 2px dashed black;
  outline-offset: -10px;
}
.box.has-advanced-upload .box__dragndrop {
  display: inline;
}

No problems at all if drag & drop file upload is not supported. Wsers will be able to upload files via good ol’ input[type="file"]!

Note on browser support: Microsoft Edge has a bug which stops drag and drop from working. It sounds like they are aware of it and hope to fix it. (Update: link to bug removed as the link stopped working. Now that Edge is Chromium, presumably, it’s not a problem anymore.)

Drag ‘n’ Drop

Here we go, here’s the good stuff.

This part deals with adding and removing classes to the form on the different states like when the user is dragging a file over the form. Then, catching those files when they are dropped.

if (isAdvancedUpload) {

  var droppedFiles = false;

  $form.on('drag dragstart dragend dragover dragenter dragleave drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
  })
  .on('dragover dragenter', function() {
    $form.addClass('is-dragover');
  })
  .on('dragleave dragend drop', function() {
    $form.removeClass('is-dragover');
  })
  .on('drop', function(e) {
    droppedFiles = e.originalEvent.dataTransfer.files;
  });

}
  • e.preventDefault() and e.stopPropagation() prevent any unwanted behaviors for the assigned events across browsers.
  • e.originalEvent.dataTransfer.files returns the list of files that were dropped. Soon you will see how to use the data for sending these files to the server.

Adding and removing .is-dragover when necessary enables us to visually indicate when it is safe for a user to drop the files:

.box.is-dragover {
  background-color: grey;
}

Selecting Files In a Traditional Way

Sometimes dragging & dropping files is not very comfortable way for selecting files for upload. Especially when a user is in front of a small screen size computer. Therefore it would be nice to let users choose the method they prefer. The file input and label are here to allow this. Styling them both in the way I’ve described allows us to keep the UI consistant:

Ajax Upload

There is no cross-browser way to upload dragged & dropped files without Ajax. Some browsers (IE and Firefox) do not allow setting the value of a file input, which then could be submitted to server in a usual way.

This won’t work:

$form.find('input[type="file"]').prop('files', droppedFiles);

Instead, we’ll use Ajax when the form is submitted:

$form.on('submit', function(e) {
  if ($form.hasClass('is-uploading')) return false;

  $form.addClass('is-uploading').removeClass('is-error');

  if (isAdvancedUpload) {
    // ajax for modern browsers
  } else {
    // ajax for legacy browsers
  }
});

The .is-uploading class does double duty: it prevents the form from being submitted repeatedly (return false) and helps to indicate to a user that the submission is in progress:

.box.is-uploading .box__input {
  visibility: none;
}
.box.is-uploading .box__uploading {
  display: block;
}

Ajax for modern browsers

If this was a form without a file upload, we wouldn’t need to have two different Ajax techniques. Unfortunately, file uploading via XMLHttpRequest on IE 9 and below is not supported.

To distinguish which Ajax method will work, we can use our existing isAdvancedUpload test, because the browsers which support the stuff I wrote before, also support file uploading via XMLHttpRequest. Here’s code that works on IE 10+:

if (isAdvancedUpload) {
  e.preventDefault();

  var ajaxData = new FormData($form.get(0));

  if (droppedFiles) {
    $.each( droppedFiles, function(i, file) {
      ajaxData.append( $input.attr('name'), file );
    });
  }

  $.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    data: ajaxData,
    dataType: 'json',
    cache: false,
    contentType: false,
    processData: false,
    complete: function() {
      $form.removeClass('is-uploading');
    },
    success: function(data) {
      $form.addClass( data.success == true ? 'is-success' : 'is-error' );
      if (!data.success) $errorMsg.text(data.error);
    },
    error: function() {
      // Log the error, show an alert, whatever works for you
    }
  });
}
  • FormData($form.get(0)) collects data from all the form inputs
  • The $.each() loop runs through the dragged & dropped files. ajaxData.append() adds them to the data stack which will be submitted via Ajax
  • data.success and data.error are a JSON format answer which will be returned from the server. Here’s what that would be like in PHP:
<?php
  // ...
  die(json_encode([ 'success'=> $is_success, 'error'=> $error_msg]));
?>

Ajax for legacy browsers

This is essentially for IE 9-. We do not need to collect the dragged & dropped files because in this case (isAdvancedUpload = false), the browser does not support drag & drop file upload and the form relies only on the input[type="file"].

Strangely enough, targeting the form on a dynamically inserted iframe does the trick:

if (isAdvancedUpload) {
  // ...
} else {
  var iframeName  = 'uploadiframe' + new Date().getTime();
    $iframe   = $('<iframe name="' + iframeName + '" style="display: none;"></iframe>');

  $('body').append($iframe);
  $form.attr('target', iframeName);

  $iframe.one('load', function() {
    var data = JSON.parse($iframe.contents().find('body' ).text());
    $form
      .removeClass('is-uploading')
      .addClass(data.success == true ? 'is-success' : 'is-error')
      .removeAttr('target');
    if (!data.success) $errorMsg.text(data.error);
    $form.removeAttr('target');
    $iframe.remove();
  });
}

Automatic Submission

If you have a simple form with only a drag & drop area or file input, it may be a user convenience to avoid requiring them to press the button. Instead, you can automatically submit the form on file drop/select by triggering the submit event:

// ...

.on('drop', function(e) { // when drag & drop is supported
  droppedFiles = e.originalEvent.dataTransfer.files;
  $form.trigger('submit');
});

// ...

$input.on('change', function(e) { // when drag & drop is NOT supported
  $form.trigger('submit');
});

If drag & drop area is visually well-designed (it’s obvious to the user what to do), you might consider hiding the submit button (less UI can be good). But be careful when hiding a control like that. The button should be visible and functional if for some reason JavaScript is not available (progressive enhancement!). Adding a .no-js class name to and removing it with JavaScript will do the trick:

<html class="no-js">
  <head>
    <!-- remove this if you use Modernizr -->
    <script>(function(e,t,n){var r=e.querySelectorAll("html")[0];r.className=r.className.replace(/(^|\s)no-js(\s|$)/,"$1js$2")})(document,window,0);</script>
  </head>
</html>
.box__button {
  display: none;
}
.no-js .box__button {
  display: block;
}

Displaying the Selected Files

If you’re not going to do auto-submission there should be an indication to the user if they have selected files successfully:

var $input    = $form.find('input[type="file"]'),
    $label    = $form.find('label'),
    showFiles = function(files) {
      $label.text(files.length > 1 ? ($input.attr('data-multiple-caption') || '').replace( '{count}', files.length ) : files[ 0 ].name);
    };

// ...

.on('drop', function(e) {
  droppedFiles = e.originalEvent.dataTransfer.files; // the files that were dropped
  showFiles( droppedFiles );
});

//...

$input.on('change', function(e) {
  showFiles(e.target.files);
});

When JavaScript Is Not Available

Progressive enhancement is about the idea that a user should be able to complete the principal tasks on a website no matter what. File uploading is no exception. If for some reason JavaScript is not available, the interface will look like this:

The page will refresh on form submission. Our JavaScript for indicating the result of submission is useless. That means we have to rely on server-side solution. Here’s how it looks and works in the demo page:

<?php

  $upload_success = null;
  $upload_error = '';

  if (!empty($_FILES['files'])) {
    /*
      the code for file upload;
      $upload_success – becomes "true" or "false" if upload was unsuccessful;
      $upload_error – an error message of if upload was unsuccessful;
    */
  }

?>

And some adjustments for the markup:

<form class="box" method="post" action="" enctype="multipart/form-data">

  <?php if ($upload_success === null): ?>

  <div class="box__input">
    <!-- ... -->
  </div>

  <?php endif; ?>

  <!-- ... -->

  <div class="box__success"<?php if( $upload_success === true ): ?> style="display: block;"<?php endif; ?>>Done!</div>
  <div class="box__error"<?php if( $upload_success === false ): ?> style="display: block;"<?php endif; ?>>Error! <span><?=$upload_error?></span>.</div>

</form>

That’s it! This already-long article could have been even longer, but I think this will get you going with a responsible drag and drop file upload feature on your own projects.

Check out the demo for more (view source to see the no-jQuery-dependency JavaScript):

View Demo