Creating a fancy, accessible File Input in 3 Steps

Posted on

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.

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:

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