Vercel Blob upload with real progress

Client uploads straight to Vercel Blob, so big files skip your server's body limit. Blob's progress callback drives each file's ring in the free dropzone.

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 { upload as blobUpload } from "@vercel/blob/client";

const upload: Uploader = async (file, onProgress, signal) => {
  await blobUpload(file.name, file, {
    access: "public",
    handleUploadUrl: "/api/blob/upload",
    multipart: file.size > 50 * 1024 * 1024, // split big files into parts
    abortSignal: signal,
    onUploadProgress: ({ percentage }) => onProgress(percentage),
  });
};

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

app/api/blob/upload/route.ts

import { handleUpload, type HandleUploadBody } from "@vercel/blob/client";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const body = (await request.json()) as HandleUploadBody;
  try {
    const json = await handleUpload({
      body,
      request,
      onBeforeGenerateToken: async () => {
        // check the session here; throw to refuse
        return {
          allowedContentTypes: ["image/*", "video/*", "application/pdf"],
          maximumSizeInBytes: 500 * 1024 * 1024,
          addRandomSuffix: true,
        };
      },
      onUploadCompleted: async ({ blob }) => {
        // save blob.url to your database (Vercel calls this, so it doesn't run on localhost)
        console.log("uploaded", blob.url);
      },
    });
    return NextResponse.json(json);
  } catch (error) {
    return NextResponse.json({ error: (error as Error).message }, { status: 400 });
  }
}

Good to know

  • The file goes from the browser to Blob. Your route only hands out a short-lived token, so the 4.5 MB request limit on Vercel functions doesn't apply to the file.
  • onUploadProgress gives a percentage, which goes straight to the dropzone's ring. Many files upload side by side, each with its own ring.
  • BLOB_READ_WRITE_TOKEN comes from the Blob store you connect in the Vercel dashboard. Pull it locally with vercel env pull.
  • onUploadCompleted is a webhook from Vercel to your route, so it can't reach localhost. Locally, save the URL from the value upload() returns, or use a tunnel.
  • A refused token (wrong type, too big, no session) rejects upload(), and the dropzone shows the message on that file with Retry.
  • Checked against the type definitions of @vercel/blob 2.8. We haven't uploaded to a live Blob store with it.

Install

  1. One commandnpm i qodesign-free framer-motion @vercel/blob
  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/*
onChange
the list of files and their state

How qodesign compares

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

Questions

Server upload or client upload?

Client upload, as above, for anything over a few MB. A server action with put() is simpler but goes through your function and its body limit.

Can I make the files private?

Yes. Set access: "private" on the client and serve them through a route that checks the session.

Is the dropzone free?

Yes. MIT licence, from npm 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.

Vercel Blob upload in Next.js with a progress bar (free dropzone)