Skip to content
Devix Open Source

Guide

The keyboard and screen readers

Uploaders are usually rebuilt from scratch — a <div> that listens for clicks, a hidden input, a list of <span>s — and then have accessibility bolted back on. Two of the libraries this replaces have open issues about exactly that: a broken <label for>, and unnamed pause and cancel buttons.

The input stays the control

<input id="docs" name="docs[]" type="file">

That element is still there, still focusable, still announced as "Attachments, file upload button", and still what a form posts. The drop zone is its <label>, so:

  • Tab reaches it, because it is a real input.
  • Enter or Space opens the file picker, because that is what a file input does.
  • Clicking anywhere in the zone opens the picker, because a label does that.
  • A screen reader reads the label you already wrote for it.

Nothing here needed a tabindex, a role="button", or a keypress handler — and so there is nothing to get subtly wrong.

The list

Part What it is
The list A <ul> labelled "Files".
Each row The name, the size, and what is happening to it, as text.
Progress A real role="progressbar" with aria-valuenow, labelled with the file's name.
Remove, retry, cancel Real buttons, each named after its file — "Remove: invoice.pdf" — so a screen reader user never has to guess which row they are on.
A refusal Written in the row, in words: "Too big — 9 MB, and the most is 5 MB".
Announcements A role="status" live region says how many have gone up.

Nothing depends on a colour

  • A refused row has an icon and a message, not only a red border.
  • A finished row has a tick, not only a green one.
  • Focus rings are drawn on every control.
  • @media (forced-colors: active) gives the zone and the progress bar system colours.
  • The dragging state changes the border style as well as its colour.
  • prefers-reduced-motion turns off the progress transition.

Right to left

The whole layout is written in logical properties — padding-inline, margin-block-start, inline-size — so a page with dir="rtl" mirrors it with nothing to configure. Sizes and times come from Intl, so they are already in the right script and the right order. This is an open request on the uploader with sixteen thousand stars, and it costs nothing.

What is announced, and what is not

Progress is deliberately not announced as it moves: a live region firing twenty times a second makes a page unusable. The live region speaks when a file is added, when one finishes, and when one is refused — the three things worth interrupting for. The progress bar itself carries the current value for anyone who goes looking.

Updated 12 Sep 2026