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.

A person is looking for a document in a briefcase. 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