Saltar al contenido principal

File Upload

Arrastra y suelta o busca archivos, con validación, previews y un hook para interfaces propias.

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}

Instalación

pnpm dlx nachui add file-upload

Anatomía

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>Suelta archivos aquí o haz clic para buscar</p>
5 </FileUpload.Dropzone>
6 <FileUpload.List />
7 <FileUpload.Errors />
8 <FileUpload.Clear>Quitar todos</FileUpload.Clear>
9</FileUpload>
La raíz es dueña del input de archivos oculto y del estado: los archivos aceptados, los errores de validación y si hay algo arrastrándose encima. El dropzone abre el selector con clic, Enter o Espacio y acepta drops. List renderiza un Item por archivo con preview, nombre, tamaño y un botón para quitarlo, y acepta una función de render cuando quieres tu propia fila. Las imágenes reciben un preview con object URL que se libera cuando el archivo sale de la lista.

Composición

Usa la siguiente composición para construir un FileUpload:
FileUpload
├── FileUpload.Dropzone
├── FileUpload.Trigger
├── FileUpload.List
│ └── FileUpload.Item
│ ├── FileUpload.ItemPreview
│ ├── FileUpload.ItemInfo
│ └── FileUpload.ItemRemove
├── FileUpload.Errors
└── FileUpload.Clear

Variantes

Múltiple con límites

multiple sigue agregando a la lista. maxFiles, maxSize y accept rechazan lo que no encaja y los motivos aparecen en Errors. Un lote que superaría maxFiles se rechaza completo.

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

Una sola imagen con el preview dentro del propio dropzone. useFileUploadContext lee el estado desde cualquier hijo, así que el preview son unas pocas líneas.

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}

Compacto

Trigger es un botón que abre el selector, para formularios donde un dropzone ocupa demasiado. La lista se acomoda como chips mediante 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}

Progreso

Pasa una función de render a List y compón las partes del item alrededor de tu propia lógica de subida. Esta demo simula la subida con un timer y una barra Progress.

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}

Grilla de imágenes

Las mismas partes acomodadas como una grilla de miniaturas, con el botón de quitar visible al pasar el mouse.

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 es el motor detrás del componente y se exporta por separado para interfaces que no encajan con las partes de arriba. Recibe las mismas opciones que la raíz y devuelve el estado más los handlers para conectar a cualquier elemento.
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[]Archivos aceptados con un id y un preview opcional
errorsstring[]Mensajes de la última validación
isDraggingbooleanHay un arrastre sobre el dropzone
addFiles(files: FileList | File[]) => voidValida y agrega archivos por código
removeFile(id: string) => voidQuita un archivo
clearFiles() => voidQuita todos los archivos
clearErrors() => voidLimpia la lista de errores
openFileDialog() => voidAbre el selector nativo
getInputProps() => InputPropsProps para el input de archivos oculto
getDropzoneProps() => DragHandlersHandlers de drag enter, leave, over y drop
formatBytes(bytes, decimals?) convierte una cantidad de bytes en 1.5 MB.

Referencia de API

FileUpload

PropTypeDefaultDescription
acceptstring-Tipos aceptados, como el atributo nativo accept
multiplebooleanfalseSigue agregando archivos en vez de reemplazar
maxFilesnumberInfinityCantidad máxima de archivos cuando es multiple
maxSizenumberInfinityTamaño máximo por archivo, en bytes
initialFilesFileMetadata[][]Archivos ya subidos, con id, name, size, type y url
disabledbooleanfalseDeshabilita el dropzone, el trigger y las acciones
namestring-Nombre del input oculto para envíos de formulario
onFilesChange(files: FileWithPreview[]) => void-Se llama con la lista completa después de cada cambio
onFilesAdded(files: FileWithPreview[]) => void-Se llama con los archivos aceptados en un lote
onError(errors: string[]) => void-Se llama cuando un lote produce errores de validación
classNamestring-Clases CSS adicionales

FileUpload.List

PropTypeDefaultDescription
childrenReactNode | (file: FileWithPreview) => ReactNode-Filas personalizadas. Sin children cada archivo renderiza un Item
classNamestring-Clases CSS adicionales

FileUpload.Item

PropTypeDefaultDescription
fileFileWithPreview-El archivo a renderizar
childrenReactNode-Partes personalizadas. Sin children renderiza preview, info y quitar
classNamestring-Clases CSS adicionales

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

Partes conectadas al estado compartido. Cada una acepta className y los atributos estándar del elemento que renderiza. ItemRemove trae un aria-label en inglés que puedes sobrescribir.
¿Encontraste algo que mejorar?

¿Notaste un error, tipografía o detalle faltante en esta página? Ayúdanos a mejorar la documentación abriendo un issue en GitHub.

Crear un Issue
Ctrl+I