An animated dropzone for UploadThing

Keep UploadThing's file router and swap its default dropzone for the free animated one. Each file gets a real progress ring from UploadThing's own progress events. MIT.

Get it free

Open the demoReact · Tailwind · TypeScript

Use it in Next.js

app/upload/page.tsx

"use client";
import UploadDropzone, { type Uploader } from "qodesign-free/upload-dropzone";
import { generateReactHelpers } from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";

const { uploadFiles } = generateReactHelpers<OurFileRouter>();

// One call per file, so every file gets its own ring and its own retry.
const upload: Uploader = async (file, onProgress, signal) => {
  await uploadFiles("fileUploader", {
    files: [file],
    signal,
    onUploadProgress: ({ progress }) => onProgress(progress),
  });
};

export default function UploadPage() {
  return <UploadDropzone upload={upload} accept="image/*,.pdf" maxSize={8 * 1024 * 1024} />;
}

app/api/uploadthing/core.ts

import { createUploadthing, type FileRouter } from "uploadthing/next";

const f = createUploadthing();

export const ourFileRouter = {
  fileUploader: f({ image: { maxFileSize: "8MB" }, pdf: { maxFileSize: "8MB" } })
    .middleware(async () => {
      // read your session here; throw to refuse the upload
      return { userId: "user_123" };
    })
    .onUploadComplete(async ({ metadata, file }) => {
      // save file.ufsUrl to your database
      return { url: file.ufsUrl, by: metadata.userId };
    }),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;

app/api/uploadthing/route.ts

import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";

export const { GET, POST } = createRouteHandler({ router: ourFileRouter });

Good to know

  • UPLOADTHING_TOKEN goes in .env. The route lives at /api/uploadthing, the default path the React helpers call.
  • The progress ring is real: UploadThing calls onUploadProgress as bytes go out, and the code hands the percentage straight to the dropzone. The ring holds just under 100 until the upload resolves.
  • Removing a file aborts it. The dropzone passes an AbortSignal, and uploadFiles takes the same signal.
  • If UploadThing refuses a file (too big, wrong type, middleware threw), uploadFiles rejects and the dropzone shows the error on that file's row with a Retry button.
  • Keep maxSize and accept in line with the file router. The dropzone checks in the browser, and the router is what actually enforces it.
  • Checked against the type definitions of uploadthing 7.7 and @uploadthing/react 7.3. We haven't run it against a live UploadThing app.

Install

  1. One commandnpm i qodesign-free framer-motion uploadthing @uploadthing/react
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

upload
(file, onProgress, signal) → Promise
maxSize
bytes, 50 MB by default
accept
for the file picker, e.g. image/*
onAllDone
every file finished

How qodesign compares

All comparisons, with prices read on each library's own site.

Questions

Why not UploadThing's own UploadDropzone?

Theirs works fine. Use this one if you want the drop animation, a ring per file and per-file retry. Both talk to the same file router.

Do I still need the UploadThing CSS or Tailwind plugin?

No. The free dropzone brings its own stylesheet, and the code above only uses UploadThing's upload function.

Is it free?

Yes. MIT licence, from the qodesign-free npm package or the shadcn registry.

Does the dropzone work in Next.js?

Yes, as a client component. Keep "use client" at the top of the file that renders it, as in the snippet above. App Router and Pages Router both work.

UploadThing dropzone for Next.js (free animated drag and drop UI)