File pickers look simple, but they are one of the most commonly broken controls on the web. Custom drag-and-drop zones routinely ship with no label, no keyboard path, and upload progress that screen readers never hear. An accessible file upload is rarely about adding more code, it is about not breaking the native <input type="file"> the browser already gives you for free.
This guide walks through the four things developers get wrong most often: associating a real label, keeping the control keyboard-operable, providing a non-drag alternative to satisfy WCAG 2.2 Success Criterion 2.5.7 Dragging Movements, and announcing errors and progress through status messaging. Examples are framework-agnostic and map directly to WCAG 2.2 Level A and AA, the baseline the European Accessibility Act enforces via EN 301 549.
Start with the native input and a real label
The single biggest accessibility win is keeping a genuine <input type="file"> in the DOM. It is focusable, operable by keyboard, exposed to assistive technology as a file-upload control, and it opens the OS file dialog that users already know. Most teams hide it not because it is broken but because it is hard to style, then rebuild a worse version on top.
Every input needs a programmatic label. A placeholder, a nearby heading, or grey text inside a drop zone does not count. Use a real <label> tied by the for/id relationship:
<label for="resume">Upload your CV (PDF or DOCX, max 10 MB)</label> followed by <input type="file" id="resume" name="resume" accept=".pdf,.docx">.
Put format and size constraints in the visible label or in hint text wired up with aria-describedby, not only in a tooltip. The accept attribute filters the dialog but is advisory, so validate on the server too. If your design hides the native control, do it with a visually-hidden class (clipped, not display:none, which removes it from the accessibility tree) and let the <label> act as the visible, clickable target.
- Do not use aria-label on a styled <div> to fake a file input; a div is not operable or announced as a control.
- Avoid a bare "Browse" with no context; the accessible name should say what is being uploaded.
- One input, one label. If you allow multiple files, add the multiple attribute and say so in the label.
Make the trigger keyboard operable
A native input is keyboard-operable by default: it takes Tab focus and Enter or Space opens the dialog. You break this when you wrap a custom button around a hidden input and forget that a <div> or <span> is not in the tab order.
If you build a custom trigger, use a real <button type="button"> that calls input.click(). A button is focusable, fires on both Enter and Space, and announces its role. If you insist on a non-button element, you must add tabindex="0", role="button", and key handlers for Enter and Space yourself, which is three chances to get it wrong. The native button is simpler and correct.
Confirm the focus indicator is visible on the trigger and never set outline:none without a replacement; WCAG 2.4.7 Focus Visible (AA) requires it, and WCAG 2.2 adds 2.4.11 Focus Not Obscured (Minimum, AA), so a sticky header must not cover the control you just focused. The clickable target should also meet 2.5.8 Target Size Minimum (24x24 CSS pixels).
Drag-and-drop needs a non-drag alternative (WCAG 2.5.7)
Drag-and-drop drop zones are popular and genuinely useful, but dragging is impossible or painful for many users: people with tremors, limited dexterity, those using a keyboard only, switch access, or voice control. WCAG 2.2 SC 2.5.7 Dragging Movements (AA) states that any function operated by a dragging movement must also be achievable with a single pointer without dragging, unless dragging is essential.
For file uploads, dragging is never essential, because the file dialog exists. The fix is straightforward: keep drag-and-drop as an enhancement, and always expose a click/tap-to-browse control backed by the native input. The drop zone is a convenience layer; the button is the guaranteed path.
- Render the <input type="file"> (or a button that triggers it) inside or beside the drop zone, never as the only way in.
- Handle dragover, dragleave and drop on the zone to set the same files, but the same upload must be reachable with one tap on the button.
- Give visual drag feedback (a highlighted border on dragover) using a class, and keep that border at 3:1 contrast against its surroundings per the non-text contrast rule.
This same single-pointer principle covers any reorder-by-drag or slider you build. If a list of uploaded files can be reordered by dragging, add move-up and move-down buttons too.
Announce errors, progress and success with status messages
Sighted users see a red border, a spinner, or a green checkmark. Screen reader users get nothing unless you tell them. WCAG 4.1.3 Status Messages (AA) requires that changes which are not a focus change still reach assistive technology, and file uploads are full of them: validation errors, percent-complete, and completion.
For validation errors, set aria-invalid="true" on the input, link the message with aria-describedby, and put the error text where it can be announced. Programmatically move focus to the first invalid field on submit so the user lands on the problem. Write specific messages: "PDF is 14 MB; the limit is 10 MB" beats "Invalid file."
For progress and success, use a live region. A polite region (aria-live="polite" or role="status") announces "Uploading, 40 percent" and then "upload complete" without stealing focus; reserve assertive for genuine errors. Throttle progress updates to avoid flooding the user with announcements every few percent. Native <progress> communicates the value visually and to AT; pair it with periodic live-region text for the spoken update.
- The live region element must exist in the DOM before you update it, or the first message is missed.
- Keep error text colour-independent: an icon and words, not red alone (contrast 4.5:1 for text).
- After a successful upload, announce it and, where useful, move focus to the next step so keyboard users are not stranded.
A quick pre-ship checklist
Before you call a file upload done, run it through these checks. Most failures are caught in minutes by unplugging the mouse and turning on a screen reader.
- Tab to the control, press Enter and Space, and confirm the OS file dialog opens.
- Confirm a screen reader announces a meaningful name, the accepted formats, and the size limit.
- Verify every drag action has an equivalent single-click path (2.5.7).
- Trigger a validation error and confirm it is announced, linked by aria-describedby, and focus moves to it.
- Start a real upload and confirm progress and completion are spoken, not just shown.
- Check the focus indicator is visible and the trigger meets the 24x24 px target size (2.5.8).
For the EU legal context behind these criteria, see the European Accessibility Act and the EN 301 549 standard, or work through the full accessibility checklist. When you are ready to find issues across your whole site, run a free scan with AccessScan to catch unlabeled inputs, missing focus styles, and contrast problems automatically, then confirm the upload flow by hand.