# File Upload Dropzone

> A drag-and-drop upload area with a real file input behind its button: dropped files land as rows on a stagger, the progress bar catches each one and counts up, and a finished upload collapses the bar into a check mark that draws itself.

Canonical: https://gsapvault.com/ui-elements/file-upload-dropzone
Live demo: https://gsapvault.com/demos/file-upload-dropzone/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | intermediate |
| Plugins | Flip |
| Techniques | progress-bar, count-up, stagger, flip-layout, svg-line-draw, micro-interaction |
| Uses Lenis | No |

## Overview

An upload dropzone that shows every stage of the job. The zone's dashed hairline turns to the accent and its icon lifts the moment a file is dragged over it, and on release the files land as rows on a stagger while each progress bar sweeps out from the left to catch its row. The percentage counts up on tabular numerals, and when a file finishes the bar collapses into a check mark that draws itself into the space the bar leaves, so completion is one movement rather than a swap of icons.

Every other state is built too. A file whose type or size the markup does not accept is rejected before it is ever sent and takes a red row naming the reason; an upload that fails offers Retry, which restarts that file alone. Removing a row lifts it out of the flow, collapses its height, and reflows the survivors with GSAP Flip so nothing jumps.

The demo's progress is simulated by a GSAP tween, and swapping in real upload progress is a single call: setUploader receives the file and a progress callback and returns a promise, so XMLHttpRequest upload progress events or a fetch stream drop straight in. Accepted types and the size ceiling are data attributes on the component root. Real buttons throughout, a progressbar role with aria-valuenow kept in step, and a polite live region announcing what landed, finished, failed or was removed. One HTML file, one stylesheet, one script, vanilla JavaScript, GSAP and the Flip plugin, no framework. Light and dark themes come from one set of custom properties switched with a single data-variant attribute.

## Features

- Real drag-and-drop plus a real multiple file input behind a Choose files button, so it is one tab stop, not two
- The zone's dashed hairline becomes the accent and the icon lifts on dragenter, settling back with an elastic ease
- Rows land on a stagger while each progress bar scales out from the left to catch its row
- Percentage counts up on tabular numerals and drives aria-valuenow on a progressbar role
- Done collapses the bar and draws a check mark in with strokeDashoffset
- Client-side validation from data-accept and data-max-size, with the reason written into the row
- Failed uploads keep a Retry button that restarts that file alone; rejections do not offer one
- Removing a row collapses it out of flow while GSAP Flip reflows the rest
- Image files preview from URL.createObjectURL, revoked when the row goes
- setUploader(fn) swaps the simulated progress for XHR or fetch upload events
- Polite aria-live announcements for added, uploaded, failed and removed files
- Light and dark themes on one set of custom properties

## Use Cases

- Document and asset uploads inside an admin or dashboard product
- Support and claims forms where the buyer must see each attachment succeed or fail
- Media libraries and CMS screens that accept several images at once
- Onboarding and verification flows that reject the wrong file type before sending it
- Any upload field where the current answer is a spinner and a page reload

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### The drop landing

Entering and leaving the zone are counted rather than toggled, so crossing a child element inside the zone cannot flicker the highlight off. The landing itself is two movements sharing one stagger: the rows come up from sixteen pixels, and a moment later each progress track runs in from the left so the bar appears to catch its own row. A near miss outside the zone is swallowed too, which is what stops the browser navigating to the dropped file.

### Progress, and replacing it

Progress is a single value from 0 to 100 that drives the bar, the percentage text and the announced value together, so the visual and the spoken number can never disagree. The demo's uploader simply animates that value. A buyer replaces it with one documented hook that receives the file and a progress callback and returns a promise: resolving marks the row done, rejecting moves it to the error state with the message shown in the row.

### Done, error and retry

Completion collapses the track while the check mark scales up in the cell the percentage vacated and draws itself in, so the bar reads as becoming the check. Rejections are decided before any upload starts, from the accepted types and size limit on the component root, and carry no Retry because the file will never pass; a failed upload keeps Retry, which restarts that one file.

### Removing a row

The departing row is pinned in place and taken out of the flow instantly, so its own collapse cannot drag its neighbours down; the survivors then slide into the gap while the list's height eases from its old value to its new one so the card does not jump. Under reduced motion the row is simply removed.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### What's Included

- `index.html`: the demo page and the markup to copy
- `assets/style.css`: component styles, plain CSS custom properties for colours
- `assets/script.js`: readable, commented source with an `onReady` guard

- **Real drag-and-drop and a real file input.** The input stays in the DOM and does the work; the Choose files button fronts it, so the component is one tab stop rather than two.
- **The zone reacts before the drop.** `dragenter` and `dragleave` are counted, not toggled, so crossing a child inside the zone cannot flicker the highlight off. The dashed hairline becomes the accent and the icon lifts, settling back on an elastic ease.
- **Progress you can replace in one call.** `window.fileDropzone.setUploader(fn)` swaps the demo's simulated tween for your own XHR or fetch upload progress.
- **Every state built, not just the happy one.** Uploading, done, rejected (non-retryable), failed (retryable) and removed, each with its own row treatment.
- **Removal reflows with Flip.** The departing row leaves the flow and collapses; the survivors travel into the gap and the card's height is tweened rather than jumping.
- **Roles:** `role="list"` on the list, `role="progressbar"` with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` on each track, `role="status"` with `aria-live="polite"` on the announcement line, and real `<button>` elements for Choose files, Retry and Remove.

### Quick Start

**1. Add to your `<head>`:**

_Code snippet omitted: it ships with the download._

**2. Add before the closing `</body>` tag:**

_Code snippet omitted: it ships with the download._

**3. Copy the component markup from `index.html` into your page.**

The script finds everything through `data-` attributes, never through class names, so you can restyle freely as long as these stay:

| Attribute | On | Required |
|-----------|----|----------|
| `data-dropzone` | the component root | yes |
| `data-dropzone-zone` | the drop target | yes |
| `data-dropzone-list` | the `<ul>` the rows go into | yes |
| `data-dropzone-input` | the `<input type="file">` | for the button path |
| `data-dropzone-browse` | the Choose files button | for the button path |
| `data-dropzone-icon` | the icon that lifts on dragover | optional |
| `data-dropzone-status` | the `aria-live` announcement line | optional |

Two options are read off the root:

_Code snippet omitted: it ships with the download._

`data-accept` takes the same syntax as the input's own `accept` attribute — extensions, exact MIME types, and wildcards such as `image/*` — and an empty or absent value accepts everything. `data-max-size` is in bytes; omit it for no ceiling. Both are validated in the browser before anything is sent, which is a courtesy to the person uploading, not a security control: validate again on the server.

The row markup is generated by the script, so there is nothing to copy for it. The `<ul>` should ship empty.

The showcase toolbar at the top of `index.html`, its inline `<style>` and `<script>`, and the `.stage` wrapper are demo furniture, not part of the component; leave them behind.

### Wiring Up Your Own Uploads

The demo's progress is simulated by a GSAP tween, which is why it needs no server. Swap in your transport with one call:

_Code snippet omitted: it ships with the download._

`item` is `{ id, name, size, type, file }`. `file` is the real `File` object when one exists. `onProgress` takes a number from 0 to 100 and is what moves the bar, the percentage and `aria-valuenow` together. Resolving marks the row done; rejecting moves it to the error state and shows the thrown message in the row, with a Retry button that restarts that one file.

The rest of the API:

| Call | What it does |
|------|--------------|
| `fileDropzone.add(descriptors)` | Adds rows from plain objects (`{ name, size, type, thumb }`) with no `File` behind them. This is how the demo's Simulate drop control works |
| `fileDropzone.setFailNext(true)` | Makes the next file added fail; the demo's Fail next toggle |
| `fileDropzone.clear()` | Removes every row |
| `fileDropzone.items` | The live `Map` of items, keyed by id |

### Keyboard & Accessibility

| Key | What it does |
|-----|--------------|
| `Tab` / `Shift+Tab` | Moves through Choose files, then each row's Retry (when it is showing) and Remove, in visual order |
| `Enter` / `Space` on Choose files | Opens the file picker |
| `Enter` / `Space` on Retry | Restarts that file's upload |
| `Enter` / `Space` on Remove | Removes that row and reflows the list |

- The zone as a whole is **not** a button and takes no focus: it is a drop target, and the button inside it is the keyboard and pointer route to the same thing. The file input carries `tabindex="-1"` and `aria-hidden="true"` so it is not a second, invisible tab stop.
- The zone carries an `aria-label` naming both routes. Each track is a `progressbar` labelled with its file name and its `aria-valuenow` is written on the same call that paints the bar, so the two can never disagree.
- Remove and Retry are labelled with the file name (`aria-label="Remove invoice.pdf"`, and a visually hidden "uploading invoice.pdf" after Retry), so they are unambiguous out of context.
- A `role="status"` region with `aria-live="polite"` announces what was added, what finished, what failed and why, and what was removed.
- `prefers-reduced-motion`: the same end states with durations at zero. Rows appear in place, progress resolves immediately, the check is drawn instantly and removal is instant. Nothing is disabled, and nothing is left hidden.
- Hover treatments are inside `@media (hover: hover)` so a tap on a touch screen cannot pin one open.

### How It Works

**The drop landing.** `dragenter` and `dragleave` maintain a depth counter rather than a boolean, because a drag crossing a child element inside the zone fires a `dragleave` that would otherwise turn the highlight off mid-drag. The landing is two tweens sharing one stagger: the rows rise from 16px on `power3.out`, and a moment later each progress track scales from `scaleX: 0` with a left transform origin so the bar appears to run in underneath its own row. `dragover` and `drop` are also swallowed at `window` level, which is what stops a near miss from navigating the browser away to the dropped file.

**Progress as a custom property.** A value between 0 and 100 is written to `--progress` on the row, and the bar's width is `calc(var(--progress) * 1%)`. The same function writes the percentage text and `aria-valuenow`, so one call keeps all three in step and the bar has no layout thrash from JavaScript-set widths.

**The done morph.** Completion tweens the track's height to zero while the check mark scales up in the cell the percentage has just vacated, and the check's path draws in from a `strokeDashoffset` equal to its own `getTotalLength()`. The two share a grid cell deliberately: the bar reads as becoming the check rather than being replaced by it.

**Errors.** Rejections are decided before anything is sent, from `data-accept` and `data-max-size`, and are marked `data-retryable="false"` because that file will never pass; an upload that fails is marked retryable. When an error is applied the row element itself is deliberately left out of the kill list, because a file rejected on arrival is still mid-landing and killing that tween would leave the row present in the DOM at zero opacity.

**Removal.** `Flip.getState` records the survivors first. The departing row is then pinned to its measured position as an absolutely positioned box, which takes it out of the flow in one frame so its own height collapse cannot drag its neighbours down with it; `Flip.from` animates the survivors into the gap while the list's height is tweened from its old value to its new one so the card does not snap shorter. Object URLs made for image previews are revoked as the row goes.

### Customisation

- Accepted types and the size ceiling: `data-accept` and `data-max-size` on the component root, plus the hint line and the input's own `accept` attribute.
- The component's width is set by its container; the demo gives it 640px, which suits a panel in a form. It has no minimum of its own.
- Row density: `padding` and `min-height` on `.fu-row`, and the `gap` on `.fu-list`.
- The bar's thickness and the check's size: `height` on `.fu-row__track` and `width`/`height` on `.fu-row__check`.
- Colours live in `assets/style.css` as custom properties per theme.
- Easing and durations are the `gsap.to` / `gsap.fromTo` calls in `assets/script.js`; the simulated upload's duration is in the default `uploader` function and is irrelevant once you call `setUploader`.

### Themes

Ships in two themes, `light` and `dark` (the demo defaults to dark). Pick one by setting the attribute:

_Code snippet omitted: it ships with the download._

In your own page the attribute can live on the component's own wrapper instead of `<body>`; nothing in `assets/script.js` reads the theme name, so the CSS is the whole mechanism. The demo's toggle and the `?variant=light` URL parameter only set that attribute.

Every colour the component and the demo ground use is a custom property in those two blocks, so re-valuing one short list is the whole of a rebrand:

`--ground`, `--ground-2`, `--raised`, `--hover`, `--ink`, `--ink-2`, `--ink-3`, `--line`, `--line-strong`, `--accent`, `--accent-ink`, `--accent-text`, `--accent-wash`, `--accent-hover`, `--danger`, `--danger-text`, `--danger-wash`, `--success-text`, `--check`, `--track-bg`, `--zone-bg`, `--row-bg`, `--thumb-bg`, `--shadow`, `--font`, `--radius`, `--radius-sm`, `--radius-xs`.

To add a third theme, copy either `[data-variant="..."]` block, rename the value and change the properties. No selector override is ever needed.

### Requirements

- GSAP 3.12+ plus the Flip plugin (demo uses 3.15.0)
- No build step, no framework

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy File Upload Dropzone](https://gsapvault.com/ui-elements/file-upload-dropzone)
- [The Vault (£99 one-time, best value): everything in the catalogue, including future releases](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
