File Uploader

A drag-and-drop file uploader with list, thumbnail, avatar, and dropbox previews, built on react-dropzone.

Default

Files can be uploaded by clicking the dropzone or by dragging and dropping files into it. Selected files are shown in a list with their name, size, and a remove button.

tsx

import { FileUploader } from "@hummingbirdui/react";
import { Upload } from "lucide-react";

export default function FileUploaderDefault() {
  return (
    <FileUploader className="mx-auto w-full max-w-md">
      <FileUploader.Dropzone>
        <FileUploader.Message>
          <div className="flex flex-col sm:flex-row items-center justify-center gap-2">
            <Upload className="size-4" />
            <p className="text-xs mb-0">
              <span>Drop files here</span>
              <span className="mx-1 text-subtle">or</span>
              <span className="text-primary">browse from device</span>
            </p>
          </div>
        </FileUploader.Message>
      </FileUploader.Dropzone>
      <FileUploader.List />
    </FileUploader>
  );
}

Thumbnail preview

FileUploader.Thumbnails displays the selected files as square thumbnails.

tsx

import { FileUploader } from "@hummingbirdui/react";
import { Upload } from "lucide-react";

export default function FileUploaderThumbnail() {
  return (
    <FileUploader className="mx-auto w-full max-w-md">
      <FileUploader.Dropzone>
        <FileUploader.Message>
          <div className="flex flex-col sm:flex-row items-center justify-center gap-2">
            <Upload className="size-4" />
            <p className="text-xs mb-0">
              <span>Drop files here</span>
              <span className="mx-1 text-subtle">or</span>
              <span className="text-primary">browse from device</span>
            </p>
          </div>
        </FileUploader.Message>
      </FileUploader.Dropzone>
      <FileUploader.Thumbnails />
    </FileUploader>
  );
}

Avatar

variant="avatar" turns the dropzone into a circular uploader that fills with the selected image.

tsx

import { FileUploader } from "@hummingbirdui/react";
import { ImagePlus } from "lucide-react";

export default function FileUploaderAvatar() {
  return (
    <FileUploader
      accept={{ "image/*": [] }}
      maxFiles={1}
      className="flex justify-center"
    >
      <FileUploader.Dropzone variant="avatar">
        <FileUploader.Message>
          <div className="flex flex-col items-center justify-center gap-2">
            <ImagePlus className="size-4" />
            <p className="text-xs mb-0">Upload Avatar</p>
          </div>
        </FileUploader.Message>
      </FileUploader.Dropzone>
    </FileUploader>
  );
}

FileDropBox

The dropbox layout places a box-style dropzone (variant="box") inline with the thumbnails.

tsx

import { FileUploader } from "@hummingbirdui/react";
import { Upload } from "lucide-react";

export default function FileUploaderDropbox() {
  return (
    <FileUploader
      accept={{ "image/*": [] }}
      className="mx-auto w-full max-w-md"
    >
      <FileUploader.Thumbnails dropbox>
        <FileUploader.Dropzone variant="box">
          <FileUploader.Message>
            <Upload className="size-4" />
          </FileUploader.Message>
        </FileUploader.Dropzone>
      </FileUploader.Thumbnails>
    </FileUploader>
  );
}

Single file

maxFiles={1} makes the uploader single-file: a new selection replaces the previous one.

tsx

import { FileUploader } from "@hummingbirdui/react";
import { Upload } from "lucide-react";

export default function FileUploaderSingle() {
  return (
    <FileUploader maxFiles={1} className="mx-auto w-full max-w-md">
      <FileUploader.Dropzone>
        <FileUploader.Message>
          <div className="flex flex-col sm:flex-row items-center justify-center gap-2">
            <Upload className="size-4" />
            <p className="text-xs mb-0">
              <span>Drop a file here</span>
              <span className="mx-1 text-subtle">or</span>
              <span className="text-primary">browse from device</span>
            </p>
          </div>
        </FileUploader.Message>
      </FileUploader.Dropzone>
      <FileUploader.List />
    </FileUploader>
  );
}

Validation

File upload can be validated using the accept, maxSize, minSize, and validator props.

tsx

import * as React from "react";
import { FileUploader } from "@hummingbirdui/react";
import { ImageUp } from "lucide-react";

const MAX_SIZE = 1024 * 1024; // 1 MB

export default function FileUploaderValidation() {
  const [error, setError] = React.useState<string | null>(null);

  return (
    <FileUploader
      accept={{ "image/*": [] }}
      maxSize={MAX_SIZE}
      onDropAccepted={() => setError(null)}
      onDropRejected={(rejections) => {
        setError(
          rejections[0]?.errors[0]?.code === "file-too-large"
            ? "Images must be smaller than 1 MB."
            : "Only image files are accepted.",
        );
      }}
      className="mx-auto w-full max-w-md"
    >
      <FileUploader.Dropzone>
        <FileUploader.Message>
          <div className="flex flex-col items-center justify-center gap-2">
            <ImageUp className="size-4" />
            <p className="text-xs mb-0">Drop images here (max 1 MB each)</p>
          </div>
        </FileUploader.Message>
      </FileUploader.Dropzone>
      {error ? <p className="mt-2 text-xs text-danger">{error}</p> : null}
      <FileUploader.Thumbnails />
    </FileUploader>
  );
}

API Reference

Built on react-dropzone. Every option from react-dropzone is passed through to the FileUploader component.

FileUploader

The provider that holds the selected files and the dropzone state. Renders a plain div.

PropTypeDefault
filesFile[]
defaultFilesFile[]
onFilesChangefunction
acceptobject
maxFilesnumber0
maxSizenumberInfinity
minSizenumber0
multiplebooleantrue
disabledbooleanfalse
noClickbooleanfalse
noDragbooleanfalse
noKeyboardbooleanfalse
validatorfunction
onDropfunction
onDropAcceptedfunction
onDropRejectedfunction
onErrorfunction
classNamestring

FileUploader.Dropzone

The visible drop target. Renders the hidden file input, wires the drag-and-drop and click-to-browse behavior, and in the avatar variant fills with the selected image.

PropTypeDefault
variantenum"default"
classNamestring

Data attributeValues
[data-drag-active]Present while dragging over
[data-drag-reject]Present while dragging files that will be rejected
[data-disabled]Present when disabled
[data-filled]Present on the avatar variant once an image is selected

FileUploader.Message

Renders a div with Hummingbird's file-uploader-message class for the dropzone content.

PropTypeDefault
classNamestring

FileUploader.List

A list-style preview of the selected files. Without renderItem, each file renders as Item + ItemImage + ItemInfo + ItemRemove.

PropTypeDefault
renderItemfunction
classNamestring

FileUploader.Thumbnails

A thumbnail grid preview of the selected files. Children render after the thumbnails — the place for a box dropzone in the dropbox layout.

PropTypeDefault
dropboxbooleanfalse
renderItemfunction
classNamestring

FileUploader.Item

One selected file. Provides the file to the item parts below it.

PropTypeDefault
file*File
classNamestring

FileUploader.ItemImage

An image preview for image files (via an object URL, revoked automatically), or a file icon for other types.

PropTypeDefault
classNamestring

FileUploader.ItemInfo

The file's name and formatted size.

PropTypeDefault
classNamestring

FileUploader.ItemRemove

A button that removes the item's file from the selection.

PropTypeDefault
asChildbooleanfalse
classNamestring

Styling

The file uploader component utilizes several CSS variables for customization, defined on the dropzone and preview classes.

.file-uploader {
  --file-uploader-padding: --spacing(6);
  --file-uploader-bg: var(--background-color-subtle);
  --file-uploader-border-color: var(--border-color-default);
  --file-uploader-border-radius: var(--radius-xl);
  --file-uploader-hover-border-color: var(--color-primary);
  --file-uploader-hover-bg: --alpha(var(--color-primary) / 5%);
  --file-uploader-reject-border-color: var(--color-danger);
  --file-uploader-reject-bg: --alpha(var(--color-danger) / 5%);
  --file-uploader-avatar-overlay: --alpha(var(--color-inverse) / 70%);
  --file-uploader-avatar-size: --spacing(40);
}
.file-uploader-list {
  --file-uploader-list-padding: --spacing(2);
  --file-uploader-list-font-size: var(--text-sm);
  --file-uploader-list-bg: var(--background-color-subtle);
  --file-uploader-list-border-radius: var(--radius-lg);
  --file-uploader-list-image-size: --spacing(13);
  --file-uploader-list-image-border-width: var(--border-width);
  --file-uploader-list-image-border-color: var(--border-color-subtle);
  --file-uploader-list-image-border-radius: var(--radius-lg);
}
.file-uploader-thumbnails {
  --file-uploader-thumbnail-size: --spacing(14);
  --file-uploader-thumbnail-border-width: var(--border-width);
  --file-uploader-thumbnail-border-color: var(--border-color-subtle);
  --file-uploader-thumbnail-border-radius: var(--radius-lg);
  --file-uploader-thumbnail-remove-btn-size: --spacing(3);
  --file-uploader-thumbnail-remove-btn-bg: var(--color-neutral);
  --file-uploader-thumbnail-remove-btn-color: var(--color-white);
  --file-uploader-thumbnail-remove-btn-radius: calc(infinity * 1px);
}