Lightweight JavaScript Drag & Drop File Upload Library


Collect files from drag-and-drop or a browse dialog in plain HTML and JavaScript. hcg-file-drop is a zero-dependency widget with type filtering, a configurable file limit, duplicate detection, an optional removable file list, and a disabled state API - without React, jQuery, or any other dependency.

On this page you will find a live demo, installation (direct download, npm, CDN), a full options and API reference, React usage notes, and copy-paste examples for uploading with fetch or XMLHttpRequest.

What is hcg-file-drop?

hcg-file-drop is a vanilla JavaScript widget for drag-and-drop file selection. Pass a container element and an options object; the library renders a focusable drop zone, optional file list, and returns the original File objects through onFilesChange and getFiles() - without React, jQuery, or any other dependency.

Why use hcg-file-drop?

Building a reliable drag-and-drop upload field by hand means juggling drag events, keyboard access, type validation, and DOM rendering. Use hcg-file-drop when you want a small, script-tag-friendly widget instead of a heavy upload plugin or framework wrapper.

The widget does not upload files for you. It keeps the original File objects so you can send them with FormData and fetch on your own schedule. See the comparison table for how it differs from a plain file input and larger upload libraries.

Live Demo

Drop files onto the zone below or click to browse. Accepted types: image/*, .pdf, .txt, .json. Maximum 5 files. Duplicates are skipped. Use Reset to clear the list or Disable to toggle the disabled state. For every option on one page, open the hcg-file-drop demo page.

Upload files to see onFilesChange output here.

Comparison Table

How hcg-file-drop compares with common alternatives.

Feature hcg-file-drop Plain file input Heavy upload plugins
DependenciesNoneNoneOften jQuery or a framework
Drag and dropYesNoYes
Type filteringYes (MIME, wildcard, extension)Limited (accept attribute)Yes
Duplicate detectionYesNoSometimes
Removable file listBuilt inNoVaries
Disabled state APIdisable() / enable()Attribute onlyVaries
Uploads to serverYou control uploadYou control uploadOften built in
Approximate sizeOne small JS file plus CSSn/aLarge
TypeScriptDefinitions includedn/aVaries
License / costFree, MITn/aVaries

Choose hcg-file-drop when you want a tiny vanilla widget that collects File objects and leaves upload logic to your app. Use a heavier plugin when you need chunked uploads, cloud storage adapters, or a full UI framework out of the box.

Features

Everything included in the library at a glance:

  • Zero dependencies - one small JavaScript file and a companion CSS file.
  • Script tag or bundler - works with a plain script tag, CommonJS, or ESM.
  • Drag and drop or click to browse - both input methods work out of the box.
  • Type filtering - accept exact MIME types, wildcards, or file extensions.
  • Single or multiple - allow many files or restrict to one with the multiple option.
  • File limit - cap the number of files held at once (default 10).
  • Append or replace - keep previous files on each new drop, or replace them with replaceOnDrop.
  • Duplicate detection - files with the same name and size are skipped.
  • Optional file list - render a removable list below the drop zone or in a custom target.
  • Disabled state - start disabled, or toggle at runtime with disable() and enable().
  • Accessible - focusable zone with Enter and Space support.
  • Multiple instances - independent widgets on the same page.
  • TypeScript - definitions ship with the package.
  • React-friendly - create in useEffect and call destroy() on unmount.

Installation

Direct download

Source code and package:

Download hcg-file-drop.js and hcg-file-drop.css, then include both on your page:

HTML
<link rel="stylesheet" href="hcg-file-drop.css">
<script src="hcg-file-drop.js"></script>
npm

Install from npm:

Bash
npm install hcg-file-drop

CommonJS (require):

JavaScript
const hcgFileDrop = require("hcg-file-drop");
// also load hcg-file-drop.css however your bundler handles CSS

ESM (import):

JavaScript
import hcgFileDrop from "hcg-file-drop";
CDN usage

Load from a CDN with one script and one stylesheet - no npm or bundler required. The global hcgFileDrop function is available as soon as the script finishes loading.

jsDelivr:

HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hcg-file-drop@1/hcg-file-drop.css">
<script src="https://cdn.jsdelivr.net/npm/hcg-file-drop@1/hcg-file-drop.js"></script>

unpkg:

HTML
<link rel="stylesheet" href="https://unpkg.com/hcg-file-drop@1/hcg-file-drop.css">
<script src="https://unpkg.com/hcg-file-drop@1/hcg-file-drop.js"></script>

Replace @1 or @1.0.0 with the release you want. You can also self-host by uploading hcg-file-drop.js and hcg-file-drop.css to your own server.

Basic Usage

Install hcg-file-drop, add a container element, and pass an options object. The widget renders itself inside the element and returns an instance you can call later.

Initialize the widget

Pass a container element and options. The drop zone and optional file list are injected automatically.

HTML
<link rel="stylesheet" href="hcg-file-drop.css">
<div id="upload-container"></div>

<script src="hcg-file-drop.js"></script>
<script>
  const uploader = hcgFileDrop(
    document.getElementById('upload-container'),
    {
      allowedTypes: ['image/*', '.pdf', '.txt', '.json'],
      maxFiles: 5,
      showFileList: true,
      onFilesChange: function (files) {
        console.log(files);
      }
    }
  );
</script>
File entry shape

Each entry passed to onFilesChange and returned from getFiles() has this shape:

JavaScript
{
  file,   // the original File object
  name,   // file name
  type,   // MIME type reported by the browser
  size    // size in bytes
}

The original File object is kept on the file property, so you can upload it yourself with FormData and fetch whenever you are ready.

Instance methods

Store the returned instance and call its methods whenever you need them.

JavaScript
const files = uploader.getFiles();
uploader.reset();
uploader.disable();
uploader.isDisabled(); // true
uploader.enable();
uploader.destroy();
console.log(hcgFileDrop.version);

Uploading Selected Files

The widget does not upload by itself. Read files with getFiles(), append each entry.file to a FormData, and send it to your server.

Using fetch
JavaScript
function uploadFiles() {
  const files = uploader.getFiles();
  if (files.length === 0) return;

  const formData = new FormData();
  files.forEach(function (entry) {
    formData.append('files[]', entry.file, entry.name);
  });

  fetch('/upload', { method: 'POST', body: formData })
    .then(function (res) {
      if (!res.ok) throw new Error('Upload failed: ' + res.status);
      return res.json();
    })
    .then(function (data) {
      console.log('Upload complete', data);
      uploader.reset();
    })
    .catch(function (err) {
      console.error(err);
    });
}
Using XMLHttpRequest

Use XMLHttpRequest when you need an upload progress indicator.

JavaScript
function uploadFilesAjax() {
  const files = uploader.getFiles();
  if (files.length === 0) return;

  const formData = new FormData();
  files.forEach(function (entry) {
    formData.append('files[]', entry.file, entry.name);
  });

  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload', true);

  xhr.upload.onprogress = function (e) {
    if (e.lengthComputable) {
      const percent = Math.round((e.loaded / e.total) * 100);
      console.log('Uploaded ' + percent + '%');
    }
  };

  xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('Upload complete', xhr.responseText);
      uploader.reset();
    } else {
      console.error('Upload failed: ' + xhr.status);
    }
  };

  xhr.onerror = function () {
    console.error('Network error during upload');
  };

  xhr.send(formData);
}

Both examples send all files in one request under the files[] field. To upload a single file, append just files[0].file instead.

Using with React

Import hcgFileDrop from the package and create the widget inside useEffect. Call destroy() in the cleanup function when the component unmounts.

Try the live React demo on StackBlitz

Uploader component
JavaScript
import { useEffect, useRef } from "react";
import hcgFileDrop from "hcg-file-drop";
import "hcg-file-drop/hcg-file-drop.css";

function Uploader({ onFilesChange }) {
  const ref = useRef(null);

  useEffect(() => {
    const instance = hcgFileDrop(ref.current, {
      allowedTypes: ['image/*', '.pdf'],
      maxFiles: 5,
      showFileList: true,
      onFilesChange
    });
    return () => instance.destroy();
  }, [onFilesChange]);

  return <div ref={ref}></div>;
}

export default Uploader;

Load hcg-file-drop.css once in your app entry or import it in the component. Use disable() and enable() from props or effects when you need to lock the widget during async work.

Multiple Instances

Create as many drop zones on a page as you need. Each call to hcgFileDrop() returns a fully independent instance with its own files, options, event listeners, and API.

HTML
<link rel="stylesheet" href="hcg-file-drop.css">

<div id="avatars"></div>
<div id="documents"></div>

<script src="hcg-file-drop.js"></script>
<script>
  const avatars = hcgFileDrop(document.getElementById('avatars'), {
    allowedTypes: ['image/*'],
    maxFiles: 1,
    showFileList: true,
    onFilesChange: function (files) { console.log('avatar:', files); }
  });

  const docs = hcgFileDrop(document.getElementById('documents'), {
    allowedTypes: ['.pdf', '.docx'],
    maxFiles: 10,
    showFileList: true,
    onFilesChange: function (files) { console.log('docs:', files); }
  });

  // avatars.disable();
  // docs.getFiles();
</script>
  • If you use fileListTarget, give each instance a different target element.
  • preventWindowDrop acts page-wide, so you only need it on one instance.
  • Call each instance's destroy() when removing it from the DOM.

Styling and Customization

All default styles live in hcg-file-drop.css, using plain single-class selectors. Edit that file directly, or override any rule from your own stylesheet loaded after it.

  • .hcg-dragdrop-zone - the drop zone (.hcg-dragdrop-zone--active while dragging over, .hcg-dragdrop-zone--disabled when disabled)
  • .hcg-dragdrop-hint - the hint text inside the zone
  • .hcg-dragdrop-input - the hidden file input
  • .hcg-dragdrop-list / .hcg-dragdrop-item - the optional file list and its rows
  • .hcg-dragdrop-name, .hcg-dragdrop-meta, .hcg-dragdrop-remove - parts of each row
CSS
.hcg-dragdrop-zone { border-color: #10b981; background: #ecfdf5; }
.hcg-dragdrop-zone--active { border-color: #059669; }
.hcg-dragdrop-item { border-radius: 6px; }

Options Reference

Pass these as the second argument to hcgFileDrop(element, options).

OptionTypeDefaultDescription
allowedTypesstring[] | string[]Accepted MIME types or dot-extensions, as an array or comma-separated string (e.g. 'image/*, .pdf'). Supports image/png, image/*, and .pdf. Empty accepts all.
maxFilesnumber10Maximum number of files held at once. Forced to 1 when multiple is false.
multiplebooleantrueAllow selecting multiple files. When false, only a single file is accepted and each new selection replaces the current one.
replaceOnDropbooleanfalseWhen false, new files are appended. When true, each new drop clears the old files and keeps only the new ones.
onFilesChangefunction-Called whenever the accepted file list changes (add, remove, or reset).
showFileListbooleanfalseRender a removable file list below the drop zone.
fileListTargetHTMLElement | string-Where to render the file list - an element, id, or CSS selector. Implies showFileList. Defaults to inside the widget element.
disabledbooleanfalseStart the widget in a disabled state.
preventWindowDropbooleanfalseStop the browser from navigating to a file dropped outside the zone. While enabled it also suppresses other drop targets on the page.

Duplicate files (same name and size) are skipped automatically.

API Reference

hcgFileDrop(element, options) returns an instance with these methods. The factory also exposes hcgFileDrop.version.

MethodReturnsDescription
getFiles()arrayShallow copy of the current accepted file entries.
reset()voidClears file state, empties the list, and fires onFilesChange.
enable()voidEnables interaction (drag, click, browse, remove).
disable()voidDisables interaction and dims the drop zone.
isDisabled()booleanWhether the widget is currently disabled.
destroy()voidRemoves all event listeners and injected DOM nodes.
onFilesChange callback

The widget reports changes through onFilesChange rather than custom DOM events. It fires when files are added by drop or browse, when a file is removed, and when reset() clears the list.

JavaScript
hcgFileDrop(container, {
  onFilesChange: function (files) {
    if (files.length === 0) {
      console.log('No files selected');
      return;
    }
    files.forEach(function (f) {
      console.log(f.name, f.type, f.size);
    });
  }
});

Accessibility

The drop zone is a focusable role="button" element with an aria-label. Users can press Enter or Space to open the file browser. When disabled, aria-disabled is set and the zone is removed from the tab order.

Pair the widget with visible labels and error messages so users know which files are accepted and what happens after selection. Do not rely on drag-and-drop alone for critical workflows.

Browser Support

Works in all modern browsers that support the File and Drag-and-Drop APIs, including the latest versions of Chrome, Firefox, Safari, and Edge. No polyfills are required.

License

Released under the MIT License. Free for personal and commercial use. Copyright HTML Code Generator.

Frequently asked questions

Does this drag and drop upload library have any dependencies?

No. It is written in plain vanilla JavaScript with zero dependencies. You only need a single script file and the companion CSS stylesheet, which works with a script tag, CommonJS, or a bundler.

Does the library upload files to a server automatically?

No. The widget collects the selected File objects and reports them through the onFilesChange callback and getFiles() method. You keep full control over how and where to upload them, for example with FormData and fetch.

How does duplicate detection work?

When a file is added, the widget compares its name and size against the files already in the list. Any file that matches an existing entry is skipped automatically.

Can the widget be disabled and re-enabled at runtime?

Yes. Call disable() to block all interaction and dim the drop zone, enable() to restore it, and isDisabled() to read the current state. You can also start disabled with the disabled option.

Does it work with React?

Yes. Create the widget inside a useEffect hook against a ref element, store the returned instance, and call destroy() in the cleanup function. See Using with React on this page.

Can I run multiple upload zones on one page?

Yes. Each call to hcgFileDrop() returns an independent instance with its own files, options, and API. Link hcg-file-drop.css once and initialize as many containers as you need.

How do I filter accepted file types?

Pass allowedTypes as an array or comma-separated string. Use exact MIME types like image/png, wildcards like image/*, or extensions like .pdf. An empty list accepts all file types.