File Upload
A file upload component with drag and drop, previewing, and progress tracking.
Drag & drop files here
Or click to browse (max 2 files, up to 5MB each)
"use client";
import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadList,
FileUploadTrigger,
} from "@/components/ui/file-upload";
export default function FileUploadDemo() {
const [files, setFiles] = React.useState<File[]>([]);
const onFileReject = React.useCallback((file: File, message: string) => {
toast(message, {
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
});
}, []);
return (
<FileUpload
maxFiles={2}
maxSize={5 * 1024 * 1024}
css={{ w: "full", maxW: "md" }}
value={files}
onValueChange={setFiles}
onFileReject={onFileReject}
multiple
>
<FileUploadDropzone>
<styled.div
css={{
display: "flex",
flexDir: "column",
alignItems: "center",
gap: "1",
textAlign: "center",
}}
>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
rounded: "full",
borderWidth: "1px",
p: "2.5",
}}
>
<LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
</styled.div>
<styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
Drag & drop files here
</styled.p>
<styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
Or click to browse (max 2 files, up to 5MB each)
</styled.p>
</styled.div>
<FileUploadTrigger asChild>
<Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
Browse files
</Button>
</FileUploadTrigger>
</FileUploadDropzone>
<FileUploadList>
{files.map((file, index) => (
<FileUploadItem key={index} value={file}>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemDelete asChild>
<Button variant="ghost" size="icon" css={{ w: "7", h: "7" }}>
<LuX />
</Button>
</FileUploadItemDelete>
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
);
}
Installation
npx nore-ui-cli@latest add file-uploadUsage
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadTrigger,
} from "@/components/ui/file-upload";
<FileUpload>
<FileUploadDropzone />
<FileUploadTrigger />
<FileUploadList>
<FileUploadItem>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemProgress />
<FileUploadItemDelete />
</FileUploadItem>
</FileUploadList>
</FileUpload>
Examples
With Validation
Validate files with the onFileValidate prop on the root component based on type, size, and custom rules. This will override the default file rejection message.
Drag & drop files here
Or click to browse (max 2 files)
"use client";
import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadList,
FileUploadTrigger,
} from "@/components/ui/file-upload";
export default function FileUploadValidation() {
const [files, setFiles] = React.useState<File[]>([]);
const onFileValidate = React.useCallback(
(file: File): string | null => {
// Validate max files
if (files.length >= 2) {
return "You can only upload up to 2 files";
}
// Validate file type (only images)
if (!file.type.startsWith("image/")) {
return "Only image files are allowed";
}
// Validate file size (max 2MB)
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
if (file.size > MAX_SIZE) {
return `File size must be less than ${MAX_SIZE / (1024 * 1024)}MB`;
}
return null;
},
[files]
);
const onFileReject = React.useCallback((file: File, message: string) => {
toast(message, {
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
});
}, []);
return (
<FileUpload
value={files}
onValueChange={setFiles}
onFileValidate={onFileValidate}
onFileReject={onFileReject}
accept="image/*"
maxFiles={2}
css={{ w: "full", maxW: "md" }}
multiple
>
<FileUploadDropzone>
<styled.div css={{ display: "flex", flexDir: "column", alignItems: "center", gap: "1" }}>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
rounded: "full",
borderWidth: "1px",
p: "2.5",
}}
>
<LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
</styled.div>
<styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
Drag & drop files here
</styled.p>
<styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
Or click to browse (max 2 files)
</styled.p>
</styled.div>
<FileUploadTrigger asChild>
<Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
Browse files
</Button>
</FileUploadTrigger>
</FileUploadDropzone>
<FileUploadList>
{files.map((file) => (
<FileUploadItem key={file.name} value={file}>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemDelete asChild>
<Button variant="ghost" size="icon" css={{ w: "7", h: "7" }}>
<LuX />
</Button>
</FileUploadItemDelete>
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
);
}
Direct Upload
Upload files directly with the onUpload prop on the root component.
Drag & drop files here
Or click to browse (max 2 files)
"use client";
import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadItemProgress,
FileUploadList,
FileUploadTrigger,
type FileUploadProps,
} from "@/components/ui/file-upload";
export default function FileUploadDirectUpload() {
const [files, setFiles] = React.useState<File[]>([]);
const onUpload: NonNullable<FileUploadProps["onUpload"]> = React.useCallback(
async (files, { onProgress, onSuccess, onError }) => {
try {
// Process each file individually
const uploadPromises = files.map(async (file) => {
try {
// Simulate file upload with progress
const totalChunks = 10;
let uploadedChunks = 0;
// Simulate chunk upload with delays
for (let i = 0; i < totalChunks; i++) {
// Simulate network delay (100-300ms per chunk)
await new Promise((resolve) => setTimeout(resolve, Math.random() * 200 + 100));
// Update progress for this specific file
uploadedChunks++;
const progress = (uploadedChunks / totalChunks) * 100;
onProgress(file, progress);
}
// Simulate server processing delay
await new Promise((resolve) => setTimeout(resolve, 500));
onSuccess(file);
} catch (error) {
onError(file, error instanceof Error ? error : new Error("Upload failed"));
}
});
// Wait for all uploads to complete
await Promise.all(uploadPromises);
} catch (error) {
// This handles any error that might occur outside the individual upload processes
console.error("Unexpected error during upload:", error);
}
},
[]
);
const onFileReject = React.useCallback((file: File, message: string) => {
toast(message, {
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
});
}, []);
return (
<FileUpload
value={files}
onValueChange={setFiles}
onUpload={onUpload}
onFileReject={onFileReject}
maxFiles={2}
css={{ w: "full", maxW: "md" }}
multiple
>
<FileUploadDropzone>
<styled.div
css={{
display: "flex",
flexDir: "column",
alignItems: "center",
gap: "1",
textAlign: "center",
}}
>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
rounded: "full",
borderWidth: "1px",
p: "2.5",
}}
>
<LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
</styled.div>
<styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
Drag & drop files here
</styled.p>
<styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
Or click to browse (max 2 files)
</styled.p>
</styled.div>
<FileUploadTrigger asChild>
<Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
Browse files
</Button>
</FileUploadTrigger>
</FileUploadDropzone>
<FileUploadList>
{files.map((file, index) => (
<FileUploadItem key={index} value={file} css={{ flexDir: "column" }}>
<styled.div css={{ display: "flex", w: "full", alignItems: "center", gap: "2" }}>
<FileUploadItemPreview />
<FileUploadItemMetadata />
<FileUploadItemDelete asChild>
<Button variant="ghost" size="icon" css={{ w: "7", h: "7" }}>
<LuX />
</Button>
</FileUploadItemDelete>
</styled.div>
<FileUploadItemProgress />
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
);
}
Circular Progress
Render a circular progress indicator instead of a linear one by enabling the circular prop on the FileUploadItemProgress component.
Drag & drop files here
Or click to browse (max 10 files, up to 5MB each)
"use client";
import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadItemProgress,
FileUploadList,
FileUploadTrigger,
} from "@/components/ui/file-upload";
export default function FileUploadCircularProgress() {
const [files, setFiles] = React.useState<File[]>([]);
const onUpload = React.useCallback(
async (
files: File[],
{
onProgress,
onSuccess,
onError,
}: {
onProgress: (file: File, progress: number) => void;
onSuccess: (file: File) => void;
onError: (file: File, error: Error) => void;
}
) => {
try {
// Process each file individually
const uploadPromises = files.map(async (file) => {
try {
// Simulate file upload with progress
const totalChunks = 10;
let uploadedChunks = 0;
// Simulate chunk upload with delays
for (let i = 0; i < totalChunks; i++) {
// Simulate network delay (100-300ms per chunk)
await new Promise((resolve) => setTimeout(resolve, Math.random() * 200 + 100));
// Update progress for this specific file
uploadedChunks++;
const progress = (uploadedChunks / totalChunks) * 100;
onProgress(file, progress);
}
// Simulate server processing delay
await new Promise((resolve) => setTimeout(resolve, 500));
onSuccess(file);
} catch (error) {
onError(file, error instanceof Error ? error : new Error("Upload failed"));
}
});
// Wait for all uploads to complete
await Promise.all(uploadPromises);
} catch (error) {
// This handles any error that might occur outside the individual upload processes
console.error("Unexpected error during upload:", error);
}
},
[]
);
const onFileReject = React.useCallback((file: File, message: string) => {
toast(message, {
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
});
}, []);
return (
<FileUpload
value={files}
onValueChange={setFiles}
maxFiles={10}
maxSize={5 * 1024 * 1024}
css={{ w: "full", maxW: "md" }}
onUpload={onUpload}
onFileReject={onFileReject}
multiple
>
<FileUploadDropzone>
<styled.div
css={{
display: "flex",
flexDir: "column",
alignItems: "center",
gap: "1",
textAlign: "center",
}}
>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
rounded: "full",
borderWidth: "1px",
p: "2.5",
}}
>
<LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
</styled.div>
<styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
Drag & drop files here
</styled.p>
<styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
Or click to browse (max 10 files, up to 5MB each)
</styled.p>
</styled.div>
<FileUploadTrigger asChild>
<Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
Browse files
</Button>
</FileUploadTrigger>
</FileUploadDropzone>
<FileUploadList orientation="horizontal">
{files.map((file, index) => (
<FileUploadItem key={index} value={file} css={{ p: "0" }}>
<FileUploadItemPreview
css={{
w: "20",
h: "20",
"& > svg": {
w: "12",
h: "12",
},
}}
>
<FileUploadItemProgress variant="circular" size={40} />
</FileUploadItemPreview>
<FileUploadItemMetadata css={{ srOnly: true }} />
<FileUploadItemDelete asChild>
<Button
variant="secondary"
size="icon"
css={{ pos: "absolute", top: "-1", right: "-1", w: "5", h: "5", rounded: "full" }}
>
<LuX className={css({ w: "3", h: "3" })} />
</Button>
</FileUploadItemDelete>
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
);
}
Fill Progress
Render a fill progress indicator instead of a linear one by enabling the fill prop on the FileUploadItemProgress component.
Drag & drop files here
Or click to browse (max 10 files, up to 5MB each)
"use client";
import * as React from "react";
import { LuUpload, LuX } from "react-icons/lu";
import { toast } from "sonner";
import { css } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import {
FileUpload,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDelete,
FileUploadItemMetadata,
FileUploadItemPreview,
FileUploadItemProgress,
FileUploadList,
FileUploadTrigger,
} from "@/components/ui/file-upload";
export default function FileUploadFillProgress() {
const [files, setFiles] = React.useState<File[]>([]);
const onUpload = React.useCallback(
async (
files: File[],
{
onProgress,
onSuccess,
onError,
}: {
onProgress: (file: File, progress: number) => void;
onSuccess: (file: File) => void;
onError: (file: File, error: Error) => void;
}
) => {
try {
// Process each file individually
const uploadPromises = files.map(async (file) => {
try {
// Simulate file upload with progress
const totalChunks = 10;
let uploadedChunks = 0;
// Simulate chunk upload with delays
for (let i = 0; i < totalChunks; i++) {
// Simulate network delay (100-300ms per chunk)
await new Promise((resolve) => setTimeout(resolve, Math.random() * 200 + 100));
// Update progress for this specific file
uploadedChunks++;
const progress = (uploadedChunks / totalChunks) * 100;
onProgress(file, progress);
}
// Simulate server processing delay
await new Promise((resolve) => setTimeout(resolve, 500));
onSuccess(file);
} catch (error) {
onError(file, error instanceof Error ? error : new Error("Upload failed"));
}
});
// Wait for all uploads to complete
await Promise.all(uploadPromises);
} catch (error) {
// This handles any error that might occur outside the individual upload processes
console.error("Unexpected error during upload:", error);
}
},
[]
);
const onFileReject = React.useCallback((file: File, message: string) => {
toast(message, {
description: `"${file.name.length > 20 ? `${file.name.slice(0, 20)}...` : file.name}" has been rejected`,
});
}, []);
return (
<FileUpload
value={files}
onValueChange={setFiles}
maxFiles={10}
maxSize={5 * 1024 * 1024}
css={{ w: "full", maxW: "md" }}
onUpload={onUpload}
onFileReject={onFileReject}
multiple
>
<FileUploadDropzone>
<styled.div
css={{
display: "flex",
flexDir: "column",
alignItems: "center",
gap: "1",
textAlign: "center",
}}
>
<styled.div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
rounded: "full",
borderWidth: "1px",
p: "2.5",
}}
>
<LuUpload className={css({ w: "6", h: "6", color: "muted.fg" })} />
</styled.div>
<styled.p css={{ fontWeight: "medium", textStyle: "sm" }}>
Drag & drop files here
</styled.p>
<styled.p css={{ color: "muted.fg", textStyle: "xs" }}>
Or click to browse (max 10 files, up to 5MB each)
</styled.p>
</styled.div>
<FileUploadTrigger asChild>
<Button variant="outline" size="sm" css={{ mt: "2", w: "fit" }}>
Browse files
</Button>
</FileUploadTrigger>
</FileUploadDropzone>
<FileUploadList orientation="horizontal">
{files.map((file, index) => (
<FileUploadItem key={index} value={file} css={{ p: "0" }}>
<FileUploadItemPreview css={{ w: "20", h: "20" }}>
<FileUploadItemProgress variant="fill" />
</FileUploadItemPreview>
<FileUploadItemMetadata css={{ srOnly: true }} />
<FileUploadItemDelete asChild>
<Button
variant="secondary"
size="icon"
css={{
position: "absolute",
top: "-1",
right: "-1",
w: "5",
h: "5",
rounded: "full",
}}
>
<LuX className={css({ w: "3", h: "3" })} />
</Button>
</FileUploadItemDelete>
</FileUploadItem>
))}
</FileUploadList>
</FileUpload>
);
}
API Reference
FileUpload
The main container component for the file upload functionality.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
value? | File[] | |
defaultValue? | File[] | |
onValueChange? | (files: File[]) => void | |
onAccept? | (files: File[]) => void | |
onFileAccept? | (file: File) => void | |
onFileReject? | (file: File, message: string) => void | |
onFileValidate? | (file: File) => string | null | undefined | |
onUpload? | (files: File[], options: { onProgress: (file: File, progress: number) => void; onSuccess: (file: File) => void; onError: (file: File, error: Error) => void }) => Promise<void> | void | |
accept? | string | |
maxFiles? | number | |
maxSize? | number | |
dir? | "ltr" | "rtl" | |
label? | string | |
name? | string | |
asChild? | boolean | |
disabled? | boolean | |
invalid? | boolean | |
multiple? | boolean | |
required? | boolean |
FileUploadDropzone
A container for drag and drop functionality.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
asChild? | boolean |
FileUploadTrigger
A button that opens the file selection dialog.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
asChild? | boolean |
FileUploadList
A container for displaying uploaded files.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
orientation? | "horizontal" | "vertical" | "vertical" |
forceMount? | boolean | |
asChild? | boolean |
FileUploadItem
Individual file item component.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
value | File | |
asChild? | boolean |
FileUploadItemPreview
Displays a preview of the file, showing an image for image files or an appropriate icon for other file types.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
render? | (file: File, fallback: () => ReactNode) => ReactNode | |
asChild? | boolean |
FileUploadItemMetadata
Displays file information such as name, size, and error messages.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
size? | "default" | "sm" | "default" |
asChild? | boolean |
FileUploadItemProgress
Shows the upload progress for a file.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
variant? | "fill" | "circular" | "linear" | "linear" |
size? | number | 40 |
forceMount? | boolean | |
asChild? | boolean |
FileUploadItemDelete
A button to remove a file from the list.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
asChild? | boolean |
FileUploadClear
A button to clear all files from the list.
| Prop | Type | Default |
|---|---|---|
css | SystemStyleObject | |
forceMount? | boolean | |
asChild? | boolean |