# File upload drop zones with no button to choose files

Source: https://easeweb.dev/learn/file-drop-zones
Topics: Drag and drop > File drop zones also have a button to choose files (WCAG 2.1.1, 2.5.7, 4.1.2)
The fix: Keep the drop, and give the zone a control that opens the file picker: either a Choose files button that calls the hidden file input's click(), or the file input itself, styled with ::file-selector-button, or a label styled as a button over a file input hidden only from sight, with the focus ring drawn on the label.
Test with: keyboard, screen reader, mouse, touch, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F108 https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

An expense claim form asks for the amount, then the receipts. The receipts box is a dashed rectangle with an upload arrow and the words "Drag and drop receipts here". With a mouse, you drag a PDF out of a folder, let go over the box, and its name appears under it. Press Tab from the Amount field, and focus goes straight to Submit claim: nothing in the box takes focus, and there is no button to press. A screen reader reads "Drag and drop receipts here" as a line of text. On a phone, the receipt is in another app, so a drag means holding it while switching to the browser, where the phone allows that at all.

The same box often says "or click to choose files" as well, and a click anywhere on it opens the file picker. A mouse user never meets a problem. Tab still skips the box, Enter does nothing, and a screen reader reads its words as text, with no role to say that the box can be pressed.

## Why it fails

The box is a `div` that a script listens to. In the first form, it listens only for a drop. Adding a file then needs a dragging movement: press on the file, carry it across, let go. WCAG 2.5.7 asks that anything done by dragging can also be done with single clicks or taps, and failure F108 is a function with no such way; here, there is none. A keyboard can't carry a file from a folder into the page either, and nothing in the box takes focus, so the receipts can't be added from the keyboard at all, which fails 2.1.1.

In the second form, the box also holds a file input hidden with the `hidden` attribute, and the script opens it with `click()` when the box is clicked. A single click now adds a file, so 2.5.7 passes, but the box that takes the click is still a `div`: it is not in the Tab order and ignores Enter and Space, which fails 2.1.1. It acts on a click, so it is a control, yet it exposes no role and no name; a screen reader reads its words as text and never says there is something to press, which fails 4.1.2. The file input that does the work is hidden from everyone, so it can't stand in.

## Who is affected

An upload is often the step a claim, an application or a support request can't be sent without, so one box that only takes drops can stop the whole form. People who can drag, but slowly, meet a longer version of the same problem: the folder and the form have to be side by side on screen, and the drop has to land inside the box.

## What automation and AI miss

A scanner sees a `div`, a paragraph and, in the second form, a file input hidden from everyone, which no rule asks for a label. The click and drop listeners are in the script, not the markup, so nothing says the box is a control. EaseWeb's audit sends a review lead for a box with a drop listener of its own, for the box around a file input hidden with `display: none`, `visibility: hidden` or the `hidden` attribute, and for a dashed or dotted box whose words ask for files to be dragged or dropped, whenever nothing in it takes focus and nothing beside it is a file input or a button to choose or upload files. Listeners that a framework puts on the root are not seen, and a button elsewhere on the page may open the same input, so a person checks each one. Asked to make the zone accessible, AI tools often add `tabindex="0"` to the box, as some upload libraries do. The box then takes focus, but with no role and no name a screen reader says little or nothing there, and Enter does nothing until a key handler is written too. The audit sends a review lead for such a box as well.

## Before: a box that takes only dropped files

```html
<div class="field">
  <span class="field-label" id="receipts-label">Receipts</span>
  <div class="drop-zone">
    <p>Drag and drop receipts here</p>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // lists each file's name in .file-list
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
```

- A drop is the only way in.
- Nothing in the box takes focus.
- No click or key adds a file.

## Before: a click on the box opens a hidden file input

```html
<div class="field">
  <span class="field-label" id="receipts-label">Receipts</span>
  <div class="drop-zone">
    <p>Drag and drop receipts here, or click to choose files</p>
    <input type="file" id="receipts" multiple hidden>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
  cursor: pointer;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // lists each file's name in .file-list
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
zone.addEventListener('click', () => input.click());
input.addEventListener('change', () => addFiles(input.files));
```

The mouse gets a click as well as the drop, but the box that takes the click is a `div`:

- Tab passes it.
- Enter and Space do nothing.
- It has no role or name.
- The real file input is hidden from everyone.

## After: a Choose files button that opens the hidden input

```html
<div class="field">
  <span class="field-label" id="receipts-label">Receipts</span>
  <div class="drop-zone">
    <p>Drag and drop receipts here, or</p>
    <button type="button" class="choose" aria-describedby="receipts-label">Choose files</button>
    <input type="file" id="receipts" multiple hidden>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
.choose {
  border: 0;
  border-radius: 999px;
  background: #1f5fbf;
  color: #fff;
  padding: 8px 18px;
  font: inherit;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // lists each file's name in .file-list
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
zone.querySelector('.choose').addEventListener('click', () => input.click());
input.addEventListener('change', () => addFiles(input.files));
```

1. Add a native Choose files button to the box. The browser puts it in the Tab order, activates it with Enter or Space, and gives it a role.
2. Let its own text name it, and make the field's label its description, so it is read something like "Choose files, button, Receipts".
3. Move the click that opens the input from the box to the button, and take `cursor: pointer` off the box. One click, tap or key press opens the file picker, and voice control users can say "Click Choose files".

This fixes both Befores and passes 2.1.1, 2.5.7 and 4.1.2. The input stays hidden with `hidden`: calling `click()` on it still opens the picker in current browsers, and the button does everything a person needs. The drop works as before.

## After: the file input itself, inside the zone

```html
<div class="field">
  <label class="field-label" for="receipts">Receipts</label>
  <div class="drop-zone">
    <p>Drag and drop receipts here, or</p>
    <input type="file" id="receipts" multiple>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
.drop-zone input::file-selector-button {
  border: 0;
  border-radius: 999px;
  background: #1f5fbf;
  color: #fff;
  padding: 8px 18px;
  font: inherit;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // lists each file's name in .file-list
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
input.addEventListener('change', () => addFiles(input.files));
```

1. Leave the file input in the page, and make the field's name its `label`. It takes focus, opens with Enter or Space, and is read as a button, or in some browsers a group holding one, named by its label, "Receipts".
2. Remove the `click()` call: nothing opens it by script.
3. Style the button part with `::file-selector-button`. That styles it, but not its words, or the text the browser writes beside it: Chrome writes "Choose Files" and "No file chosen", Firefox "Browse…" and "No files selected.", each in the browser's own language, not the page's.

This fixes both Befores and passes 2.1.1, 2.5.7 and 4.1.2. The browser's text also says "No file chosen" after a drop, because a drop on the box doesn't fill the input, so it disagrees with the list under the box unless the script copies the dropped files into it.

## After: a label styled as a button, over an input hidden only from sight

```html
<div class="field">
  <span class="field-label" id="receipts-label">Receipts</span>
  <div class="drop-zone">
    <p>Drag and drop receipts here, or</p>
    <input type="file" id="receipts" class="visually-hidden" multiple aria-describedby="receipts-label">
    <label class="choose" for="receipts">Choose files</label>
  </div>
  <ul class="file-list"></ul>
</div>
```

```css
.drop-zone {
  border: 2px dashed #8a94a6;
  border-radius: 12px;
  padding: 24px;
  text-align: center;
}
.choose {
  border-radius: 999px;
  background: #1f5fbf;
  color: #fff;
  padding: 8px 18px;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.drop-zone input:focus-visible + .choose {
  outline: 3px solid #1f5fbf;
  outline-offset: 3px;
}
```

```js
const zone = document.querySelector('.drop-zone');
function dropFiles(event) {
  event.preventDefault();
  addFiles(event.dataTransfer.files); // lists each file's name in .file-list
}
zone.addEventListener('dragover', (event) => event.preventDefault());
zone.addEventListener('drop', dropFiles);
const input = document.getElementById('receipts');
input.addEventListener('change', () => addFiles(input.files));
```

1. Hide the input from sight but not from the keyboard, with `visually-hidden`. Tab still stops on it, and Enter or Space opens the picker.
2. Put a `label` styled as a button straight after it. A click on the label opens the picker with no script.
3. Make the field's name the input's description. A screen reader reads the input as a button, or in some browsers a group holding one, named by the label, "Choose files", with "Receipts" after it.
4. Draw the focus ring on the label from the input's `:focus-visible`, because the input itself draws nothing.

This fixes both Befores and passes 2.1.1, 2.5.7 and 4.1.2, as long as the input is hidden this way, never with `display: none`, `visibility: hidden` or `hidden`: any of those takes it out of the Tab order, and the label then works for a pointer alone. The focus ring depends on the label coming straight after the input.

## Which option to use

All three pass, and all three keep the drop. Use the button unless you have a reason not to: it is the only one whose words are the page's own in every browser, and whose focus ring is on the element that has focus, for one line of script. The file input itself needs no script and no hidden element, so it suits a plain form, or a page that can't run script before the form is used; but the browser writes its button's words and the "No file chosen" beside it in its own language, and that text has to be kept in step with the files that were dropped. The label over a hidden input also needs no script and looks exactly as designed, but the focus ring and the thing that takes focus are two elements tied by a CSS selector, so a change to the markup can lose the ring without anything else breaking.

## Implementation decisions

Keep the drop. It is a quick way in for people who have the folder open beside the form, and the button makes it a shortcut rather than the only way. A click on the whole box can stay too, as a bigger target for the mouse, as long as the button is there; let the box's own click ignore clicks that start on the button, or the picker can be asked to open twice.

Several smaller decisions decide how the zone works once files are in it:

- Saying what was added: after a drop or after the picker closes, list the files in the zone and say so in a status region, such as "2 files added: taxi-receipt.pdf, hotel-invoice.pdf", as [status messages that are shown but never announced](/learn/status-messages) explains. Focus returns to the button when the picker closes, so a screen reader user would otherwise have to go looking.
- Removing a file: give each listed file its own Remove button, named with the file ("Remove taxi-receipt.pdf"), and move focus to the next file or back to Choose files after a removal.
- Required uploads: the browser can't move focus to a file input hidden with `hidden` to show its own message, so `required` on it can stop the form with nothing shown. Check the files yourself, and put the message beside the button, tied to it, as [form errors that don't say how to fix them](/learn/accessible-form-errors) shows.
- Phones: the button opens the same input, so a phone still offers its own choices, such as the camera, the photo library or files. `accept="image/*,.pdf"` narrows what can be picked; say the same types in the zone's words, so nobody learns them from an error.
- Upload libraries: Dropzone.js makes the whole box clickable, and react-dropzone gives the box a Tab stop of its own; check what each one exposes, and add a real button if the box is all there is.

## Verify the fix

1. Put the mouse aside. Press Tab from the field before the zone, and confirm that focus stops on the zone's button or file input, with a ring you can see.
2. Press Enter, and confirm the file picker opens; choose two files and confirm both are listed. Do it again with Space.
3. With a screen reader, Tab to the control and confirm it is read as a button with its name, and that the files chosen can be found after the picker closes.
4. With a mouse, using single clicks only, add a file. Repeat with taps on a phone, and with voice control by saying the control's visible text.
5. Drag a file from a folder onto the zone, and confirm it is still added and listed with the others.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the upload component or its library changes.

## Limits

This is a starting pattern, not a guarantee for every application. Moving cards and list items by dragging is covered in [drag-and-drop boards that only a mouse can use](/learn/drag-and-drop), and asking what an uploaded photo shows in [image uploads that never ask for alt text](/learn/alt-text-prompts). A progress bar for a large upload has its own needs: see [progress bars and meters that screen readers can't read](/learn/progress-bars). Pasting a file, or uploading a whole folder, is not covered here. The file picker belongs to the operating system, and its keys and screen reader support differ between systems: test the complete journey with your target assistive technology.

## What screen readers do (tested by hand by EaseWeb)

`aria-describedby`: Gives an element a description, such as a hint or an error, from text elsewhere on the page.

| Screen reader | Browser and system | Tab to it | Down Arrow |
| --- | --- | --- | --- |
| JAWS | Chrome, Windows 11 | Not tested | Not tested |
| NVDA | Chrome, Windows 11 | Not tested | Not tested |
| JAWS | Edge, Windows 11 | Not tested | Not tested |
| NVDA | Firefox, Windows 11 | Not tested | Not tested |
| NVDA | Edge, Windows 11 | Not tested | Not tested |
| JAWS | Firefox, Windows 11 | Not tested | Not tested |
| VoiceOver | Safari, macOS | Not tested | Not tested |
| VoiceOver | Safari, iOS | Not tested | Not tested |
| TalkBack | Chrome, Android | Not tested | Not tested |

https://easeweb.dev/learn/screen-reader-support

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
