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.
- Allows users to drag and drop a file
- 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.
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:
- Uses a hidden file input
- Has a large label to trigger the file input
- Listens for drag and drop events
- 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.