Upload
freeUpload handles file selection, drop zones, validation, and removable file lists.
Preview
Dark
Drag and Drop file here or Choose file
Supported formats: XLS, XLSXMaximum size: 25MB
Table Name.xls
3 MB
import { useState } from 'react';
import CloseButton from '@/components/ui/CloseButton';
import Upload from '@/components/ui/Upload';
import Alert from '@/components/ui/Alert';
import { PiUploadSimple } from 'react-icons/pi'
const ACCEPTED_FILES =
'.xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const MAX_SIZE = 25 * 1000 * 1000;
const createDemoFile = (name: string, size: number, type: string) =>
({ name, size, type }) as File;
const INITIAL_FILES = [
createDemoFile('Table Name.xls', 3000000, 'application/vnd.ms-excel'),
];
const formatSize = (size: number) =>
size >= 1000 * 1000
? `${Math.round(size / (1000 * 1000))} MB`
: `${Math.round(size / 1000)} KB`;
export default function UsageDemo() {
const [files, setFiles] = useState<File[]>(INITIAL_FILES);
const [message, setMessage] = useState('');
const beforeUpload = (files: FileList | null) => {
const allowed = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
];
if (!files) {
return true;
}
for (const file of files) {
if (!allowed.includes(file.type)) {
return 'Upload an XLS or XLSX spreadsheet.';
}
if (file.size > MAX_SIZE) {
return 'Spreadsheet must be smaller than 25MB.';
}
}
return true;
};
return (
<div className="grid w-full max-w-xl gap-4">
<Upload
draggable
accept={ACCEPTED_FILES}
uploadLimit={1}
fileList={files}
beforeUpload={beforeUpload}
onBeforeUpload={(result) => setMessage(String(result))}
onChange={(_, fileList) => {
setFiles(fileList.slice(-1));
setMessage('');
}}
className="w-full"
onFileRemove={(_, fileList) => setFiles(fileList)}
tip={
<div className="mt-4 flex flex-wrap items-center justify-between gap-4 text-xs text-muted-foreground">
<span>Supported formats: XLS, XLSX</span>
<span>Maximum size: 25MB</span>
</div>
}
customFileItem={({ file, onRemove, FileIcon }) => (
<div className="rounded-card border p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-center gap-4">
<FileIcon file={file} />
<div className="grid min-w-0">
<p className="truncate text-sm font-semibold text-foreground">
{file.name}
</p>
<p className="text-xs text-muted-foreground">
{formatSize(file.size)}
</p>
</div>
</div>
<CloseButton
aria-label={`Remove ${file.name}`}
onClick={onRemove}
/>
</div>
</div>
)}
>
<div className="grid justify-items-center gap-4 px-8 py-12 text-center">
<span className="relative flex size-12 items-center justify-center">
<span className="h-10 w-10 border rounded-lg flex items-center justify-center">
<PiUploadSimple className="text-lg" />
</span>
</span>
<div className="grid gap-2">
<p className="font-medium">
Drag and Drop file here or{' '}
<span className="underline underline-offset-2">
Choose file
</span>
</p>
</div>
</div>
</Upload>
{message ? (
<Alert
showIcon
closable
variant="destructive"
onClose={() => setMessage('')}
>
{message}
</Alert>
) : null}
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add UploadExamples
Basic
Preview
Dark
import Upload from '@/components/ui/Upload';
export default function BasicDemo() {
return (
<div className="w-full max-w-xl flex justify-center min-w-0">
<Upload className="mx-auto" />
</div>
);
}Drag And Drop
Preview
Dark
Drop files here or browse
Multiple documents can be attached at once.
import Upload from '@/components/ui/Upload';
import { PiCloudArrowUp } from 'react-icons/pi'
export default function DragAndDropDemo() {
return (
<Upload draggable multiple className="w-full max-w-xl">
<div className="grid justify-items-center gap-4 py-8 px-8 text-center">
<span className="flex size-12 items-center justify-center rounded-control border bg-muted text-muted-foreground">
<PiCloudArrowUp className="size-6" />
</span>
<div className="grid gap-2">
<p className="font-medium text-foreground">
Drop files here or browse
</p>
<p className="text-sm text-muted-foreground">
Multiple documents can be attached at once.
</p>
</div>
</div>
</Upload>
);
}Custom Trigger
Preview
Dark
import Upload from '@/components/ui/Upload';
import Button from '@/components/ui/Button';
import { PiUploadSimple } from 'react-icons/pi'
export default function CustomTriggerDemo() {
return (
<Upload>
<Button variant="solid" icon={<PiUploadSimple />}>
Select contract
</Button>
</Upload>
);
}Validation
Preview
Dark
Supported formats: PNG, JPEG. Maximum size: 500 KB.
import { useState } from 'react';
import Upload from '@/components/ui/Upload';
import Alert from '@/components/ui/Alert';
const MAX_SIZE = 500 * 1000;
export default function ValidationDemo() {
const [message, setMessage] = useState('');
const beforeUpload = (files: FileList | null) => {
const allowed = ['image/png', 'image/jpeg'];
if (!files) {
return true;
}
for (const file of files) {
if (!allowed.includes(file.type)) {
return 'Only PNG and JPEG images are accepted.';
}
if (file.size > MAX_SIZE) {
return 'Image must be smaller than 500 KB.';
}
}
return true;
};
return (
<div>
<Upload
accept="image/png,image/jpeg"
uploadLimit={2}
beforeUpload={beforeUpload}
onBeforeUpload={(result) => setMessage(String(result))}
onChange={(file) => setMessage(`${file.name} passed validation.`)}
tip={
<span className="text-muted-foreground mt-2">
Supported formats: PNG, JPEG. Maximum size: 500 KB.
</span>
}
/>
{message && (
<Alert
showIcon
variant="destructive"
className="mt-2"
onClose={() => setMessage('')}
>
{message}
</Alert>
)}
</div>
);
}Avatar Image
Preview
Dark
Profile photo
import { useEffect, useState } from 'react';
import Avatar from '@/components/ui/Avatar';
import Button from '@/components/ui/Button';
import Upload from '@/components/ui/Upload';
import { PiImageSquare, PiUser } from 'react-icons/pi'
export default function AvatarImageDemo() {
const [preview, setPreview] = useState<string>();
useEffect(() => {
return () => {
if (preview) {
URL.revokeObjectURL(preview);
}
};
}, [preview]);
const beforeUpload = (files: FileList | null) => {
const allowed = ['image/png', 'image/jpeg', 'image/gif'];
if (!files) {
return true;
}
for (const file of files) {
if (!allowed.includes(file.type)) {
return 'Upload PNG, JPEG, or GIF only.';
}
}
return true;
};
const handleUpload = (file: File) => {
const nextPreview = URL.createObjectURL(file);
setPreview((current) => {
if (current) {
URL.revokeObjectURL(current);
}
return nextPreview;
});
};
return (
<div className="flex items-center gap-4">
<Avatar
size={72}
src={preview}
icon={<PiUser className="size-6" />}
className={preview ? '' : 'border-2 border-dashed'}
/>
<div className="grid gap-2">
<p className="text-sm font-medium text-foreground">
Profile photo
</p>
<Upload
showList={false}
uploadLimit={1}
accept="image/png,image/jpeg,image/gif"
beforeUpload={beforeUpload}
onChange={handleUpload}
>
<Button size="sm" icon={<PiImageSquare />}>
Upload image
</Button>
</Upload>
</div>
</div>
);
}Controlled
Preview
Dark
Controlled file count
2 filesVendor-agreement.pdf
232 kbInvoice-export.zip
118 kbimport { useState } from 'react';
import Upload from '@/components/ui/Upload';
import Tag from '@/components/ui/Tag';
const createDemoFile = (name: string, size: number, type: string) =>
({ name, size, type }) as File;
const INITIAL_FILES = [
createDemoFile('Vendor-agreement.pdf', 232000, 'application/pdf'),
createDemoFile(
'Invoice-export.zip',
118000,
'application/x-zip-compressed',
),
];
export default function ControlledDemo() {
const [files, setFiles] = useState<File[]>(INITIAL_FILES);
return (
<div className="grid w-full max-w-xl gap-4">
<div className="flex items-center justify-between gap-4">
<p className="text-sm text-muted-foreground">
Controlled file count
</p>
<Tag
size="sm"
className="border-transparent bg-palette-gray-soft text-palette-gray-soft-foreground"
>
{files.length} files
</Tag>
</div>
<Upload
multiple
fileList={files}
onChange={(_, fileList) => setFiles(fileList)}
onFileRemove={(_, fileList) => setFiles(fileList)}
/>
</div>
);
}Custom File Item
Preview
Dark
Recording.mp3
audio/mpeg4180 KB
Release-notes.txt
text/plain21 KB
Archive.zip
application/x-zip-compressed1620 KB
import { useState } from 'react';
import Upload from '@/components/ui/Upload';
import Button from '@/components/ui/Button';
import CloseButton from '@/components/ui/CloseButton';
import Tag from '@/components/ui/Tag';
const createDemoFile = (name: string, size: number, type: string) =>
({ name, size, type }) as File;
const INITIAL_FILES = [
createDemoFile('Recording.mp3', 4180000, 'audio/mpeg'),
createDemoFile('Release-notes.txt', 21000, 'text/plain'),
createDemoFile('Archive.zip', 1620000, 'application/x-zip-compressed'),
];
const formatSize = (size: number) => `${Math.round(size / 1000)} KB`;
export default function CustomFileItemDemo() {
const [files, setFiles] = useState<File[]>(INITIAL_FILES);
return (
<Upload
fileList={files}
onChange={(_, fileList) => setFiles(fileList)}
onFileRemove={(_, fileList) => setFiles(fileList)}
fileListClass="w-full max-w-2xl rounded-card border"
customFileItem={({ file, index, onRemove, FileIcon }) => {
const isLast = index === files.length - 1;
return (
<div
className={`flex items-center justify-between gap-4 p-4 ${
isLast ? '' : 'border-b'
}`}
>
<div className="flex min-w-0 items-center gap-4">
<FileIcon file={file} />
<div className="grid min-w-0 gap-2">
<p className="truncate text-sm font-medium text-foreground">
{file.name}
</p>
<div className="flex flex-wrap items-center gap-2">
<Tag
size="sm"
className="border-transparent bg-palette-cyan-soft text-palette-cyan-soft-foreground"
>
{file.type || 'file'}
</Tag>
<span className="text-xs text-muted-foreground">
{formatSize(file.size)}
</span>
</div>
</div>
</div>
<CloseButton
aria-label={`Remove ${file.name}`}
onClick={onRemove}
/>
</div>
);
}}
/>
);
}Disabled
Preview
Dark
Uploading is unavailable for archived records.
import Upload from '@/components/ui/Upload';
export default function DisabledDemo() {
return (
<div className="grid w-full max-w-xl gap-4">
<Upload disabled />
<Upload draggable disabled className="w-full">
<div className="py-8 text-center text-sm text-muted-foreground">
Uploading is unavailable for archived records.
</div>
</Upload>
</div>
);
}API
Upload manages browser file selection and an in-memory list of selected files. It does not send files to a server by itself; use onChange, beforeUpload, and customFileItem to connect it to the workflow around it.
| Prop | Description | Type | Default |
|---|---|---|---|
accept | File accept string forwarded to the hidden file input. | string | - |
beforeUpload | Synchronously validates selected files before they are added to the list. Return false or a message string to block them. | UploadBeforeUpload | - |
children | Custom trigger content. Without children, Upload renders its default button or dropzone text. | ReactNode | - |
customFileItem | Custom renderer for each listed file. Receives the file, index, remove callback, and internal FileIcon renderer. | UploadCustomFileItem | - |
disabled | Prevents file selection and applies disabled styling. | boolean | false |
draggable | Renders a dashed drop area and enables drag-over feedback. | boolean | false |
fileList | Controlled file list. When omitted, Upload stores selected files internally. | File[] | - |
fileListClass | Class names applied to the file-list wrapper. | string | - |
fileItemClass | Class names applied to default file rows. | string | - |
multiple | Allows selecting more than one file. | boolean | - |
onBeforeUpload | Called when beforeUpload blocks a file, with false or the returned message. | (result: boolean | string) => void | - |
onChange | Called after a valid file is added. Receives the first selected file and the full list. | UploadChangeHandler | - |
onFileRemove | Called after a file is removed. Receives the removed file and the next list. | UploadRemoveHandler | - |
showList | Shows the selected file list below the trigger. | boolean | true |
tip | Hint content rendered below the trigger. | string | ReactNode | - |
uploadLimit | Limit checked before adding another selection. When set to 1, the next valid upload replaces the current file. | number | - |
className | Extra classes for the root upload container. | string | - |
ref | Ref forwarded to the root upload container. | Ref<HTMLDivElement> | - |
...nativeProps | Native props forwarded through the root container type. | UploadNativeProps | - |
Types
type UploadBeforeUpload = (
files: FileList | null,
fileList: File[],
) => boolean | string
type UploadChangeHandler = (file: File, fileList: File[]) => void
type UploadRemoveHandler = (file: File, fileList: File[]) => void
type UploadCustomFileItem = (props: {
file: File
index: number
onRemove: () => void
FileIcon: (props: FileIconProps) => JSX.Element
}) => ReactNode
type FileIconProps = {
file: File
}
type UploadNativeProps = Omit<ComponentPropsWithRef<'div'>, 'onChange'>