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.
| Prop | Type | Default |
|---|---|---|
files | File[] | — |
defaultFiles | File[] | — |
onFilesChange | function | — |
accept | object | — |
maxFiles | number | 0 |
maxSize | number | Infinity |
minSize | number | 0 |
multiple | boolean | true |
disabled | boolean | false |
noClick | boolean | false |
noDrag | boolean | false |
noKeyboard | boolean | false |
validator | function | — |
onDrop | function | — |
onDropAccepted | function | — |
onDropRejected | function | — |
onError | function | — |
className | string | — |
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.
| Prop | Type | Default |
|---|---|---|
variant | enum | "default" |
className | string | — |
| Data attribute | Values |
|---|---|
[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.
| Prop | Type | Default |
|---|---|---|
className | string | — |
FileUploader.List
A list-style preview of the selected files. Without renderItem, each file renders as Item + ItemImage + ItemInfo + ItemRemove.
| Prop | Type | Default |
|---|---|---|
renderItem | function | — |
className | string | — |
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.
| Prop | Type | Default |
|---|---|---|
dropbox | boolean | false |
renderItem | function | — |
className | string | — |
FileUploader.Item
One selected file. Provides the file to the item parts below it.
| Prop | Type | Default |
|---|---|---|
file* | File | — |
className | string | — |
FileUploader.ItemImage
An image preview for image files (via an object URL, revoked automatically), or a file icon for other types.
| Prop | Type | Default |
|---|---|---|
className | string | — |
FileUploader.ItemInfo
The file's name and formatted size.
| Prop | Type | Default |
|---|---|---|
className | string | — |
FileUploader.ItemRemove
A button that removes the item's file from the selection.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
className | string | — |
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);
}