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.
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.
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;
andmin-width: 48px
allow touchscreen users to tap the button easily with their finger pad. The value of48px
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.