Skip to main content

File Upload

Drag and drop or browse for files, with validation, previews and a hook for custom UIs.

Drop a file here or click to browse

Any file up to 5 MB

1'use client';
2
3import { CloudUploadIcon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { FileUpload } from '@/components/ui/file-upload';
6
7export function Default() {
8 return (
9 <FileUpload className="max-w-md" maxSize={5 * 1024 * 1024}>
10 <FileUpload.Dropzone>
11 <HugeiconsIcon icon={CloudUploadIcon} strokeWidth={1.5} />
12 <p className="text-foreground font-medium">Drop a file here or click to browse</p>
13 <p className="text-xs">Any file up to 5 MB</p>
14 </FileUpload.Dropzone>
15 <FileUpload.List />
16 <FileUpload.Errors />
17 </FileUpload>
18 );
19}

Installation

pnpm dlx nachui add file-upload

Anatomy

1import { FileUpload } from '@/components/ui/file-upload';
1<FileUpload multiple maxFiles={3} maxSize={2 * 1024 * 1024} accept="image/*,.pdf">
2 <FileUpload.Dropzone>
3 <UploadIcon />
4 <p>Drop files here or click to browse</p>
5 </FileUpload.Dropzone>
6 <FileUpload.List />
7 <FileUpload.Errors />
8 <FileUpload.Clear>Remove all</FileUpload.Clear>
9</FileUpload>
The root owns the hidden file input and the state: the accepted files, the validation errors and whether something is being dragged over. The dropzone opens the picker on click, Enter or Space and accepts drops. List renders one Item per file with a preview, the name, the size and a remove button, and takes a render function when you want your own row. Image files get an object URL preview that is revoked when the file leaves the list.

Composition

Use the following composition to build a FileUpload:
FileUpload
├── FileUpload.Dropzone
├── FileUpload.Trigger
├── FileUpload.List
│ └── FileUpload.Item
│ ├── FileUpload.ItemPreview
│ ├── FileUpload.ItemInfo
│ └── FileUpload.ItemRemove
├── FileUpload.Errors
└── FileUpload.Clear

Variants

Multiple with limits

multiple keeps adding to the list. maxFiles, maxSize and accept reject what does not fit and the reasons show up in Errors. A batch that would go over maxFiles is refused as a whole.

Drop up to 3 files

PDF, Word or images, 2 MB each

1'use client';
2
3import { CloudUploadIcon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { FileUpload } from '@/components/ui/file-upload';
6
7export function Multiple() {
8 return (
9 <FileUpload
10 className="max-w-md"
11 multiple
12 maxFiles={3}
13 maxSize={2 * 1024 * 1024}
14 accept=".pdf,.docx,image/*"
15 >
16 <FileUpload.Dropzone>
17 <HugeiconsIcon icon={CloudUploadIcon} strokeWidth={1.5} />
18 <p className="text-foreground font-medium">Drop up to 3 files</p>
19 <p className="text-xs">PDF, Word or images, 2 MB each</p>
20 </FileUpload.Dropzone>
21 <FileUpload.List />
22 <FileUpload.Errors />
23 <FileUpload.Clear>Remove all</FileUpload.Clear>
24 </FileUpload>
25 );
26}

Avatar

A single image with the preview inside the dropzone itself. useFileUploadContext reads the state from any child, so the preview is a few lines.

PNG or JPG up to 1 MB

1'use client';
2
3import { Camera01Icon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { FileUpload, useFileUploadContext } from '@/components/ui/file-upload';
6
7function AvatarDropzone() {
8 const { files } = useFileUploadContext();
9 const preview = files[0]?.preview;
10
11 return (
12 <FileUpload.Dropzone
13 aria-label="Upload avatar"
14 className="size-24 min-h-0 overflow-hidden rounded-full p-0"
15 >
16 {preview ? (
17 <img src={preview} alt="" className="size-full object-cover" />
18 ) : (
19 <HugeiconsIcon icon={Camera01Icon} strokeWidth={1.5} />
20 )}
21 </FileUpload.Dropzone>
22 );
23}
24
25export function Avatar() {
26 return (
27 <FileUpload accept="image/*" maxSize={1024 * 1024} className="w-auto items-center gap-2">
28 <AvatarDropzone />
29 <p className="text-muted-foreground text-xs">PNG or JPG up to 1 MB</p>
30 <FileUpload.Clear>Remove photo</FileUpload.Clear>
31 <FileUpload.Errors className="text-center" />
32 </FileUpload>
33 );
34}

Compact

Trigger is a button that opens the picker, for forms where a dropzone takes too much room. The list is laid out as chips through className.
Images or PDF
1'use client';
2
3import { Attachment01Icon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { FileUpload } from '@/components/ui/file-upload';
6
7export function Compact() {
8 return (
9 <FileUpload className="max-w-md" multiple accept="image/*,.pdf">
10 <div className="flex items-center gap-3">
11 <FileUpload.Trigger>
12 <HugeiconsIcon icon={Attachment01Icon} />
13 Attach files
14 </FileUpload.Trigger>
15 <span className="text-muted-foreground text-xs">Images or PDF</span>
16 </div>
17 <FileUpload.List className="flex-row flex-wrap">
18 {(file) => (
19 <FileUpload.Item file={file} className="max-w-56 py-2 ps-2 pe-2">
20 <FileUpload.ItemPreview className="size-8 rounded-sm [&_svg]:size-4" />
21 <FileUpload.ItemInfo className="gap-0 [&>span:last-child]:hidden" />
22 <FileUpload.ItemRemove className="size-6" />
23 </FileUpload.Item>
24 )}
25 </FileUpload.List>
26 <FileUpload.Errors />
27 </FileUpload>
28 );
29}

Progress

Pass a render function to List and compose the item parts around your own upload logic. This demo simulates the upload with a timer and a Progress bar.

Drop files to start uploading

Progress is simulated in this demo

1'use client';
2
3import { CloudUploadIcon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { useEffect, useRef, useState } from 'react';
6import { FileUpload, type FileWithPreview } from '@/components/ui/file-upload';
7import { Progress } from '@/components/ui/progress';
8
9function UploadRow({ file }: { file: FileWithPreview }) {
10 const [value, setValue] = useState(0);
11 const timer = useRef<ReturnType<typeof setInterval> | null>(null);
12
13 useEffect(() => {
14 timer.current = setInterval(() => {
15 setValue((current) => {
16 if (current >= 100) {
17 if (timer.current) clearInterval(timer.current);
18 return 100;
19 }
20 return Math.min(100, current + 8);
21 });
22 }, 180);
23 return () => {
24 if (timer.current) clearInterval(timer.current);
25 };
26 }, []);
27
28 return (
29 <FileUpload.Item file={file}>
30 <FileUpload.ItemPreview />
31 <FileUpload.ItemInfo>
32 <div className="mt-1 flex items-center gap-2">
33 <Progress value={value} className="h-1 rounded-full" />
34 <span className="text-muted-foreground w-9 text-end text-xs tabular-nums">{value}%</span>
35 </div>
36 </FileUpload.ItemInfo>
37 <FileUpload.ItemRemove />
38 </FileUpload.Item>
39 );
40}
41
42export function UploadProgress() {
43 return (
44 <FileUpload className="max-w-md" multiple maxFiles={4}>
45 <FileUpload.Dropzone>
46 <HugeiconsIcon icon={CloudUploadIcon} strokeWidth={1.5} />
47 <p className="text-foreground font-medium">Drop files to start uploading</p>
48 <p className="text-xs">Progress is simulated in this demo</p>
49 </FileUpload.Dropzone>
50 <FileUpload.List>{(file) => <UploadRow file={file} />}</FileUpload.List>
51 <FileUpload.Errors />
52 </FileUpload>
53 );
54}

Image grid

The same parts arranged as a grid of thumbnails, with the remove button revealed on hover.

Drop images here

Up to 6 images

1'use client';
2
3import { ImageAdd01Icon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { FileUpload } from '@/components/ui/file-upload';
6
7export function Images() {
8 return (
9 <FileUpload className="max-w-md" multiple accept="image/*" maxFiles={6}>
10 <FileUpload.Dropzone className="min-h-32">
11 <HugeiconsIcon icon={ImageAdd01Icon} strokeWidth={1.5} />
12 <p className="text-foreground font-medium">Drop images here</p>
13 <p className="text-xs">Up to 6 images</p>
14 </FileUpload.Dropzone>
15 <FileUpload.List className="grid grid-cols-3 gap-2">
16 {(file) => (
17 <FileUpload.Item file={file} className="group relative aspect-square p-0">
18 <FileUpload.ItemPreview className="size-full rounded-lg [&_svg]:size-8" />
19 <FileUpload.ItemRemove className="bg-background/90 absolute top-1.5 right-1.5 size-6 opacity-0 shadow-sm group-focus-within:opacity-100 group-hover:opacity-100" />
20 </FileUpload.Item>
21 )}
22 </FileUpload.List>
23 <FileUpload.Errors />
24 </FileUpload>
25 );
26}

Hook

useFileUpload is the engine behind the component and is exported on its own for interfaces that do not fit the parts above. It takes the same options as the root and returns the state plus the handlers to wire into any element.
1import { useFileUpload, formatBytes } from '@/components/ui/file-upload';
2
3const { files, errors, isDragging, openFileDialog, getInputProps, getDropzoneProps, removeFile } =
4 useFileUpload({ multiple: true, maxSize: 5 * 1024 * 1024 });
5
6<input {...getInputProps()} className="sr-only" />
7<div {...getDropzoneProps()} data-dragging={isDragging || undefined}>...</div>
ReturnTypeDescription
filesFileWithPreview[]Accepted files with an id and an optional preview
errorsstring[]Messages from the last validation
isDraggingbooleanA drag is over the dropzone
addFiles(files: FileList | File[]) => voidValidate and add files programmatically
removeFile(id: string) => voidRemove one file
clearFiles() => voidRemove every file
clearErrors() => voidReset the error list
openFileDialog() => voidOpen the native picker
getInputProps() => InputPropsProps for the hidden file input
getDropzoneProps() => DragHandlersDrag enter, leave, over and drop handlers
formatBytes(bytes, decimals?) turns a byte count into 1.5 MB.

API Reference

FileUpload

PropTypeDefaultDescription
acceptstring-Accepted types, as the native accept attribute
multiplebooleanfalseKeep adding files instead of replacing
maxFilesnumberInfinityMaximum number of files when multiple
maxSizenumberInfinityMaximum size per file, in bytes
initialFilesFileMetadata[][]Files already uploaded, with id, name, size, type and url
disabledbooleanfalseDisable the dropzone, trigger and actions
namestring-Name of the hidden input for form submissions
onFilesChange(files: FileWithPreview[]) => void-Called with the full list after every change
onFilesAdded(files: FileWithPreview[]) => void-Called with the files accepted in a batch
onError(errors: string[]) => void-Called when a batch produces validation errors
classNamestring-Additional CSS classes

FileUpload.List

PropTypeDefaultDescription
childrenReactNode | (file: FileWithPreview) => ReactNode-Custom rows. With no children each file renders an Item
classNamestring-Additional CSS classes

FileUpload.Item

PropTypeDefaultDescription
fileFileWithPreview-The file to render
childrenReactNode-Custom parts. Without children renders preview, info and remove
classNamestring-Additional CSS classes

FileUpload.Dropzone, FileUpload.Trigger, FileUpload.ItemPreview, FileUpload.ItemInfo, FileUpload.ItemRemove, FileUpload.Errors, FileUpload.Clear

Parts wired to the shared state. Each accepts className and the standard attributes of the element it renders. ItemRemove ships with an English aria-label you can override.
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue
Ctrl+I