Enhancing User Experience with a Custom File Uploader in React

Learn to build a Custom File Uploader with Drag-and-Drop Feature with React.js, Tailwind CSS and Typescript.

One key way to enhance user experience on the web is by simplifying how users interact with file uploads. Traditional file input elements can feel outdated and clunky, this is where custom file uploaders with drag-and-drop functionality come into play.

In this article, we will build a simple drag-and-drop file uploader using React and Tailwind CSS. By the end of this article, we'll have a fully functional drag-and-drop file uploader that looks great and offers a seamless user experience.

Prerequisite

To follow along with this article, you'll need the following:

  • Basic knowledge of React and Javascript

  • Basic knowledge of Tailwind CSS and utility classes

  • Node.js and npm installed on your machine

  • Basic knowledge of Typescript

Let's get started.

image showing a sample of the custom uploader we are building

An image of what the custom drag-and-drop uploader will look like at the end of this article.

Setup Your Project

We assume you already have your React app created and Tailwind CSS config set up for this article. If you have yet to do that, you can find out how to do it using the resources below.

Now that you have created your React App and configured the Tailwind CSS config let's go ahead and create our custom uploader file.

In your src folder create a new folder named CustomUploader and then create a new file index.tsx

You can achieve this by typing this command into your terminal

For Windows OS:

mkdir src/CustomUploader && type nul > src/CustomUploader/index.tsx

For macOS/Linux:

mkdir -p src/CustomUploader && touch src/CustomUploader/index.tsx
💡
Make sure you are in the right project directory.

Update the Content of the index.tsx File

Now open the index.tsx file you created and then write a simple functional component like below

const CustomFileUploader = ():JSX.Element => {
   return (
    <div>
      hello world
    </div>
     )
     }

export default CustomFileUploader;

Import the CustomFileUploader component into your App.tsx file and spin up your development server. Lunch your app in your preferred browser using http://localhost:5173/ and you will see just the text hello world on your screen.

Style Up

Now let's dive deep and style the CustomFileUploader component to look just like the above image.

Replace the div in your CustomFileUploader.tsx with the below code

     <div className="flex flex-col gap-2">
        <label htmlFor="fileUpload" className="text-lg text-black">
          Upload Resume
        </label>
        <div className="py-12 px-4 bg-white border-2 border-dashed rounded-md cursor-pointer">
          <p className="text-md text-gray-600 text-center">Drag & drop your files here or 
          <span className="underline font-semibold cursor-pointer">browse</span>
          </p>
        </div>
      </div>

The Input Element

Next, we will add the HTML input element to our "border-dashed" div . Add the code snippet below to the CustomFileUploader component.

 <input
     id="fileUpload"
     type="file"
     accept=".pdf,.doc,.docx"
     className="hidden"
  />
💡
You will notice that our input element has a class of hidden and an id attribute attached to it.

Here are a few reasons why;

  • Label Association: The id allows you to associate the input with a <label> element. This makes the input more accessible by enabling users to click on the label to trigger the file input, rather than having to click directly on the input element, which is often styled as hidden.

  • Javascript/React interaction: The id also allows you to easily target and reference this specific input element in your JavaScript or React code, for instance, when accessing the DOM directly using document.getElementById('fileInput') , which we will get to in a moment.

Your CustomFileUploader component should look like this by now.

const CustomFileUploader = ():JSX.Element => {
   return (
    <div className="flex flex-col gap-2">
       <label htmlFor="fileUpload" className="text-lg text-black">
          Upload Resume
        </label>
         <div className="py-12 px-4 bg-white border-2 border-dashed rounded-md cursor-pointer">
          <input id="fileUpload" type="file" accept=".pdf,.doc,.docx" className="hidden"/>
          <p className="text-md text-gray-600 text-center">Drag & drop your files here or 
          <span className="underline font-semibold cursor-pointer">browse</span>
          </p>
        </div>
      </div>
         )
         }

export default CustomFileUploader;

Reload your browser window to see the changes we have made. You should see the dotted box, but we still can't upload a file.

Upload Functionality

Now to the fun part, let's add some functionality to our component.

First, we use the useState hook to create and manage the state of the selected file, the initial value will be set to null.

  const [selectedFile, setSelectedFile] = useState<File | null>(null);

Next, we create our handleFileChange function

This function handles the file input changes. It accepts a parameter event , an object of the type React.ChangeEvent<HTMLInputElement> representing a change event triggered by a file input element. We then extract the selected file from the event target and check if the files property is true (i.e., not null or undefined). If files is true, we then call the setSelectedFile() function passing the first file in the files array as an argument.

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = event.target.files;
    if (files) {
      setSelectedFile(files[0]);
    }
  };

We then add an onChange event handler to our input element and pass the handleFileChange function to it.

//updated input element

 <input
     id="fileInput"
     type="file"
     accept=".pdf,.doc,.docx"
     className="hidden"
     onChange={handleFileChange} // include this line 
     />

We now have a good-looking File uploader, but it still can't handle our file upload. To fix this, we introduce a function triggerFileUpload.

 const triggerFileUpload = () => {
    document.getElementById("fileInput")?.click()
  }

Let's break the triggerFileUpload function down to understand its components.

  • document.getElementById("fileInput") extracts an HTML element with the id "fileInput" from the document.

  • The ?. called the optional chaining operator. It lets you access/read a property's value located deep within a chain of related objects without verifying each reference's validity. Confused ???

Here is a simpler explanation for you my friend.
Imagine you have a big box, and inside that box, there's another smaller box. And inside the smaller box, there's a tiny box with a special toy inside. You want to get that toy, but first, you must open each box individually to check if it’s there. What if I told you that you could just ask for the toy, and it would come to you, even if you didn’t open all the boxes? That would save you a lot of time and effort, right? That is just how the optional chaining operator works.
  • click() is a method that simulates a click event on the retrieved element. When called on a file input element, it opens the file selection dialog, allowing the user to select a file.

And then we call the triggerFileUpload function on the span tag where we have the word browse so when a user clicks on browse , their default file explorer opens up.

 <span 
    className="underline font-semibold cursor-pointer"
    onMouseDown={triggerFileUpload}
  >
    browse
 </span>

// you can choose to use onClick() if that is what you prefer

Our CustomFileUploader component can now select and upload a file. Let's go ahead and wrap it up by introducing the drag-and-drop feature/functionality.

Drag-and-Drop

For that, we use a state variable to track whether an element(in our case file) is being dragged or not.

  const [isDragging, setIsDragging] = React.useState<boolean>(false);

Then we have the below functions;

 const handleDragOver = (event: { preventDefault: () => void; }) => {
    event.preventDefault();
    setIsDragging(true);
  };

const handleDragLeave = () => {
    setIsDragging(false);
  };

  const handleDrop = (event: { dataTransfer: { files: any } }) => {
    const file = event.dataTransfer.files;
    if (file) {
      setSelectedFile(file[0]);
    }
    setIsDragging(false);
  };
  • The handleDragOver and handleDragLeave functions call the setIsDragging function from our useState hook and sets the isDragging state variable to true or false respectively.

  • The handleDrop function does the same thing as our handleFileChange .Only this time we extract the dragged/selected file event dataTransfer object and not the event target of the event object.

We then call these functions on our CustomFileUploader parent div . I.e the div with dotted borders like this.

  <div
     className="py-12 px-4 bg-white border-2 border-dashed rounded-md cursor-pointer"
     onDragOver={handleDragOver}
     onDragLeave={handleDragLeave}
     onDrop={handleDrop}
    >
        .......other code
  </div>

You will notice that we introduced some event handlers to our parent div to help us achieve the drag-and-drop functionality. Let's go over these event handlers quickly.

  • OnDragOver: This event handler is called when a user drags an element over the parent div element. the handleDragOver function is executed when this event occurs.

  • OnDragLeave: When a user drags an element out of the parent div element. The handleDragLeave function is fired when this event happens.

  • OnDrop: This event handler is called when a user drops an element into the parent div and in turn, the handleDrop function is executed when this event occurs.

If you follow this article to this point, Congratulations you now have a functional custom file uploader with drag-and-drop functionality. Your final code should be like what we have below.

const CustomFileUploader = (): JSX.Element {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [isDragging, setIsDragging] = useState<boolean>(false);

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = event.target.files;
    if (files) {
      setSelectedFile(files[0]);
    }
  };
  const triggerFileUpload = () => {
    document.getElementById("fileInput")?.click();
  };

  const handleDragOver = (event: { preventDefault: () => void; }) => {
    event.preventDefault();
    setIsDragging(true);
  };

  const handleDragLeave = () => {
    setIsDragging(false);
  };
  const handleDrop = (event: { dataTransfer: { files: any } }) => {
    const file = event.dataTransfer.files;
    if (file) {
      setSelectedFile(file[0]);
    }
    setIsDragging(false);
  };

  return (
    <div className="flex justify-center items-center h-screen">
      <div className="flex flex-col gap-2 w-1/2">
        <label htmlFor="fileupload" className="text-lg text-black">
          Upload Resume
        </label>
        <div
          className="py-12 px-4 bg-white border-2 border-dashed rounded-md cursor-pointer"
          onDragOver={handleDragOver}
          onDragLeave={handleDragLeave}
          onDrop={handleDrop}
        >
          <input
            id="fileInput"
            type="file"
            accept=".pdf,.doc,.docx"
            className="hidden"
            onChange={handleFileChange}
          />
          <p className="text-md text-gray-600 text-center">
            Drag & drop your files here or{" "}
            <span
              className="underline font-semibold cursor-pointer"
              onMouseDown={triggerFileUpload}
            >
              browse
            </span>
          </p>
        </div>
        {selectedFile?.name}
      </div>
    </div>
  );
}

export default CustomFileUploader;

Conclusion

In this article, we've taken a comprehensive journey through building a custom drag-and-drop file uploader using React and Tailwind CSS. From setting up the basic structure to implementing drag-and-drop functionality and file selection, we've covered all the essential steps to create a functional and visually appealing file uploader.

You now know how to create a seamless file uploader with a great user experience, using React's component-based architecture and Tailwind CSS's utility-first approach.

Whether you're building a simple blog or a complex enterprise application, a custom file uploader can elevate your user interface and improve overall user experience. I hope this article has provided you with the knowledge and inspiration to take your file-uploading game to the next level.

Happy coding!!!

Resources and References