Back to Articles
React
File Upload
Accessibility
Libraries

Why I Built Uplofile

May 26, 20265 min read

Most upload components try to own too much. Uplofile exists because upload UI should be composable while your app stays in control of the upload pipeline.


File uploads look like a UI problem at first.

You need a button. Maybe a dropzone. Maybe a preview list. Maybe progress, retry, cancel, and remove actions.

Then reality shows up.

Uploads quickly become about infrastructure:

  • Where should the file go?
  • Do you use presigned URLs?
  • Is the upload multipart?
  • Do you need resumable uploads?
  • What happens when the user cancels?
  • How do you delete a file after it was already uploaded?
  • Who owns retry behavior?

That is why I built Uplofile.

Not because the React ecosystem lacked upload libraries. It had plenty.

I built it because the tools I tried were not composable enough for the way I wanted to build.

What Felt Wrong

Most upload libraries start simple.

Then you try to fit them into a real product and the wiring starts growing.

You bring your own design system, but the component already has opinions about markup. You have your own backend flow, but the library wants to control how uploading works. You need a custom preview layout, but the API makes that feel like a workaround. You want accessibility, but you also want full control over the interface.

The problem is not that those libraries are bad.

The problem is that file upload is too application-specific for one component to own everything.

A component should not decide your backend. It should not decide your storage provider. It should not decide whether you use fetch, S3, multipart uploads, resumable uploads, or a custom API.

Those are infrastructure decisions.

They belong in your app.

The Design Principle

Uplofile is built around one idea:

Your upload component should not own your upload pipeline.

Uplofile handles the React part:

  • upload UI state
  • drag-and-drop behavior
  • file selection
  • previews
  • progress state
  • cancel, retry, and remove actions
  • accessible interaction patterns

Your application handles the infrastructure part:

  • transport
  • backend routes
  • storage provider
  • auth rules
  • retry strategy
  • resumable upload protocol

That separation is the whole point.

Uplofile gives you the upload experience. Your app defines what "uploading" means.

The Boundary Is Just a Function

The most important API is the upload function.

You receive the file, an abort signal, and a progress setter. Then you wire that to whatever your app uses.

async function upload(
  file: File,
  signal: AbortSignal,
  setProgress: (progress: number) => void,
) {
  const body = new FormData();
  body.append("file", file);

  const response = await fetch("/api/uploads", {
    method: "POST",
    body,
    signal,
  });

  if (!response.ok) {
    throw new Error("Upload failed");
  }

  const result = (await response.json()) as { url: string; id?: string };
  setProgress(100);

  return result;
}

That is the handoff.

Uplofile does not care if /api/uploads stores the file in S3, R2, Supabase, your own server, or somewhere else.

It only cares that your function resolves when the upload is done.

Then Compose the UI

Once the upload function exists, the interface stays small.

"use client";

import {
  UplofileDropzone,
  UplofilePreview,
  UplofileRoot,
  UplofileTrigger,
} from "uplofile";

export function AvatarUploader() {
  return (
    <UplofileRoot upload={upload} maxCount={1} accept="image/*">
      <UplofileDropzone className="rounded-md border border-dashed p-6">
        <p>Drop an image here or</p>

        <UplofileTrigger className="underline">
          select one
        </UplofileTrigger>

        <UplofilePreview />
      </UplofileDropzone>
    </UplofileRoot>
  );
}

The UI is yours.

Use Tailwind, CSS modules, shadcn/ui, plain CSS, or your own component system. Move the trigger somewhere else. Render your own preview list. Use a dropzone, or do not.

The primitives are there so the upload interface can fit your product instead of forcing your product to fit the library.

Why This Matters

File upload UI is rarely just one button.

In real apps, you eventually need product-specific behavior:

  • avatar upload
  • document upload
  • media gallery upload
  • drag-and-drop upload
  • form-based upload
  • strict server-side delete
  • validation before upload
  • custom progress display

These flows often look similar, but they are not the same.

That is where composability matters.

I did not want a library where every new product requirement meant fighting the abstraction. I wanted primitives that made the common parts easy and left the important decisions open.

The Mental Model

With Uplofile, you do not start by thinking:

How do I bend this upload component into my system?

You start with:

What should this upload experience look like?

Then you create it. Then you wire your upload.

That is the reason Uplofile exists.

It is not trying to be your backend. It is not trying to be your storage strategy. It is not trying to own your design system.

It is a set of composable React upload primitives that stay out of the way.

And for upload UI, that is exactly what I wanted.

If Uplofile fits the way you think about upload UI, try it on npm. And if you want more composable React primitives like this to exist, star the project on GitHub. It helps more developers find it.

Enjoyed this article?

Quick reaction helps me write better follow-up posts.

Connect with me