Uploads that show their work.

Animated, accessible file uploads for React. 17 motion presets on a real upload engine: byte-level progress, retries, pause, cancel and validation, with zero dependencies.

npm install upload-motion
  • 0 runtime dependencies
  • 17 motion presets
  • ≈ 9 kB headless hook
  • TypeScript · ESM + CJS · SSR

Expand it to manage each file, or pause the whole batch.

Drop files to uploador · Any file type · up to 25 MB
Simulated uploads · nothing leaves your browser

Playground

Tune it live, copy the code

Every control maps to a real prop on the real component. Drop your own files or generate samples in the browser, then copy a snippet that matches what you see.

Drag & drop files hereAny file type · up to 25 MB · max 10 files
Callbacks appear here as they fire: onUploadStart, onUploadSuccess, onUploadError…
import { FileUpload, createXhrUploader } from 'upload-motion';
import 'upload-motion/styles.css';

// Your endpoint: receives multipart form data in the "file" field.
const upload = createXhrUploader({ url: '/api/upload' });

export function Uploader() {
  return (
    <FileUpload
      preset="essential"
      upload={upload}
      maxFiles={10}
      maxSize={25 * 1024 * 1024}
      onUploadSuccess={(file, response) => console.log('uploaded', file.name, response)}
    />
  );
}

Presets

17 presets, one engine

From a quiet admin list to a basketball court. Presets only change presentation, so switching is one prop and your upload logic never changes.

Hoop Shot hoop-shot

A playful, physics-driven uploader: pull a file back, watch the dotted trajectory, release to shoot it into the hoop. Real gravity, rim bounces and bank shots – plus keyboard and "Shoot all" for instant perfect shots.

  • Consumer apps
  • Onboarding
  • Portfolios
  • Delight moments
  • Manual start

Upload files

Drag and drop, or take the shot.

Uploaded 0
Drop files hereor take the shot · Any file type
Simulated uploads

Engine

The motion is optional. The engine isn’t.

This lab uses only the headless useUploader hook and plain markup. Raise the failure rate, limit concurrency and watch the queue: waiting slots, backoff retries, pause and resume.

Concurrency
Failure rate
Auto retries
Uploading
0
Queued
0
Done
0
Failed
0
Speed
–

Load a batch of seven files and watch the queue work.

Or drop your own files here.

Queue.tsx
import { useUploader, createXhrUploader, formatBytes } from 'upload-motion';

export function Queue() {
  const { items, totals, dropzone, pause, resume, cancel, retry } = useUploader({
    upload: createXhrUploader({ url: '/api/upload' }),
    concurrency: 2,
    retry: { attempts: 2 },          // 800 ms, then 1.6 s backoff
  });

  return (
    <div {...dropzone.getRootProps({ focusable: true })}>
      <input {...dropzone.getInputProps()} />
      <progress value={totals.progress} />
      {items.map((item) => (
        <Row key={item.id} item={item} onRetry={() => retry(item.id)} />
      ))}
    </div>
  );
}
  • Honest by design. No handler, no “uploaded”. Simulated transports are flagged and badged.
  • Progress never runs backwards within an attempt, and 100% waits for the server before it says done.
  • Retries with backoff only for retryable failures: network, timeouts, 5xx, 408 and 429.
  • Bring any backend. Multipart POST, presigned S3/R2/GCS PUTs, or your own handler.

Quick start

Three ways in

Import the stylesheet once. Works with Vite, Next.js (App and Pages Router), Remix and CRA, on React 18.2 or newer.

  • Server components ready. The React entry ships with "use client", and upload-motion/core is server-safe.
  • Any backend. Files arrive as multipart form data in the file field, or as a raw presigned PUT.
Attachments.tsx
import { FileUpload, createXhrUploader } from 'upload-motion';
import 'upload-motion/styles.css'; // once, anywhere in your app

const upload = createXhrUploader({ url: '/api/upload' });

export function Attachments() {
  return (
    <FileUpload
      preset="wave-bar"
      upload={upload}
      accept="image/*,.pdf"
      maxSize={25 * 1024 * 1024}
      onUploadSuccess={(file, response) => console.log(file.name, response)}
    />
  );
}

Accessibility

Keyboards, screen readers and calm

Motion is the garnish, never the information. Every preset works without a mouse, announces what happens, and settles down when the OS asks for less motion.

Keyboard support
KeyAction
TabEvery action is a labelled button: browse, upload, pause, cancel, retry, remove
Enter / SpaceOpens the file dialog on focusable drop zones
⌘V / Ctrl+VPastes files into a focused drop zone
Enter on a tokenHoop Shot takes the perfect shot, no aiming needed
↑ ↓ Home End EscNavigate and close the Wave Bar actions menu
  • Live announcements. Additions, completions and cancellations are polite; errors and rejected files are assertive.
  • Real progressbars with readable values such as “42%, 3 seconds left”.
  • Not colour alone. Failures always carry an icon and a sentence, like “report.pdf is 31 MB; the limit is 25 MB.”
  • Reduced motion collapses animations to instant state changes and removes particles; nothing is hidden.
  • Tidy resources. Preview object URLs are revoked, requests abort on unmount, and progress renders once per frame.

API

Props at a glance

Every option is typed. The README has the full reference, the composable parts and the headless API.

PropTypeDefaultDescription
preset"wave-bar" | "card-fan" | "hoop-shot" | … (17)"essential"Animation preset, or your own preset object.
uploadUploadHandler–Transport. Without one, files stay "selected" and are never reported as uploaded.
acceptstring | string[]–Same syntax as the HTML accept attribute, e.g. "image/*,.pdf".
maxSize / minSizenumber–Bytes per file. Messages read "report.pdf is 31 MB; the limit is 25 MB."
maxFiles / multiplenumber / boolean– / trueQueue limit; multiple={false} replaces the current file.
validate(file, { items }) => string | null–Custom rule; return a message to reject.
autoUploadbooleantrueStart on add. Card Fan and Hoop Shot default to manual start.
concurrencynumber3Parallel uploads.
retrynumber | { attempts, delay }0Automatic retries for retryable errors with exponential backoff.
storeUploadStore–Bring your own store so uploads survive unmounts.
appearance{ theme, accent, radius, density, … }–Theme tokens, all mapped to --um-* CSS variables.
motion{ speed, intensity, particles, reducedMotion }–Global motion controls. Reduced motion follows the OS by default.
labelsPartial<UploadLabels>–Override any string for i18n.
renderFile / zoneContent(item, helpers) => ReactNode / ReactNode–Replace the file card or the drop-zone content.
onUploadSuccess …callbacks–onFilesAdded, onFilesRejected, onUploadStart, onUploadProgress, onUploadError, onUploadCancel, onQueueComplete…

Also exported: Uploader, UploadDropzone, UploadFileList, UploadProgress, useUploader, createUploadStore, createXhrUploader and the per-file visualizers.