Posted on

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" aria-hidden="true">{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). I set aria-hidden="true" to hide the paragraph itself from assistive technologies. Now the text is only read by screen readers when the user arrives at the file input, thanks to the aria-describedby attribute.

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" aria-hidden="true">{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 mixed as my accessibility audits on different platforms have shown:

  • Windows 10, Google Chrome 101.0.4951.67, NVDA 2021.3.5: 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”. When I navigate away and then return, the screen reader announces “Upload pdf, button, testfile dot pdf”.
  • Windows 10, Firefox 100.0.2, NVDA 2021.3.5: On focus, NVDA reads “Clickable, upload pdf, grouping, browse, button”. After selecting the file “testfile.pdf” the browser automatically focuses on the file input and reads “Button, browse, test pdf”. When I navigate away and then return, the screen reader announces “Clickable, upload pdf, grouping, browse, button”. Apparently, the aria-describedby attribute is ignored by Firefox and/or NVDA, which is a known issue.
  • Samsung Galaxy S20, Android 12, Google Chrome 101.0.4951.61, TalkBack: On focus, TalkBack reads “Upload pdf, button, no file selected, double tap to activate”. After selecting the file “testfile.pdf” the screen reader announces the element as “Upload pdf, button, testfile dot pdf, double tap to activate”.
  • Samsung Galaxy S20, Android 12, Firefox 100.3.0, 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.
  • iPhone 8, iOS 15.1, 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.

Posted on