BlogReactJS

Creating a ReactJS drag and drop file upload component

Written by Codemzy on March 2nd, 2022

Do you need to create a drag and drop file upload component in React? This blog post has you covered. Build a component that listens to drag events, handles drops, and falls back to the traditional file picker.

Drag and drop is pretty cool, and your users think so too! It's probably something they have come to expect.

This blog post is about drag and drop file uploads. If you want to drag and drop components around instead (like to re-order a list) check out my reusable drag-and-drop component for React.

Do you want to share a file on WeTransfer? You can drag and drop to upload it. Do you need to upload a video to YouTube? You can use drag and drop to upload it.

Here's what we are going to build:

See the Pen ReactJS Drag Drop File Upload by Codemzy (@codemzy) on CodePen.

It does two things.

  1. Allows users to drag and drop a file
  2. Allows users to click and pick a file in the traditional way

To keep things simple, we're not going to use any other libraries for this, just ReactJS and JavaScript.

Let's get started.

Build The Component

Under the hood, were going to use the native file input.

<input> elements with type="file" let the user choose one or more files from their device storage. Once chosen, the files can be uploaded to a server using form submission, or manipulated using JavaScript code and the File API.

- MDN Web Docs <input type="file">

For this example, I'm allowing any type of file. And multiple files. In the real world, you might want to limit the file type. You can do this by adding the accept attribute. For example if you want to only allow certain image files accept="image/jpeg, image/jpg, image/png".

// drag drop file component
function DragDropFile() {
  return (
    <form id="form-file-upload">
      <input type="file" id="input-file-upload" multiple={true} />
      <label id="label-file-upload" htmlFor="input-file-upload">
        <div>
          <p>Drag and drop your file here or</p>
          <button className="upload-button">Upload a file</button>
        </div> 
      </label>
    </form>
  );
};

But we don't want our users to see that file input, because we don't live in the olden days. And we have no control over how each browser displays it. And you don't want your app to look ugly! So we need to add some CSS to hide it.

As well as hiding the file input, I've created a label for it. This label will take over the entire form (once we add the CSS) so that any click anywhere on the drag and drop UI will open up the file picker.

I've also added a button to upload a file with the traditional browser file picker. This helps the experience by giving the user something to click to upload, and also if they are navigating using the keyboard. It's not wired up yet, but we'll get to that shortly.

Here's the CSS for what we have so far:

#form-file-upload {
  height: 16rem;
  width: 28rem;
  max-width: 100%;
  text-align: center;
  position: relative;
}

#input-file-upload {
  display: none;
}

#label-file-upload {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border-width: 2px;
  border-radius: 1rem;
  border-style: dashed;
  border-color: #cbd5e1;
  background-color: #f8fafc;
}

.upload-button {
  cursor: pointer;
  padding: 0.25rem;
  font-size: 1rem;
  border: none;
  font-family: 'Oswald', sans-serif;
  background-color: transparent;
}

.upload-button:hover {
  text-decoration-line: underline;
}

The Drag

Next, let's get the drag and drop working. It's the main aim of this component, after all!

If you drag and drop now, nothing will happen. We can change that by listening for drag events.

First, we need to know if the user is dragging something into the component. We're going to do this by adding a drag listener to our form onDragEnter={handleDrag} and a handleDrag function. We will also add some state dragActive to keep track of when the user is dragging over our component.

// drag drop file component
function DragDropFile() {
  // drag state
  const [dragActive, setDragActive] = React.useState(false);
  
  // handle drag events
  const handleDrag = function(e) {
    e.preventDefault();
    e.stopPropagation();
    if (e.type === "dragenter" || e.type === "dragover") {
      setDragActive(true);
    } else if (e.type === "dragleave") {
      setDragActive(false);
    }
  };
  
  return (
    <form id="form-file-upload" onDragEnter={handleDrag} onSubmit={(e) => e.preventDefault()}>
      <input type="file" id="input-file-upload" multiple={true} />
      <label id="label-file-upload" htmlFor="input-file-upload" className={dragActive ? "drag-active" : "" }>
        <div>
          <p>Drag and drop your file here or</p>
          <button className="upload-button">Upload a file</button>
        </div> 
      </label>
    </form>
  );
};

And a bit of CSS so that when we drag over the form, the background changes to white.

#label-file-upload.drag-active {
  background-color: #ffffff;
}

You might notice that our handle drag event also covers dragover and dragleave events, but we're so far only listening to dragenter events. Before we listen to those other events, there's a little gotcha here to be aware of.

You might think, "I'll just add those listeners on the form". That's certainly what I tried at first. The problem is, there are other elements inside the form. And when the drag goes over those elements, a dragleave event is triggered, and our white background starts flickering and the whole thing is a mess!

To get around this issue, when dragActive is true you can add an invisible element to cover the entire form. This then listens to the events without interference from any other elements. And this can also handle the drop.

      //...
      </label>
      { dragActive && <div id="drag-file-element" onDragEnter={handleDrag} onDragLeave={handleDrag} onDragOver={handleDrag} onDrop={handleDrop}></div> }
    </form>
  );
};

And the extra CSS needed:

#drag-file-element {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 1rem;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
}

The Drop

Ok, now we have this onDrop={handleDrop} we need a handleDrop function.

// triggers when file is dropped
const handleDrop = function(e) {
  e.preventDefault();
  e.stopPropagation();
  setDragActive(false);
  if (e.dataTransfer.files && e.dataTransfer.files[0]) {
    // at least one file has been dropped so do something
    // handleFiles(e.dataTransfer.files);
  }
};

This function resets the dragActive state to false because the drag has ended, and checks if at least one file has been dropped, so it can do something with it. What it does is up to you, but you would probably send it back to your server or some object storage.

The Click

And finally, we need to wire up what happens if that button or label gets clicked, and our user selects a file the old fashioned way!

To do that we, can add an onChange listener to our hidden input.

<input type="file" id="input-file-upload" multiple={true} onChange={handleChange} />

And a handleChange function, similar to our drop function.

// triggers when file is selected with click
const handleChange = function(e) {
  e.preventDefault();
  if (e.target.files && e.target.files[0]) {
    // at least one file has been selected so do something
    // handleFiles(e.target.files);
  }
};

Now, because our label covers the entire form, a click anywhere on the form will activate the input and open up the file picker.

We don't need a button inside the label, if you remove the button, it all works great for mouse and touchscreen users.

But you can't tab and focus on a label with the keyboard. So there's currently no way to activate the file picker if you are navigating the page using a keyboard.

If you do want a button for keyboard users, here's what you can do.

First, add a ref to the input using the useRef hook.

// ref
const inputRef = React.useRef(null);
//...
// add the ref to the input
<input ref={inputRef} type="file" id="input-file-upload" multiple={true} onChange={handleChange} />

And next, add an onClick function to the button, that will click the input when the button is clicked.

// triggers the input when the button is clicked
const onButtonClick = () => {
  inputRef.current.click();
};
//...
// add the onClick to the button
<button className="upload-button" onClick={onButtonClick}>Upload a file</button>

The Drag & Drop Component

Ok, phew, it's done! Let's recap and look at the final code.

Our drag and drop component:

  1. Uses a hidden file input
  2. Has a large label to trigger the file input
  3. Listens for drag and drop events
  4. Also has a button for keyboard users

React JS

// drag drop file component
function DragDropFile() {
  // drag state
  const [dragActive, setDragActive] = React.useState(false);
  // ref
  const inputRef = React.useRef(null);
  
  // handle drag events
  const handleDrag = function(e) {
    e.preventDefault();
    e.stopPropagation();
    if (e.type === "dragenter" || e.type === "dragover") {
      setDragActive(true);
    } else if (e.type === "dragleave") {
      setDragActive(false);
    }
  };
  
  // triggers when file is dropped
  const handleDrop = function(e) {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(false);
    if (e.dataTransfer.files && e.dataTransfer.files[0]) {
      // handleFiles(e.dataTransfer.files);
    }
  };
  
  // triggers when file is selected with click
  const handleChange = function(e) {
    e.preventDefault();
    if (e.target.files && e.target.files[0]) {
      // handleFiles(e.target.files);
    }
  };
  
// triggers the input when the button is clicked
  const onButtonClick = () => {
    inputRef.current.click();
  };
  
  return (
    <form id="form-file-upload" onDragEnter={handleDrag} onSubmit={(e) => e.preventDefault()}>
      <input ref={inputRef} type="file" id="input-file-upload" multiple={true} onChange={handleChange} />
      <label id="label-file-upload" htmlFor="input-file-upload" className={dragActive ? "drag-active" : "" }>
        <div>
          <p>Drag and drop your file here or</p>
          <button className="upload-button" onClick={onButtonClick}>Upload a file</button>
        </div> 
      </label>
      { dragActive && <div id="drag-file-element" onDragEnter={handleDrag} onDragLeave={handleDrag} onDragOver={handleDrag} onDrop={handleDrop}></div> }
    </form>
  );
};

CSS

#form-file-upload {
  height: 16rem;
  width: 28rem;
  max-width: 100%;
  text-align: center;
  position: relative;
}

#input-file-upload {
  display: none;
}

#label-file-upload {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border-width: 2px;
  border-radius: 1rem;
  border-style: dashed;
  border-color: #cbd5e1;
  background-color: #f8fafc;
}

#label-file-upload.drag-active {
  background-color: #ffffff;
}

.upload-button {
  cursor: pointer;
  padding: 0.25rem;
  font-size: 1rem;
  border: none;
  font-family: 'Oswald', sans-serif;
  background-color: transparent;
}

.upload-button:hover {
  text-decoration-line: underline;
}

#drag-file-element {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 1rem;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
}

Of course, this is just your front end code. Once the files get dropped, you will need to do something with them. You might want to use a HTTP client like axios or fetch to send the files back to your server and upload them somewhere, for example.

And although this post was just focused on the drag and drop functionality, you probably also want to do some validation on your server to stop invalid or malicious files. You might even want to process the files, for example, I use sharp to reduce image sizes before storing them.