How to customize the file upload button in React

The default file upload button is ugly. Customizing its style is hard. I’ve surveyed the solutions available across the web. Here’s what I have concluded as the best solution.

MasaKudamatsu
Web Dev Survey from Kyoto
7 min readMar 31, 2020

--

NOTE: This article is last updated on August 18, 2023.

TL;DR

import { useRef } from 'react';
import './styles.css';
export const FileUploader = ({handleFile}) => { // Create a reference to the hidden file input element
const hiddenFileInput = useRef(null);

// Programatically click the hidden file input element
// when the Button component is clicked

const handleClick = event => {
hiddenFileInput.current.click();
};
// Call a function (passed as a prop from the parent component)
// to handle the user-selected file
const handleChange = event => {
const fileUploaded = event.target.files[0];
handleFile(fileUploaded);
};
return (
<>
<button className="button-upload" onClick={handleClick}>
Upload a file
</button>
<input
type="file"
onChange={handleChange}
ref={hiddenFileInput}
style={{display: 'none'}} // Make the file input element invisible
/>
</>
);

Then style the button with the .button-upload selector in styles.css.

Here is the CodeSandbox demo of the above code.

Problem

The default style of file upload buttons is ugly.

The default style of <input type=”file”/> with Chrome 80.

Removing its style with CSS is hard.

It’s a topic that consistently arises among web developers:

But many suggestions ignore accessibility by using display:none, or compromise cross-browser compatibility (as in the CSS-Tricks article above).

More recently, the ::file-selector-button pseudo element to style iput[type="file"] has been suggested (Hlopov 2022), given that it is becoming widely supported by modern browsers (accounting for 90.99% of global page views in July 2023, according to Can I Use?). But as far as I know, it does not allow you to customize the style of the “no file chosen” text.

Note: I use the Author-Date referencing system in this article, to refer to various articles on web development.

The solution

The solution described below is (originally?) suggested by teshguru and H. Pauwelyn (2012–2019). It is also described in MDN Contributors (2020).

Below I refactor the code so it will work with React. Indeed, I use this code for Line-height Picker (a web tool that I have built) to allow the user to upload a font file, specifically inside a React component called FontFileUploader (linked to my GitHub repo).

Step 1: Make the input element invisible

Add the CSS declaraion display: none to the input element:

export const FileUploader = () => {
return (
<>
<input
type="file"
style={{display:'none'}} // NOTICE!
/>
</>
);
};

For how the inline style works in JSX, see React (2020b).

The display: none is usually frowned upon as compromising accessibility. In this particular case, however, it’s all right, because we will use another HTML element to represent the file upload button.

Step 2: Add a button element that you style to your taste

Create a button element with your custom style:

import "./styles.css"; // ADDED

export const FileUploader = () => {
return (
<>
{/* ADDED FROM HERE */}
<button className="button-upload">
Upload a file
</button>
{/* ADDED UNTIL HERE */}

<input
type="file"
style={{display:'none'}}
/>
</>
);
};
/* ./styles.css */
.button-upload {
cursor: pointer;
font: inherit;
min-height: 48px;
min-width: 48px;
}

In this example, I customize the button with what I would call “modern CSS reset” for <button> elements:

  • cursor: pointer; allows mouse users to notice the button’s clickability when they hover over it, by making the cursor behave like when hovering over link text.
  • font: inherit; matches the style of the button’s label text with that of its surroundings (by default, <button> elements do not inherit font style from its parent element).
  • min-height: 48px; and min-width: 48px allow touchscreen users to tap the button easily with their finger pad. The value of 48px is used because “[t]he 48x48 pixel area corresponds to around 9mm, which is about the size of a person’s finger pad area” (Gash et al. 2020).

Step 3: Add a click event handler to the Button element

import "./styles.css"; 

export const FileUploader = () => {
const handleClick = event => {}; // ADDED

return (
<>
<button
className="button-upload"
onClick={handleClick} // ADDED
>
Upload a file
</button>
<input
type="file"
style={{display:'none'}}
/>
</>
);
};

The idea is to let the user initiate the process of uploading a file by clicking the button.

Step 4: Trigger the clicking of the input element

import { useRef } from 'react';         // ADDED
import "./styles.css";

export const FileUploader = () => {
const hiddenFileInput = useRef(null); // ADDED

const handleClick = event => {
hiddenFileInput.current.click(); // ADDED
};

return (
<>
<button
className="button-upload"
onClick={handleClick}
>
Upload a file
</button>
<input
type="file"
ref={hiddenFileInput} // ADDED
style={{display:'none'}}
/>
</>
);
};

This way, the user’s clicking of the button programmatically translates into the clicking of the invisible input element.

In the above code, we use the useRef() hook to refer to the hidden input element. For the detail on the useRef() hook, see React (2020a). For why we should use useRef() instead of document.getElementById(), see Farmer (2015–2018).

Step 5: Add a click event handler to the input element

import { useRef } from 'react';         
import "./styles.css";

export const FileUploader = () => {
const hiddenFileInput = useRef(null);

const handleClick = event => {
hiddenFileInput.current.click();
};

const handleChange = event => {}; // ADDED

return (
<>
<button
className="button-upload"
onClick={handleClick}
>
Upload a file
</button>
<input
type="file"
onChange={handleChange} // ADDED
ref={hiddenFileInput}
style={{display:'none'}}
/>
</>
);
};

The handleChange() function will be called from the clicking of the input element, which is in turned triggered by the user’s clicking of the button.

Step 6: Access to the uploaded file

import { useRef } from 'react';         
import "./styles.css";

export const FileUploader = () => {
const hiddenFileInput = useRef(null);

const handleClick = event => {
hiddenFileInput.current.click();
};

const handleChange = event => {
const fileUploaded = event.target.files[0]; // ADDED
};

return (
<>
<button
className="button-upload"
onClick={handleClick}
>
Upload a file
</button>
<input
type="file"
onChange={handleChange}
ref={hiddenFileInput}
style={{display:'none'}}
/>
</>
);
};

The <input type="file"/> element attaches an array called files to event.target. The uploaded file is the first item in this array. So event.target.files[0] gives you an access to the uploaded file.

For more detail, see MDN Web Docs.

Step 7: Handle the uploaded file

Most likely the function to handle the uploaded file is defined and passed as a prop by a parent component:

import { useRef } from 'react';         
import "./styles.css";

export const FileUploader = ({handleFile}) => { // REVISED
const hiddenFileInput = useRef(null);

const handleClick = event => {
hiddenFileInput.current.click();
};

const handleChange = event => {
const fileUploaded = event.target.files[0];
handleFile(fileUploaded); // ADDED
};

return (
<>
<button
className="button-upload"
onClick={handleClick}
>
Upload a file
</button>
<input
type="file"
onChange={handleChange}
ref={hiddenFileInput}
style={{display:'none'}}
/>
</>
);
};

which assumes that the handling function is passed as the prop named handleFile.

That’s it!

Demo

In response to a comment by streeptococcus, I’ve created a CodeSandbox demo of the above React code. Have a look!

Another (less desirable) solution

Alternatively, you can nest the <input type="file"> element into the label element. Style the label element as your favorite button while “visually-hiding” the input element so that the screen readers can still “see” it.

For detail, see MDN Contributors (2020). The same solution is also proposed by the following two webpages:

The latter of which is mentioned in Ash (2018).

The limitation of this solution is that you do need to reveal the ugly file upload button when it is focused by keyboard-using sighted users. An article from The A11Y Project writes:

…natively focusable elements (such as a, button, input, etc) … must become visible when they receive keyboard focus. Otherwise, a sighted keyboard user would have to try and figure out where their visible focus indicator had gone to. —Rupert and Fairchild (2013–2019)

Because of this limitation, I believe this second solution is less desirable.

Changelog

v1.1.0 (August 18, 2023): Add the CodeSandbox demo; modernize the code with the named export/import and the destructured parameters; use CSS modules instead of Styled Components; and revise the text to reflect these changes and to mention the limitation of the ::file-selector-button pseudo-element.

v1.0.1 (July 8, 2020): Use the useRef() hook, instead of document.getElementById(), to refer to the hidden input element.

References

Ash, Oliver Joseph. (2018) “A comment to ‘Styling an input type=“file” button’”, Stack Overflow, Sep 25, 2018.

Farmer, Andrew H. (2015–2018) “Why to use refs instead of IDs”, JavaScript Stuff, Oct 21, 2015 (updated on Jan 27, 2018).

Gash, Dave, Meggin Kearney, Rachel Andrew, Rob Dodson, and Patrick H. Lauke (2020) “Accessible tap targets”, web.dev, Mar 31, 2020.

MDN Contributors. (2020) “Using files from web applications”, MDN Web Docs, Mar 29, 2020.

Hlopov, Nikita (2022) “Custom styled input type file upload button with pure CSS”, nikitahl.com, Jul 15, 2022.

React. (2020a) “Refs and the DOM”, React Docs, accessed on Jul 8, 2020.

React. (2020b) “Style”, React Docs, accessed on Jun 8, 2020.

Rupert, Dave, and Michael Fairchild. (2013–2019) “How-to: Hide content”, The A11Y Project, Feb 15, 2013, and Jul 28, 2019.

Styled-Components. (2020) “Why should I avoid declaring styled components in the render method?Styled-Components Documentation: FAQs, accessed on Jun 8, 2020.

teshguru and H. Pauwelyn (2012–2019) “An answer to ‘Styling an input type=“file” button’”, Stack Overflow, Mar 3, 2012 and Jul 23, 2019.

--

--

MasaKudamatsu
Web Dev Survey from Kyoto

Self-taught web developer (currently in search of a job) whose portfolio is available at masakudamatsu.dev