Accessible Styled Form Controls

Styled File Uploads


Last updated:

Cross-browser styling for the native HTML file input type.

Pattern Demo

Visual design note

If a file name is too long, the element that contains the file name or number of files will wrap to another line. This is different than a native file upload, but is done to provide better responsive behavior.

Pattern Details

Pattern Markup
<div class="file-up" data-file-input>
  <label for="file_upload">
    <span class="file-up__label__text">
    <input type="file" id="file_upload">
  <-- generated by JavaScript -->
  <span class="file-up__output" aria-hidden="true">

The baseline markup for this component is a div wrapper. The div serves as an outer wrapper to the form control, and uses flexbox to position the label, which is a parent to the visually hidden input, and selected file output span.

Note: if using the accept attribute, or multiple attribute, it'd be good to provide context to the user that only certain file types, or that multiple files can be uploaded, respectively.

The JavaScript

The containing div[data-file-input] provides the necessary selector hook for the JavaScript. An input[type="file"] can't be styled directly, due to what makes up this component living in the Shadow DOM. Access to the Shadow DOM is still not widely supported at this time. Instead the JavaScript creates a span, injects some classes, and listens for a change event to the file input.

If using the multiple attribute, instead of the uploaded file name, the number of attached files will instead be visually printed on screen.

If the form control should be set to disabled, the JavaScript will add a class to the wrapping element, to add a visual style to promote the disabled state. (The actual input is hidden, so it can't be relied on to visually convey state, but it will announce it's state to screen reader users that happen upon it, when navigating with the virtual cursor, for example.)

If the data-file-input attribute has a value of "compact", then the class .file-up--compact will be added to the wrapper. This class will collapse the "no file selected" area until a user has selected a file or files to upload.

Affects on Screen Reader Announcements?

JAWS 2018 + Internet Explorer 11

Native file uploads consist of two focusable elements in Internet Explorer 11, so for sighted users interacting with this pattern, there will be two focus stops.

Focusing the input (with tab key) announces:
[Accessible Name] file upload edit. Enter a name of a file to upload.

Focusing the browse button (with tab key) announces:
[Accessible Name] file upload edit. Browse....

If using the virtual cursor to navigate, JAWS will announce "Upload an image" when focus is on the label, and File upload edit" and "Browse... button" when virtual cursor focus is on each element, respectively.

When an image, or images have been selected for upload, JAWS will append the file name or names to the announcements.

JAWS 2018 + Firefox 63 (nightly)

Native file uploads have a single focus stop in tested, non-Microsoft browsers. Styled or unstyled, JAWS will announce the following:

[Accessible Name], browse. No file selected (dot) frame. Browse button.

When a file has been uploaded, JAWS will announce: [Accessible Name], browse. [file name] frame. Browse button.

If multiple files have been uploaded JAWS will announce: [Accessible Name], browse. [# files selected] (dot) frame. Browse button.

JAWS 2018 or NVDA 2018.2.1 + Chrome (latest)

While Chrome has a visually similar native file upload style to Firefox, the announcement of the element differs. Styled or unstyled, JAWS and NVDA will announce the following:

[Accessible Name] button.

After uploading a file or files, JAWS and NVDA + Chrome will not announce the name or number of any selected files. To provide an experience that is similar to other browser announcements, this script will add an aria-describedby to the file upload input and unique ID to the span the file name or number of files is printed in. Resulting in:

[Accessible Name] button. [text contents of the output area].

NVDA 2018.2.1 + Firefox 63 (nightly)

File upload form controls have some serious issues with NVDA. The accessible name is not announced at all, nor is the default "no file selected" message. Firefox + NVDA announce the following when a styled or unstyled file upload is focued:

Clickable browse button

After uploading a file or files, tabbing to the control sill only produces the previous announcement. If using the virtual cursor, NVDA will announce the label and uploaded file name or number of files, if those text nodes are highlighted.

VoiceOver + Safari 11.1.1 on macOS High Sierra

The ordering in which VoiceOver announces a file upload control differs from how JAWS or NVDA announce the control. Instead of starting with the accessible name, VoiceOver announces a styled or unstyled file upload control in the following order:

[No file announcement (or) file name/number] [Accessible Name], file upload button.

For the styled upload controls, if navigating with VO and left or right arrow keys, it will seem as if there are two focus stops on the control. However this is merely VoiceOver focusing the label and then the file upload control itself, and is only odd due to the fact that the styled control is visually conveyed as a single element.

VoiceOver + Safari on iOS 11.3 & 11.4

It should be noted that, though they're named the same, VoiceOver for iOS should be treated as a different screen reader than it's desktop counterpart. Both styled and unstyled file upload controls have the following default state announcement:

[Accessible name], no file selected, button

Testing with photos and videos, regardless of whether it's a single or multi file upload control, after a user selects a file or files, VoiceOver will replace the "no file selected" announcement with "# Photo/video" or "# Photo and # Video" if both file types are chosen. The file name is not announced.

TalkBack (Android Accessibility Suite 6.2) + Android Chrome

Like Chrome on desktop, regardless of if files are selected or not, native file upload controls are only announced as the following:

[Accessible Name] button. Double-tap to activate.

Due to sniffing for Chrome, the aria-describedby gets added to the file upload control, so the styled control adds the text from the output span as an accessible description. This results in either "no file selected", or a file name being announced prior to the "Double-tap to activate" announcement.

Interestingly, multi file uploading doesn't appear to work with Chrome on Android. Or at least when testing, I could determine how to do it as selecting the first file automatically selected and closed the choose file UI.

Usage note:

If using NVDA with mouse settings updated to "report role when mouse enters object" this component will announce itself as a "text frame" since the input is visually hidden on top of the visual 'button'.

Due to browsers exposing native file uploads in different ways (both visually and with what's announced), a single design for a file upload may be confusing for some users if the visual style doesn't match expected announcements. A more robust, custom, pattern may need to be considered.