While working on my side project, NotchTools.com, I needed a drag-and-drop file uploader for Image Tools. 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:
The drop area is styled using DaisyUI classes. The dragActive
state changes the border color when a file is being dragged over the area.
<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), 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;
Thank you :)
Top comments (0)