Accessibility pattern · Form inputs

File upload

The drop zone is a native file input stretched over a styled area, so Tab, Enter, Space and the system file picker all work before any script runs. Each chosen file gets a row with its name, size, a progress bar, any error in words, and a Remove button that names the file.

WCAG criteria
7
Keyboard rules
3
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Your latest salary slip and a photo of your PAN card, for the home loan application.

or drag them here

PDF, JPG or PNG, up to 5 MB each.

  • salary-slip-september.pdf 412 KB

    Uploaded

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to the file input (drawn as the drop zone), then to each file's Remove button.
Enter or SpaceOn the drop zone, opens the system file picker; files chosen there are added to the list.
Enter or SpaceOn a Remove button, removes that file (or stops its upload) and moves focus to the next file, or back to the input.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the drop zoneSupporting documents Choose files, button. Your latest salary slip… PDF, JPG or PNG, up to 5 MB each.
Two files are chosen, one too large1 file uploading. 1 file can't be added; see the list.
An upload finishesbank-statement.pdf uploaded
Focus reaches a Remove buttonRemove bank-statement.pdf, button, Uploaded
A file is removedbank-statement.pdf removed

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-file-upload" data-ap-file-upload data-max-mb="5" data-types="pdf jpg jpeg png">
  <div class="ap-file-upload__head">
    <label class="ap-label ap-file-upload__label" for="file-upload-input">Supporting documents</label>
    <p class="ap-hint" id="file-upload-help">Your latest salary slip and a photo of your PAN card, for the home loan application.</p>
  </div>

  <div class="ap-file-upload__zone" data-ap-zone>
    <input class="ap-file-upload__input" id="file-upload-input" type="file" name="documents" multiple accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" aria-describedby="file-upload-help file-upload-rules" />
    <span class="ap-file-upload__art" aria-hidden="true">
      <svg viewBox="0 0 24 24" focusable="false"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>
    </span>
    <p class="ap-file-upload__lead"><label class="ap-file-upload__choose" for="file-upload-input">Choose files</label> <span class="ap-file-upload__or">or drag them here</span></p>
    <p class="ap-file-upload__rules" id="file-upload-rules">PDF, JPG or PNG, up to 5 MB each.</p>
  </div>

  <ul class="ap-file-upload__list" aria-label="Chosen files" data-ap-list>
    <li class="ap-file-upload__item" data-state="done">
      <span class="ap-file-upload__icon" aria-hidden="true">
        <svg viewBox="0 0 24 24" focusable="false"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5"/></svg>
      </span>
      <div class="ap-file-upload__body">
        <p class="ap-file-upload__row"><span class="ap-file-upload__file" id="file-upload-name-1" translate="no">salary-slip-september.pdf</span> <span class="ap-file-upload__size" translate="no">412 KB</span></p>
        <p class="ap-file-upload__state" id="file-upload-state-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg><span>Uploaded</span></p>
      </div>
      <button type="button" class="ap-file-upload__remove" id="file-upload-rm-1" aria-labelledby="file-upload-rm-1 file-upload-name-1" aria-describedby="file-upload-state-1">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
        <span class="ap-file-upload__sr">Remove</span>
      </button>
    </li>
  </ul>
  <p class="ap-file-upload__sr" role="status" data-ap-say></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The file input has a real label, and its hint and rules are tied to it with aria-describedby, so they are read on focus.

  • 2.1.1 Keyboard Level A

    Everything works from the keyboard through the native input; dragging is an extra for pointer users, never the only way.

  • 2.5.3 Label in Name Level A

    The input's name contains both visible words on it, Supporting documents and Choose files, so voice commands match what people see.

  • 3.3.1 Error Identification Level A

    A file that cannot be used stays in the list with the reason in words, next to its name, not just a red outline.

  • 3.3.2 Labels or Instructions Level A

    Accepted types and the size limit are shown before anyone chooses a file, inside the zone and in the input's description.

  • 4.1.2 Name, Role, Value Level A

    Each Remove button's name includes its file name, and each progress bar is named by its file.

  • 4.1.3 Status Messages Level AA

    Files added, uploads finished, errors and removals are announced politely, one message at a time, without moving focus.

Usage

When to use it

Use it

  • Forms that need documents or images, like applications, claims, support tickets or profile photos.
  • Uploads that start as soon as files are chosen, where people need to see each file's progress and outcome.

Use something else

  • A single small file in a short form: a plain labelled file input is enough.
  • Pasting text that could be typed: a text field is quicker than making someone save a file first.

Common failures

How it usually goes wrong

  • A div that only accepts drops

    A drop target with no file input cannot be reached by keyboard, switch or screen reader users at all. The input here is the zone.

  • Hiding the input with display: none

    That removes it from the Tab order and from assistive technology. The input here is transparent but still there, focusable and labelled.

  • Rules revealed only after a failure

    Telling people the limit after a long upload wastes their time. The types and size limit are on screen and read with the input.

  • Errors shown as a red border

    A colored outline says nothing about what went wrong. Each rejected file says why in words, with an icon, and is announced.

  • Remove buttons that all say Remove

    In a list of five files, five buttons called Remove are impossible to tell apart. Each one here says which file it removes.

  • Focus lost after a removal

    When the focused row disappears, focus falls to the top of the page. It moves to the next file's button, or back to the input.

Notes

Building it

  • The input is stretched over the zone with opacity: 0, so a click anywhere opens the picker and the zone shows its focus ring through :has(:focus-visible).
  • The accept attribute only filters the picker; files that are dropped, or chosen with All files, still need checking, which the script does by extension and type.
  • The input is cleared after each choice so the same file can be added again after a removal; the list, not the input, is what gets uploaded.
  • Announcements go through one polite status region in a queue, so a finished upload never cuts off the message before it.
  • The demo simulates the network. Turn on Slow connection to watch the progress bars; in a real form, report progress from XMLHttpRequest upload events or a streamed fetch.

Sources: HTML: the file input · GOV.UK Design System: File upload

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction