Creating a fancy, accessible File Input in 3 Steps
Native HTML elements are accessible by default and you can style most of them with CSS however you like – as I've shown in my blog post about web forms. Then again, there's elements like the file input, which is very hard to style.
The <input type="file">
element is rendered as a button that allows the user to open the
operating system's file picker. This button is completely unstylable – it can't be sized or colored, and it won't even
accept a different font. But don't despair! I'll show you how to make it work.
Photo: © Anete Lusina / pexels.com
Step 1: Use Native HTML Elements
When I include a file picker in a web form, I want all users to be able to select and upload files. This includes keyboard
and screen reader users as well. Which is why I'll use the native, accessible <input type="file">
element. I've created a demo using the React framework. Here's what my JSX code
looks like:
<label htmlFor="filepicker" className={styles.filePicker}>
<span>Upload PDF</span>
<input
id="filepicker"
type="file"
accept=".pdf"
aria-describedby="selected-file"
onChange={event => onFilePickerChange(event)}
/>
</label>
<p id="selected-file">{selectedFile}</p>
I use a label
element that contains the visible label of my file picker ("Upload PDF") as well as
a visually hidden input
element. This way, the label
element serves as the
visible UI component and you can apply any custom styling (more on that in step 2).
The p
element displays the hint "No file selected" or, if a file was selected, the file's name
(see step 3). Thanks to the aria-describedby
attribute, the text is also read by screen readers when
the user arrives at the file input.
I also tried setting aria-hidden="true"
to hide the paragraph itself from assistive technologies. As
this led to problems with certain browser and screen reader combinations, I removed the attribute again. Now screen reader users
may hear the hint twice. Which is better than not hearing it at all.
Step 2: Apply CSS Magic
Next, I use CSS to visually hide the input
element and position it above the label. This
enables me to style the label
element and thereby the visible file picker however I want:
form label[for].filePicker {
position: relative;
background-color: rgb(49, 4, 92);
color: white;
font-size: 1rem;
// ... more custom styling
}
form label[for].filePicker input[type=file] {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
opacity: 0;
}
It's important that you don't hide the <input type="file">
from assistive technologies.
Using display: none
or setting the element's size to zero would hide it from screen readers.
To give visual feedback to sighted users, I display an outline on hover and when the input
element
within the label
receives focus:
form label[for].filePicker:focus-within,
form label[for].filePicker:hover {
outline: 2px solid black;
outline-offset: 2px;
}
Step 3: A Dash of JavaScript
As a last step, I want to display the file name after the user has selected a file. To achieve this, I use
the change
event, which is fired when an alteration to the input's value is committed by the user.
const FileUpload: React.FunctionComponent = () => {
const [selectedFile, setSelectedFile] = useState('No file selected');
const onFilePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? []);
if (files.length > 0) {
setSelectedFile(files[0].name);
}
}
return (
// Only the relevant sections of the JSX code
<input type="file"
// ...
onChange={event => onFilePickerChange(event)}
/>
<p id="selected-file">{selectedFile}</p>
// ...
);
};
Of course, you can do all that in plain JavaScript too – or any framework of your choosing. My code is simply an example of an implementation as a React component. You can view the complete source code on GitHub.
The Perfect File Input, right?
Check out the result of my styled file input below. I've also included a standard file input for comparison:
Awesome, right? I'm happy with the final result. But still, there's room for improvement. The
styled <input type="file">
works perfectly for sighted keyboard users. For screen reader users,
the results are pretty good, but not perfect.
Firefox fixed an important issue with aria-describedby
in version 121. At the moment of my retests, this new version was still in beta. Here's a detailed account of my accessibility
audits on different platforms:
- Windows 11, Google Chrome 120.0.6099.71, NVDA 2023.3: On focus, NVDA reads “Upload pdf, button, no file selected”. After selecting the file “testfile.pdf” the browser automatically focuses on the file input and reads “Button, upload pdf, testfile dot pdf”. When I navigate away and then return, the screen reader announces “Upload pdf, testfile dot pdf, button, testfile dot pdf”.
- Windows 11, Firefox Beta 121.0b9, NVDA 2023.3: On focus, NVDA reads “Clickable, upload pdf, browse, button, no file selected”. After selecting the file “testfile.pdf” the browser automatically focuses on the file input and reads “Button, upload pdf, browse, upload pdf, browse, testfile dot pdf”. When I navigate away and then return, the screen reader announces “Clickable, upload pdf, browse, button, testfile dot pdf”.
- Samsung Galaxy S20, Android 13, Google Chrome 119.0.6045.194, TalkBack: On focus, TalkBack reads “Upload pdf, no file chosen, button, no file selected, double tap to activate”. After selecting the file “testfile.pdf” the screen reader announces the element as “Upload pdf, testfile dot pdf”.
- Samsung Galaxy S20, Android 13, Firefox for Android Beta 121.0b9, TalkBack: The screen reader only focuses on the text “Upload PDF”. It provides no information about the button role or the file name. At least, the file input can be triggered via double tap. After selecting the file “testfile.pdf”, this text is displayed next to the button and read by the screen reader on focus.
- iPhone 8, iOS 16.7.2, Safari, VoiceOver: On focus, VoiceOver reads “Upload pdf, button, no file selected”. After selecting the file “testfile.pdf” the screen reader announces the element as “Upload pdf, button, testfile pdf”.
Conclusion
As my demo shows, you can create an accessible file picker with custom styling using
the <input type="file">
element and a bit of ingenuity.
Unfortunately, I've encountered some accessibility problems with certain browser and screen reader combinations. Maybe this will be fixed in future updates of the browsers and/or screen readers. Or maybe I'll discover a better solution. We'll see.
Update on 03/26/2023
I was wrong: There actually is a way to customize the native <input type="file">
HTML element.
You can use the ::file-selector-button
CSS pseudo-element to style the rendered button.
Unfortunately, you can't change the text rendered inside the button.
Here's a demo with more info about it.
Update on 12/10/2023
I revised the HTML code in my example and redid the screen reader tests with the current browser versions.
Posted on