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
Filehas a name, size, type and the data; aFileListis a list of them. - Object URL
- A temporary
blob:address created byURL.createObjectURLthat 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'supload.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.
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.
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.
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
Raise the size limit
In the drop zone, set
MAX_BYTESto 3 bytes. Predict the first log line:logo.svgis now rejected as too large. Change it back. - 2
Cancel the photo instead
In the upload queue, change the script to cancel
photo.jpgafter 100ms. Predict both log lines. (Photo cancelled then done; video done both times.) - 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
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
Console
Nothing logged yet.Expected console output
accepted logo.svg | rejected notes.txt: not an image
previews: 1, src starts with blob: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
Console
Nothing logged yet.Expected console output
photo.jpg done, video.mp4 cancelled
photo.jpg done, video.mp4 doneKeyboard: 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
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, Milkimport { 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()).
Break #2
Never revoking object URLs
Create previews with src={URL.createObjectURL(file)} directly in JSX.
Break #3
Drag-only reordering
Remove the onKeyDown handler from the sortable list.
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
createImageBitmapand a canvas (OffscreenCanvasin a worker), which saves bandwidth for phone photos of several megabytes. - ▸
During a pointer drag, reordering state on every
pointermoveis fine for short lists; for long or virtualised lists, move a lightweight overlay withtransform(no re-render per move, like dnd-kit'sDragOverlay) and commit the new order once on drop.
Remember this
- 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),multipleallows several files, andcaptureasks phones to open the camera. File inputs are uncontrolled in React: you can't set their value, only reade.target.files, aFileListofFileobjects (each hasname,sizein bytes,typesuch as"image/png"andlastModified). Resete.target.value = ""after reading, otherwise choosing the same file again fires nochangeevent. To style the picker, hide the input and trigger it from your own button (or style a<label>for it). - 2
Validation and previews. Check
typeandsizebefore uploading and show clear errors per file. The server must check again:typecomes from the file extension and can lie. For an instant preview,URL.createObjectURL(file)gives ablob: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 callURL.revokeObjectURL(url), so create it in an effect and revoke it in the cleanup. (FileReader.readAsDataURLalso works but copies the whole file into a long base64 string.) - 3
Drag-and-drop zones. The browser's drag events:
dragenter,dragover(fires continuously while over the zone),dragleaveanddrop. You must callpreventDefault()indragover(anddrop), otherwise the browser refuses the drop or opens the file in the tab. Dropped files are ine.dataTransfer.files.dragleavealso fires when the pointer moves onto a child element, so a hover highlight can flicker; count enters and leaves, or checke.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
Upload progress and cancelling.
fetchcan't report upload progress, so progress bars useXMLHttpRequest:xhr.upload.onprogress = (e) => setProgress(e.loaded / e.total),xhr.abort()to cancel. Keep each upload'sAbortControllerorxhrin 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
Sortable lists with pointer events. Pointer events (
pointerdown,pointermove,pointerup) cover mouse, pen and touch with one API. Onpointerdownremember the start position; onpointermoveturn the distance moved into a target index (Math.round(dy / rowHeight)) and reorder; onpointerupfinish. Listen to move/up onwindow(or usesetPointerCapture) so the drag continues outside the item, and settouch-action: noneon the handle so touch drags don't scroll the page. The HTML5draggableAPI is awkward for lists (no touch support, limited styling), which is why libraries use pointer events. - 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-liveregion ("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
How do you show an instant preview of a chosen image, and what must you clean up?
How do you build an accessible drag-and-drop upload area?
How would you implement upload progress and cancel in React?
How do you make a sortable list accessible?
Practice
Add a Remove button to each preview in the drop zone example, and make sure its object URL is freed.
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.