Creating a Drag & Drop File Uploader in React (Next.js)
While working on my side project, NotchTools.com, I needed a drag-and-drop file uploader. I’m using DaisyUI, a Tailwind-based component library, for styling throughout the project. However, DaisyUI does not provide a pre-built file upload component with drag-and-drop functionality. So, I decided to create my own sharable and reusable component that fits seamlessly with the Tailwind/DaisyUI design philosophy.
Key Features:
- Drag & Drop Support: Allows users to upload files by dragging them into the upload area.
- Customizable File Acceptance: You can specify the types of files allowed, whether to support single or multiple uploads.
- Error Handling: Proper error messages are displayed for invalid file types or exceeding the file limit.
Component Props:
- onFileSelect: A callback function that triggers when files are selected either by dragging or clicking to upload.
- multiple: A boolean flag (optional, defaults to
false
) to allow multiple file uploads. - accept: A string (optional) specifying the accepted file types (e.g.,
image/*
). - className: Optional custom classes for styling the main upload area.
Code Walkthrough:
1. State Management:
The component uses two key states:
dragActive
: A boolean that tracks whether a drag event is active (i.e., when a user is dragging a file over the upload area).error
: Stores error messages related to invalid file types or if too many files are uploaded.
const [dragActive, setDragActive] = useState(false);
const [error, setError] = useState<string | null>(null);
2. Drag Events Handling:
For the drag-and-drop functionality, the component listens to the following drag events:
- onDragEnter and onDragOver: To detect when the file is being dragged over the area.
- onDragLeave: To reset the state when the file is dragged out of the area.
- onDrop: To handle the actual file drop and trigger validation.
The handleDrag
function toggles the dragActive
state based on the event type to visually update the component.
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(e.type === "dragenter" || e.type === "dragover");
};
3. File Drop Validation:
When files are dropped, the handleDrop
function runs several validations:
- Multiple File Check: If
multiple
isfalse
, the component only allows one file to be uploaded at a time. - File Type Validation: Checks whether the dropped file(s) match the accepted file types using the
accept
prop.
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
const files = e.dataTransfer.files;
if (!files.length) return;
if (!multiple && files.length > 1) {
setError("Only one file is allowed");
return;
}
const acceptedTypes = accept ? accept.split(",") : [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const isValidFileType = acceptedTypes.some((type) => {
const regex = new RegExp(type.replace("*", ".*"));
return regex.test(file.type);
});
if (accept && !isValidFileType) {
setError(`Invalid file type: ${file.name}`);
return;
}
}
setError(null);
onFileSelect(files);
};
4. Fallback for File Input:
For users who prefer to upload files by clicking, a hidden file input field is provided. The triggerFileSelect
function programmatically opens the file dialog when the upload area is clicked.
const triggerFileSelect = () => {
inputRef.current?.click();
};
5. Tailwind & DaisyUI Integration:
The drop area is styled using Tailwind CSS classes. The dragActive
state dynamically changes the border color when a file is being dragged over the area to provide a better user experience. This design integrates smoothly with DaisyUI, following its utility-first approach.
<div
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
className={`${className} border-2 border-dashed p-6 rounded-lg text-center cursor-pointer transition-colors ${
dragActive ? "border-primary" : "border-base-50 bg-base-200"
}`}
onClick={triggerFileSelect}
>
<input
ref={inputRef}
type="file"
className="hidden"
multiple={multiple}
accept={accept}
onChange={handleChange}
/>
<p className="text-base-content/50">
{multiple
? "Drag & Drop files here or click to upload multiple files"
: "Drag & Drop a file here or click to upload"}
</p>
</div>
6. Error Messaging:
If there is an issue with the uploaded files (e.g., invalid type or exceeding file limit), an error message is displayed beneath the upload area.
{
error && <p className="text-xs text-error mt-2">{error}</p>;
}
Full Component Code:
Here I’m using next.js for the project.
"use client";
import React, { useState, useRef } from "react";
interface FileUploadProps {
onFileSelect: (files: FileList) => void;
multiple?: boolean;
accept?: string;
className?: string;
}
const FileUpload: React.FC<FileUploadProps> = ({
onFileSelect,
multiple = false,
accept,
className,
}) => {
const [dragActive, setDragActive] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const [error, setError] = useState<string | null>(null);
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(e.type === "dragenter" || e.type === "dragover");
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
const files = e.dataTransfer.files;
// Check if any files are dropped
if (!files.length) {
return;
}
// If multiple is false and more than one file is dropped
if (!multiple && files.length > 1) {
setError("Only one file is allowed");
return;
}
// Validate file types
const acceptedTypes = accept ? accept.split(",") : [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Check if the file type matches the accept prop (using regex or exact match)
const isValidFileType = acceptedTypes.some((type) => {
const regex = new RegExp(type.replace("*", ".*")); // Convert 'image/*' to 'image/.*'
return regex.test(file.type);
});
if (accept && !isValidFileType) {
setError(`Invalid file type: ${file.name}`);
return;
}
}
// If everything is valid, reset error and call the onFileSelect function
setError(null);
onFileSelect(files);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
onFileSelect(e.target.files);
}
};
const triggerFileSelect = () => {
inputRef.current?.click();
};
return (
<>
<div
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
className={`${className} border-2 border-dashed p-6 rounded-lg text-center cursor-pointer transition-colors ${
dragActive ? "border-primary" : "border-base-50 bg-base-200"
}`}
onClick={triggerFileSelect}
>
<input
ref={inputRef}
type="file"
className="hidden"
multiple={multiple}
accept={accept}
onChange={handleChange}
/>
<p className="text-base-content/50">
{multiple
? "Drag & Drop files here or click to upload multiple files"
: "Drag & Drop a file here or click to upload"}
</p>
{/* {accept && (
<p className="text-xs text-gray-400 mt-2">
Accepted file types: {accept}
</p>
)} */}
</div>
{error && <p className="text-xs text-error mt-2">{error}</p>}
</>
);
};
export default FileUpload;
Conclusion:
Creating a drag-and-drop file uploader component was essential for my project, NotchTools.com, since the default DaisyUI library didn’t offer this functionality. This component integrates seamlessly with Tailwind CSS, and it can be reused across different projects where file uploading is required.
Potential Enhancements:
- File Preview: Add a feature to preview files before they are uploaded.
- Progress Indicator: Add a progress bar to improve UX for larger file uploads.