Shadcn File Upload for React and Tailwind
A component that allows users to upload files.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/file-upload.jsonpnpm dlx shadcn@latest add https://kit.dev/r/file-upload.jsonnpx shadcn@latest add https://kit.dev/r/file-upload.jsonyarn shadcn@latest add https://kit.dev/r/file-upload.json<Step>This component depends on Button. Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import { ark } from "@ark-ui/react/factory";
import {
FileUpload as ArkFileUpload,
useFileUploadContext as useArkFileUploadContext,
} from "@ark-ui/react/file-upload";
import { UploadIcon, XIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
export const useFileUpload = useArkFileUploadContext;
export const FileUpload = (
props: React.ComponentProps<typeof ArkFileUpload.Root>
) => {
const { className, children, ...rest } = props;
return (
<ArkFileUpload.Root
className={cn(
"group/file-upload",
"relative",
"flex flex-col justify-center gap-4",
className
)}
data-slot="file-upload"
{...rest}
>
{children}
<ArkFileUpload.HiddenInput />
</ArkFileUpload.Root>
);
};
export const FileUploadTrigger = (
props: React.ComponentProps<typeof ArkFileUpload.Trigger>
) => <ArkFileUpload.Trigger data-slot="file-upload-trigger" {...props} />;
export const FileUploadDropzone = (
props: React.ComponentProps<typeof ArkFileUpload.Dropzone>
) => {
const { className, ...rest } = props;
return (
<ArkFileUpload.Dropzone
className={cn(
"[--space:--spacing(6)]",
"p-(--space)",
"flex flex-col items-center justify-center gap-2",
"text-center",
"rounded-2xl border-2 border-input border-dashed",
"cursor-pointer",
"data-cover:absolute data-cover:inset-0 data-cover:flex data-cover:items-center data-cover:justify-center",
"data-dragging:border-primary data-dragging:bg-primary/10",
"data-invalid:border-destructive dark:data-invalid:border-destructive-foreground",
className
)}
data-slot="file-upload-dropzone"
{...rest}
/>
);
};
export const FileUploadDropzoneIcon = (
props: React.ComponentProps<typeof ark.div>
) => {
const { className, children, ...rest } = props;
return (
<ark.div
className={cn(
"p-3",
"bg-muted/48",
"text-muted-foreground",
"rounded-full border",
"group-data-dragging/file-upload:border-primary/24 group-data-dragging/file-upload:bg-primary/5 group-data-dragging/file-upload:text-primary",
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
data-slot="file-upload-dropzone-icon"
{...rest}
>
{children || <UploadIcon />}
</ark.div>
);
};
export const FileUploadTitle = (
props: React.ComponentProps<typeof ark.div>
) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn("font-medium text-foreground text-sm", className)}
data-slot="file-upload-title"
{...rest}
/>
);
};
export const FileUploadDescription = (
props: React.ComponentProps<typeof ark.div>
) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn("font-medium text-muted-foreground text-sm", className)}
data-slot="file-upload-title"
{...rest}
/>
);
};
export const FileUploadHelper = (
props: React.ComponentProps<typeof ark.div>
) => {
const { className, ...rest } = props;
return (
<ark.div
className={cn("text-muted-foreground text-xs", className)}
data-slot="file-upload-dropzone-helper"
{...rest}
/>
);
};
export const FileUploadItemGroup = (
props: React.ComponentProps<typeof ArkFileUpload.ItemGroup>
) => <ArkFileUpload.ItemGroup data-slot="file-upload-item-group" {...props} />;
interface FileUploadListProps
extends Omit<React.ComponentProps<typeof ArkFileUpload.Item>, "file"> {}
export const FileUploadList = (props: FileUploadListProps) => {
const { className, ...rest } = props;
const fileUpload = useFileUpload();
const files = fileUpload.acceptedFiles;
if (files.length === 0) {
return null;
}
return (
<FileUploadItemGroup className="flex flex-col gap-2">
{files.map((file, index) => {
const isImage = file.type.startsWith("image/");
const key = `${file.name}-${index}`;
const extension = file.name.split(".").pop();
return (
<FileUploadItem
className={cn(
"flex-1 items-start justify-start gap-4",
"bg-card",
"p-2",
"rounded-xl border",
"fade-in-0 slide-in-from-top-5 animate-in",
"motion-reduce:animate-none!",
className
)}
file={file}
key={key}
{...rest}
>
<FileUploadItemPreview
className="size-8"
{...(isImage ? { type: "image/*" } : { type: ".*" })}
>
{isImage ? (
<FileUploadItemPreviewImage />
) : (
<span className="uppercase">{extension}</span>
)}
</FileUploadItemPreview>
<div className="min-w-0 flex-1 overflow-hidden">
<FileUploadItemName />
<FileUploadItemSize />
</div>
<FileUploadItemDeleteTrigger
asChild
className="me-auto rtl:ms-auto"
>
<Button
className={cn(
"rounded-lg",
"hover:bg-destructive/10 hover:text-destructive",
"dark:hover:bg-destructive-foreground/10 dark:hover:text-destructive-foreground"
)}
size="icon-xs"
variant="ghost"
>
<XIcon />
</Button>
</FileUploadItemDeleteTrigger>
</FileUploadItem>
);
})}
</FileUploadItemGroup>
);
};
export const FileUploadItem = (
props: React.ComponentProps<typeof ArkFileUpload.Item>
) => {
const { className, ...rest } = props;
return (
<ArkFileUpload.Item
className={cn("relative inline-flex", className)}
data-slot="file-upload-item"
{...rest}
/>
);
};
export const FileUploadItemPreview = (
props: React.ComponentProps<typeof ArkFileUpload.ItemPreview>
) => {
const { className, ...rest } = props;
return (
<ArkFileUpload.ItemPreview
className={cn(
"flex shrink-0 items-center justify-center",
"font-semibold text-[0.5rem] text-primary",
"bg-primary/10",
"select-none",
"rounded-full",
className
)}
data-slot="file-upload-item-preview"
{...rest}
/>
);
};
export const FileUploadItemPreviewImage = (
props: React.ComponentProps<typeof ArkFileUpload.ItemPreviewImage>
) => {
const { className, ...rest } = props;
return (
<ArkFileUpload.ItemPreviewImage
className={cn(
"aspect-square size-full",
"object-cover",
"rounded-lg",
className
)}
data-slot="file-upload-item-preview-image"
{...rest}
/>
);
};
export const FileUploadItemName = (
props: React.ComponentProps<typeof ArkFileUpload.ItemName>
) => {
const { className, ...rest } = props;
return (
<ArkFileUpload.ItemName
className={cn(
"truncate font-medium text-xs",
"min-w-0",
"overflow-hidden",
className
)}
data-slot="file-upload-item-name"
{...rest}
/>
);
};
export const FileUploadItemSize = (
props: React.ComponentProps<typeof ArkFileUpload.ItemSizeText>
) => {
const { className, ...rest } = props;
return (
<ArkFileUpload.ItemSizeText
className={cn("text-muted-foreground text-xs", className)}
data-slot="file-upload-item-size"
{...rest}
/>
);
};
export const FileUploadItemDeleteTrigger = (
props: React.ComponentProps<typeof ArkFileUpload.ItemDeleteTrigger>
) => (
<ArkFileUpload.ItemDeleteTrigger
data-slot="file-upload-item-delete-trigger"
{...props}
/>
);
export const FileUploadClearTrigger = (
props: React.ComponentProps<typeof ArkFileUpload.ClearTrigger>
) => (
<ArkFileUpload.ClearTrigger
data-slot="file-upload-clear-trigger"
{...props}
/>
);
export const FileUploadRootProvider = (
props: React.ComponentProps<typeof ArkFileUpload.RootProvider>
) => (
<ArkFileUpload.RootProvider
data-slot="file-upload-root-provider"
{...props}
/>
);Update the import paths to match your project setup.
Anatomy
FileUpload
├── FileUploadTrigger
├── FileUploadDropzone
│ ├── FileUploadDropzoneIcon
│ ├── FileUploadTitle
│ ├── FileUploadDescription
│ └── FileUploadHelper
├── FileUploadItemGroup
│ └── FileUploadList
│ └── FileUploadItem
│ ├── FileUploadItemPreview
│ │ └── FileUploadItemPreviewImage
│ ├── FileUploadItemName
│ ├── FileUploadItemSize
│ └── FileUploadItemDeleteTrigger
└── FileUploadClearTriggerUsage
import {
FileUpload,
FileUploadDropzone,
FileUploadDropzoneIcon,
FileUploadTitle,
FileUploadDescription,
FileUploadHelper,
FileUploadTrigger,
FileUploadList,
} from "@/components/ui/file-upload";<FileUpload maxFiles={2} accept={["image/*png,image/jpeg*", ".pdf"]}>
<FileUploadDropzone>
<FileUploadDropzoneIcon />
<FileUploadTitle>Drop files here</FileUploadTitle>
<FileUploadDescription>or</FileUploadDescription>
<FileUploadTrigger />
<FileUploadHelper>You can upload up to 2 files at a time.</FileUploadHelper>
</FileUploadDropzone>
<FileUploadList />
</FileUpload>Examples
Multiple Files
Use the maxFiles prop to allow uploading multiple files at once.
Trigger only
Dropzone only
Use FileUploadDropzone to enable drag-and-drop. It exposes a data-dragging attribute for styling the drag state.
Accepted Files
Use the accept prop to restrict file types.
With Field
Use Field to add helper text and error handling.
Custom Preview
const { acceptedFiles } = useFileUploadContext();
return (
<div>
{acceptedFiles.map((file) => (
<div key={file.name}>{file.name}</div>
))}
</div>
);Media Capture
Use the capture prop to capture and upload media directly from the device camera.
The capture prop is only supported on mobile devices.
Directory Upload
Use the directory prop to upload entire folders.
Attention: Directory Upload
When uploading directories with many files, set maxFiles to a higher value or remove it entirely to prevent rejections.
Clear Trigger
Use FileUploadClearTrigger to remove all uploaded files at once.
Custom spacing
Use [--space:--spacing("value")] on FileUploadDropzone to adjust internal spacing.
Default spacing is --spacing(6).
You can use breakpoint utilities to change the internal spacing at different screen sizes.
md:[--space:--spacing(6)] lg:[--space:--spacing(8)]API Reference
FileUpload
Root component. Manages file state and wraps the upload UI.
| Prop | Type | Default |
|---|---|---|
accept | string | Record<string, string[]> | FileMimeType[] | - |
acceptedFiles | File[] | - |
allowDrop | boolean | true |
capture | "user" | "environment" | - |
defaultAcceptedFiles | File[] | - |
directory | boolean | false |
disabled | boolean | - |
invalid | boolean | - |
maxFiles | number | 1 |
maxFileSize | number | Infinity |
minFileSize | number | 0 |
name | string | - |
onFileAccept | (details: FileAcceptDetails) => void | - |
onFileChange | (details: FileChangeDetails) => void | - |
onFileReject | (details: FileRejectDetails) => void | - |
readOnly | boolean | - |
required | boolean | - |
transformFiles | (files: File[]) => Promise<File[]> | - |
validate | (file: File, details: FileValidateDetails) => FileError[] | null | - |
className | string | - |
FileUploadTrigger
Opens the native file picker on click.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
FileUploadDropzone
Drop zone for drag-and-drop. Exposes data-dragging when files are dragged over.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
disableClick | boolean | - |
className | string | - |
| Attribute | Default |
|---|---|
--space | --spacing(8) |
FileUploadDropzoneIcon
Icon shown in the dropzone.
| Prop | Type | Default |
|---|---|---|
className | string | - |
FileUploadTitle
Title text for the dropzone.
| Prop | Type | Default |
|---|---|---|
className | string | - |
FileUploadDescription
Description or hint for the dropzone.
| Prop | Type | Default |
|---|---|---|
className | string | - |
FileUploadHelper
Small text that provides additional information about the dropzone.
| Prop | Type | Default |
|---|---|---|
className | string | - |
FileUploadList
List of uploaded files. Hidden when empty.
| Prop | Type | Default |
|---|---|---|
className | string | - |
FileUploadItemGroup
List of uploaded files.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
type | "accepted" | "rejected" | - |
className | string | - |
FileUploadItem
Single uploaded file with preview and remove button.
| Prop | Type | Default |
|---|---|---|
file | File | required |
asChild | boolean | - |
className | string | - |
FileUploadItemPreview
Preview container for a file.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
type | string | '.*' |
className | string | - |
FileUploadItemPreviewImage
Image thumbnail for image files. Use inside FileUploadItemPreview with type="image/*".
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
FileUploadItemName
Shows the file name.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
FileUploadItemSize
Shows the formatted file size.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
FileUploadItemDeleteTrigger
Removes a single file from the list.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
FileUploadClearTrigger
Removes all files from the list.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
className | string | - |
useFileUploadContext
Hook to access file upload state and methods. Use inside FileUpload or FileUploadRootProvider.
| Property | Type |
|---|---|
acceptedFiles | File[] |
rejectedFiles | FileRejection[] |
openFilePicker | () => void |
deleteFile | (file: File, type?: ItemType) => void |
clearFiles | () => void |
clearRejectedFiles | () => void |
getFileSize | (file: File) => string |
createFileUrl | (file: File, cb: (url: string) => void) => () => void |
setClipboardFiles | (dt: DataTransfer) => boolean |
For a complete list of props, see the Ark UI documentation.