Command Palette

Search for a command to run...

Hectal
PHASE 24Advanced ~33 min· topic 7 of 10

Topic 24.7

Uploads, Drag and Drop and Rich Inputs

In one line

File uploads need a file input and a drop zone that validate type and size, instant previews through object URLs that are freed afterwards, a progress bar, cancel and retry; reorderable lists need pointer events for mouse, pen and touch plus a keyboard way to move items with spoken announcements.

Think of it like this

Posting a parcel at a counter. You hand over the box (pick a file), the clerk checks the size and what's inside (validation), shows you a label to confirm (preview), and a tracking page shows how far it has travelled (progress). You can cancel before it leaves. Rearranging items on a shelf is like a sortable list: you can lift and slide things with your hands, but someone who can't reach should be able to say "move the milk two places down" and get the same result (keyboard alternative).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

File / FileList
The browser's objects for chosen files: a File has a name, size, type and the data; a FileList is a list of them.
Object URL
A temporary blob: address created by URL.createObjectURL that points at a file in memory.
revokeObjectURL
Frees an object URL's memory once you no longer need it.
DataTransfer
The object that carries dragged data, including files, in drag-and-drop events.
Upload progress
How many bytes have been sent so far, reported by XMLHttpRequest's upload.onprogress.
Presigned URL
A short-lived URL from your server that lets the browser upload straight to cloud storage.
Pointer events
One set of events (pointerdown, pointermove, pointerup) for mouse, pen and touch.
touch-action
A CSS property that tells the browser not to scroll or zoom when a touch starts on an element.

Step by step

01Accept files two ways

A drop zone for mouse users, plus a real button that clicks a hidden file input. Both feed the same add(files) function.

Accept files two waysdiagram
Rendering diagram…

02Preview with object URLs, then free them

Create the URL in an effect keyed on the file and revoke it in the cleanup. Removing a file or unmounting the preview frees its memory.

03Track each upload

State holds { id, name, progress, status } for display; a ref holds each upload's controller for cancelling.

upload.jswhole filejavascript
function upload(file, { onProgress, signal }) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/api/upload");
    xhr.upload.onprogress = (e) => e.lengthComputable && onProgress(Math.round((e.loaded / e.total) * 100));
    xhr.onload = () => (xhr.status < 300 ? resolve(JSON.parse(xhr.responseText)) : reject(new Error("HTTP " + xhr.status)));
    xhr.onerror = () => reject(new Error("Network error"));
    xhr.onabort = () => reject(new DOMException("Upload cancelled", "AbortError"));
    signal.addEventListener("abort", () => xhr.abort());
    const body = new FormData();
    body.append("file", file);
    xhr.send(body);
  });
}

04Reorder with pointer events

Pointer down on the handle starts a drag; each move computes the target index from the distance; pointer up ends it and announces the result.

Reorder with pointer eventsdiagram
Rendering diagram…

05Add the keyboard path

Space grabs, arrows move, Space drops, Escape restores the saved order. Every step updates an aria-live message.

Try it yourself

  1. 1

    Raise the size limit

    In the drop zone, set MAX_BYTES to 3 bytes. Predict the first log line: logo.svg is now rejected as too large. Change it back.

  2. 2

    Cancel the photo instead

    In the upload queue, change the script to cancel photo.jpg after 100ms. Predict both log lines. (Photo cancelled then done; video done both times.)

  3. 3

    Escape cancels a keyboard move

    In the sortable list script, replace the last " " with "Escape". Predict the order and the announcement: the original order comes back and "Move cancelled" is announced.

Code & diagrams

A drop zone and file button with validation and object-URL previews New tab

The script drops two files by dispatching a real `DragEvent` with a `DataTransfer`. The text file is rejected; the SVG gets a `blob:` preview that's revoked when the preview unmounts. Drag a file from your computer onto the zone, or use the button.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

accepted logo.svg | rejected notes.txt: not an image
previews: 1, src starts with blob:
An upload queue with progress, cancel and retry New tab

Each file gets its own `AbortController`, kept in a ref. Cancelling the video stops its fake upload; Retry starts it again with a new controller. Pick some files yourself to watch the bars.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

photo.jpg done, video.mp4 cancelled
photo.jpg done, video.mp4 done
A sortable list: pointer drag plus a keyboard alternative New tab

Keyboard: Space, ArrowDown, ArrowDown, Space moves Milk to position 3, and focus stays on its handle because items are keyed by name. Pointer: dragging Apples up three rows puts it first. Try both: drag a handle, or Tab to one and use Space and the arrows.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Keyboard: Bread, Eggs, Milk, Apples | focus on Reorder Milk
Announced: Milk dropped at position 3
Pointer: Apples, Bread, Eggs, Milk
The libraries teams use (not live: install react-dropzone and @dnd-kit/core + @dnd-kit/sortable)jsx
import { useDropzone } from "react-dropzone";
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

export function Dropzone({ onFiles }) {
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: onFiles,
    accept: { "image/*": [".png", ".jpg", ".jpeg"] },
    maxSize: 1024 * 1024,
  });
  return (
    <div {...getRootProps()}>
      <input {...getInputProps()} />
      {isDragActive ? "Drop the files here" : "Drag images here, or click to choose"}
    </div>
  );
}

function SortableItem({ id }) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
  return (
    <li ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}>
      <button {...attributes} {...listeners} aria-label={"Reorder " + id}>⠿</button> {id}
    </li>
  );
}

export function SortableList({ items, setItems }) {
  // Pointer for mouse/touch, Keyboard for Space + arrows; dnd-kit announces moves itself.
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );
  const onDragEnd = ({ active, over }) => {
    if (over && active.id !== over.id) {
      setItems((list) => arrayMove(list, list.indexOf(active.id), list.indexOf(over.id)));
    }
  };
  return (
    <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        <ul>{items.map((id) => <SortableItem key={id} id={id} />)}</ul>
      </SortableContext>
    </DndContext>
  );
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forgetting preventDefault in dragover

In the drop zone, change onDragOver to () => setOver(true) (no e.preventDefault()).

terminal
$ Preview (drag a real file onto the zone)
── what you'll see ──
The cursor shows "not allowed", the drop event never fires, and on some browsers the file opens in the tab, replacing the page.

Break #2

Never revoking object URLs

Create previews with src={URL.createObjectURL(file)} directly in JSX.

terminal
$ Memory panel
── what you'll see ──
Memory grows with every render and every file: each render creates a new blob: URL that is never freed, and the image reloads each time.

Break #3

Drag-only reordering

Remove the onKeyDown handler from the sortable list.

terminal
$ Keyboard / screen reader test
── what you'll see ──
Keyboard users can focus the handles but nothing happens: they can't reorder the list at all.

Myth vs fact

Myth

accept on the file input stops wrong files.

Fact

It only filters the picker; users can switch to "All files", and drops ignore it. Validate in code and again on the server.

Myth

A file's type tells you what's really inside.

Fact

It's guessed from the extension. Servers must inspect the content (magic bytes), limit size, and store uploads away from executable paths.

Myth

HTML5 draggable is the right tool for sortable lists.

Fact

It has no touch support on many devices, limited visuals and no keyboard path. Pointer events (or dnd-kit) work better.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Uploading straight to object storage (S3, GCS, R2) with a presigned URL or a multipart upload keeps big files off your servers. The server only signs the request and later verifies the stored object. Validate size in the signature policy, not only in the browser.

  • ▸

    For resumable uploads (flaky mobile networks), the tus protocol and S3 multipart split files into chunks that can be retried individually; progress is the sum of finished chunks plus the current one.

  • ▸

    Images can be resized and compressed in the browser before upload with createImageBitmap and a canvas (OffscreenCanvas in a worker), which saves bandwidth for phone photos of several megabytes.

  • ▸

    During a pointer drag, reordering state on every pointermove is fine for short lists; for long or virtualised lists, move a lightweight overlay with transform (no re-render per move, like dnd-kit's DragOverlay) and commit the new order once on drop.

Remember this

  1. 1

    File inputs. <input type="file"> opens the system file picker. accept="image/*,.pdf" filters what the picker shows (only a hint: always validate again), multiple allows several files, and capture asks phones to open the camera. File inputs are uncontrolled in React: you can't set their value, only read e.target.files, a FileList of File objects (each has name, size in bytes, type such as "image/png" and lastModified). Reset e.target.value = "" after reading, otherwise choosing the same file again fires no change event. To style the picker, hide the input and trigger it from your own button (or style a <label> for it).

  2. 2

    Validation and previews. Check type and size before uploading and show clear errors per file. The server must check again: type comes from the file extension and can lie. For an instant preview, URL.createObjectURL(file) gives a blob: URL pointing at the file in memory; use it as an <img src> or <video src>. Each URL keeps the file in memory until you call URL.revokeObjectURL(url), so create it in an effect and revoke it in the cleanup. (FileReader.readAsDataURL also works but copies the whole file into a long base64 string.)

  3. 3

    Drag-and-drop zones. The browser's drag events: dragenter, dragover (fires continuously while over the zone), dragleave and drop. You must call preventDefault() in dragover (and drop), otherwise the browser refuses the drop or opens the file in the tab. Dropped files are in e.dataTransfer.files. dragleave also fires when the pointer moves onto a child element, so a hover highlight can flicker; count enters and leaves, or check e.currentTarget.contains(e.relatedTarget). A drop zone is never the only way in: keep a real button that opens the file picker, for keyboard and touch users (phones don't drag files).

  4. 4

    Upload progress and cancelling. fetch can't report upload progress, so progress bars use XMLHttpRequest: xhr.upload.onprogress = (e) => setProgress(e.loaded / e.total), xhr.abort() to cancel. Keep each upload's AbortController or xhr in a ref (it's not something to render), keep { progress, status } per file in state, and update with the updater form because many progress events arrive. Large files use chunked or resumable uploads (tus, S3 multipart), often straight to storage with a presigned URL so the file doesn't pass through your app server. Show <progress> with an accessible name.

  5. 5

    Sortable lists with pointer events. Pointer events (pointerdown, pointermove, pointerup) cover mouse, pen and touch with one API. On pointerdown remember the start position; on pointermove turn the distance moved into a target index (Math.round(dy / rowHeight)) and reorder; on pointerup finish. Listen to move/up on window (or use setPointerCapture) so the drag continues outside the item, and set touch-action: none on the handle so touch drags don't scroll the page. The HTML5 draggable API is awkward for lists (no touch support, limited styling), which is why libraries use pointer events.

  6. 6

    The keyboard alternative and announcements. Dragging is impossible for keyboard and many assistive-technology users, and WCAG 2.2 (criterion 2.5.7, Dragging Movements) requires a non-dragging way. The common pattern: a focusable handle button; Space or Enter picks the item up, ArrowUp/ArrowDown move it, Space drops it, Escape cancels and restores the old order. Announce every step in an aria-live region ("Milk grabbed", "moved to position 3", "dropped"). Simpler alternatives also count: Move up / Move down buttons, or a position menu. Key items by a stable id so React moves the DOM nodes and focus stays on the handle.

Explain it without notes

01

How do you show an instant preview of a chosen image, and what must you clean up?

02

How do you build an accessible drag-and-drop upload area?

03

How would you implement upload progress and cancel in React?

04

How do you make a sortable list accessible?

Practice

01

Add a Remove button to each preview in the drop zone example, and make sure its object URL is freed.

02

Add Move up and Move down buttons to the sortable list as an extra non-drag alternative.

Trade-offs

  • ↔

    A hand-built drop zone and sortable list are small and teach the mechanism, but libraries (react-dropzone, dnd-kit) handle touch quirks, auto-scrolling, collision detection, announcements and edge cases across browsers.

  • ↔

    Uploading through your server is simple and lets you inspect files on the way, but costs bandwidth and memory; direct-to-storage uploads scale better but need signed URLs and a verification step.

Done when you can

  • I can accept files from a styled button and a drop zone, and validate type and size.

  • I can preview files with object URLs and revoke them.

  • I can show per-file upload progress with cancel and retry.

  • I can reorder a list with pointer events, including touch.

  • I can offer a keyboard way to reorder with live announcements.