Uploading a document is usually the last step of something that matters. A claim, an application, a proof of address. It is also the least tested interaction on most sites, because the developer uploads one file during the build and never comes back.
Do Not Hide the Input, Hide It the Right Way
The native file input is ugly, hard to style, and inconsistent across browsers, which is why the usual approach is display: none on the input and a styled button beside it. That takes the control out of the tab order and out of the accessibility tree, and now the only people who can upload anything are the ones using a mouse.
The documented answer is more specific than hide it visually. Use opacity: 0 on the input, positioned over the styled area, and pair it with a real label styled to look like your button. MDN gives the reason for choosing opacity over the alternatives in one line. Assistive technology reads visibility: hidden and display: none as meaning the input is not interactive, and opacity does not.
There is a second reason to prefer it, and it decides the next section. A transparent input sitting over the styled area is still a real input, so it can be the drop target for a drag as well as the thing a click reaches. A clipping technique that shrinks the input to one pixel keeps it focusable and cannot be dropped onto, so you end up building the drop zone twice.
The quick test
Tab to the upload control and press Enter or Space. If the file picker does not open, something is wrong, and the hiding technique is only one candidate. A handler calling preventDefault, a label that is not associated with the input, or a click handler on a wrapper rather than on the control will all produce the same silence. Check on more than one browser, and check on a phone, where the behavior differs.
A Drop Zone Is Never the Only Route
Dragging asks for precise pointer control, which is the same difficulty 2.5.7 was written about. Whether that criterion reaches this exact case is arguable, because the drag begins in the operating system's file manager rather than in your page, and W3C publishes nine worked examples for the criterion and none of them is a file drop zone.
The remedy is the same either way, and it stands on the keyboard rule and on plain usability without needing the argument settled. A visible button that opens the file picker, present on the page rather than revealed on hover. One thing worth knowing about why the button specifically is what discharges this. Making the drop zone focusable and Enter-activatable solves the keyboard problem and leaves the pointer problem exactly where it was, because W3C says keyboard equivalence does not satisfy the dragging criterion unless the equivalent controls can also be clicked or tapped. The two are evaluated independently.
So the pattern that works is a drop zone with a plainly visible Choose a file button inside it, and our drag and drop guide has the general shape of the problem.
Say What You Will Accept, Before They Try
Type and size limits belong next to the control rather than in the error message that arrives after a failed upload, which is 3.3.2 Labels or Instructions and basic courtesy on a slow connection. Two scope notes on that criterion. It applies to every field, optional ones included, because requires in its wording means accepts rather than mandatory. And W3C's counterweight is worth having in your pocket, since it says too much information or instruction can be just as harmful as too little.
- Name the accepted formats in words, not only in an
acceptattribute. The reason is stronger than politeness.acceptdoes not validate anything. It hints to the browser about which files to show in the picker, and W3C's documentation says outright that it has to be backed up by server-side validation. So the restriction has to exist twice, once as words for the user and once as a check on the server, and the attribute is neither of those. - State the size limit in human units before the upload, not after it fails. Give the expected format and an example while you are there.
- Tie field-specific instructions to the input with
aria-describedby, and know that a description is announced after the label rather than with it. Instructions that apply to the whole form belong before theformelement, because screen readers switch into a forms mode inside it and in that mode usually read only the form controls themselves. A paragraph of upload rules sitting between two fields may never be read at all. - Say how many files are allowed if the input takes more than one.
Progress, Success and Failure All Need Announcing
A spinner is a visual event. For somebody who cannot see it, a twenty-second upload with no announcement is twenty seconds of nothing, and the natural response to nothing is to press the button again.
- Announce the start once, politely.
- Announce progress at intervals rather than continuously. There is no published figure for the right interval and there is a published warning about making an application too chatty for a screen reader user, so pick something coarse and test it with somebody rather than tuning it by feel.
- Announce completion by name. Passport.pdf uploaded, rather than uploaded, because on a form taking four documents the name is the whole message.
- Announce failure with a reason and a fix, which is 3.3.1 Error Identification and 3.3.3 Error Suggestion together. Upload failed is not enough. Your file is 12MB and the limit is 5MB is.
- Announce a cancelled picker. There is a DOM event for the moment somebody opens the file dialog and closes it without choosing, and nothing else tells them the state went back to where it was.
The mechanics are a live region, and progress has its own published technique rather than being generic status work. A live region reporting a progress bar is the one to use. There is a newer browser API aimed at exactly this, and it is still experimental, so treat it as a progressive enhancement sitting on top of a live region rather than as a replacement for one.
Two choices inside the live region decide whether any of this arrives intact. Set aria-atomic="true", because a region where only the percentage changes announces the number and nothing else, so passport.pdf, 50 percent uploaded becomes fifty. And for a multi-file upload where completions pile up one after another, role="log" appends rather than replacing, which is the behavior you want when four files finish in sequence.
The Uploaded File List
Each file needs a readable name and a remove control that says what it removes. A row of identical X buttons announces as a row of identical X buttons.
The population that hurts most is not the one people picture. A screen reader user has the file name in the row beside the button and can work it out. A speech input user activates a control by saying its name out loud, and five controls all called Remove cannot be told apart by voice at all. That is a named failure in its own right, and it is why Remove passport.pdf as the accessible name is worth the ten minutes. Visually it can still be an icon.
Two more things about that row. The remove button is a small target in a tight list, which is 2.5.8 Target Size territory. And removing an attached file is irreversible from the user's point of view, so W3C's own advice on destructive actions applies. Ask for confirmation, or provide an undo, rather than letting one mis-aimed tap delete the document somebody spent ten minutes scanning.