File Upload
A shadcn-style file upload component built with Ark UI primitives.
Try a large file to see rejectedFiles
import { UploadIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadRejectedDemo = () => (
<FileUpload className="max-w-md" maxFileSize={500} maxFiles={2}>
<FileUploadLabel>Rejections (< 500 bytes)</FileUploadLabel>
<FileUploadDropzone>
<UploadIcon className="size-6 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Try a large file to see rejectedFiles
</span>
</FileUploadDropzone>
<FileUploadContext>
{({ rejectedFiles }) =>
rejectedFiles.length > 0 ? (
<ul className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-destructive text-xs">
{rejectedFiles.map((rej) => (
<li key={`${rej.file.name}-${rej.file.size}`}>
{rej.file.name}: {rej.errors.join(", ")}
</li>
))}
</ul>
) : null
}
</FileUploadContext>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadRejectedDemo;
Installation
npx shadcn@latest add @ark-cn/file-uploadInstall the dependency required by this primitive:
npm install @ark-ui/react class-variance-authorityCopy the component source into your app:
TSXcomponents/ui/file-upload.tsx
"use client";
import {
FileUpload as FileUploadPrimitive,
useFileUpload,
} from "@ark-ui/react/file-upload";
import type { VariantProps } from "class-variance-authority";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export { useFileUpload };
export type FileUploadItemPreviewImageProps =
FileUploadPrimitive.ItemPreviewImageProps;
export const FileUpload = ({
className,
children,
...props
}: FileUploadPrimitive.RootProps) => (
<FileUploadPrimitive.Root
className={cn("flex w-full flex-col gap-2 text-foreground", className)}
data-slot="file-upload"
{...props}
>
{children}
<FileUploadPrimitive.HiddenInput />
</FileUploadPrimitive.Root>
);
export const FileUploadLabel = ({
className,
...props
}: FileUploadPrimitive.LabelProps) => (
<FileUploadPrimitive.Label
className={cn(
"font-medium text-foreground text-sm leading-none data-disabled:opacity-64",
className,
)}
data-slot="file-upload-label"
{...props}
/>
);
export const FileUploadDropzone = ({
className,
...props
}: FileUploadPrimitive.DropzoneProps) => (
<FileUploadPrimitive.Dropzone
className={cn(
"flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-input bg-muted/20 px-4 py-6 text-center transition-colors",
"hover:bg-muted/40 data-dragging:border-primary data-dragging:bg-primary/5 data-dragging:border-solid",
"data-invalid:border-destructive data-disabled:cursor-not-allowed data-disabled:opacity-64",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
className,
)}
data-slot="file-upload-dropzone"
{...props}
/>
);
export const FileUploadTrigger = ({
...props
}: FileUploadPrimitive.TriggerProps) => (
<FileUploadPrimitive.Trigger data-slot="file-upload-trigger" {...props} />
);
export const FileUploadClearTrigger = ({
...props
}: FileUploadPrimitive.ClearTriggerProps) => (
<FileUploadPrimitive.ClearTrigger {...props} />
);
export const FileUploadItemGroup = ({
className,
...props
}: FileUploadPrimitive.ItemGroupProps) => (
<FileUploadPrimitive.ItemGroup
className={cn("m-0 flex list-none flex-col gap-2 p-0", className)}
data-slot="file-upload-item-group"
{...props}
/>
);
export const FileUploadItem = ({
className,
...props
}: FileUploadPrimitive.ItemProps) => (
<FileUploadPrimitive.Item
className={cn(
"flex items-center gap-3 rounded-lg border border-border bg-card p-2 shadow-xs/5",
className,
)}
data-slot="file-upload-item"
{...props}
/>
);
export const FileUploadItemName = ({
className,
...props
}: FileUploadPrimitive.ItemNameProps) => (
<FileUploadPrimitive.ItemName
className={cn(
"min-w-0 flex-1 truncate font-medium text-foreground text-sm",
className,
)}
data-slot="file-upload-item-name"
{...props}
/>
);
export const FileUploadItemSizeText = ({
className,
...props
}: FileUploadPrimitive.ItemSizeTextProps) => (
<FileUploadPrimitive.ItemSizeText
className={cn("shrink-0 text-muted-foreground text-xs", className)}
data-slot="file-upload-item-size-text"
{...props}
/>
);
export const FileUploadItemDeleteTrigger = ({
className,
variant = "ghost",
size = "icon-xs",
...props
}: FileUploadPrimitive.ItemDeleteTriggerProps &
VariantProps<typeof buttonVariants>) => (
<FileUploadPrimitive.ItemDeleteTrigger
className={cn(buttonVariants({ variant, size }), className)}
data-slot="file-upload-item-delete-trigger"
{...props}
/>
);
export const FileUploadItemPreview = ({
className,
...props
}: FileUploadPrimitive.ItemPreviewProps) => (
<FileUploadPrimitive.ItemPreview
className={cn(
"flex size-10 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-5 rounded",
className,
)}
data-slot="file-upload-item-preview"
{...props}
/>
);
export const FileUploadItemPreviewImage = ({
className,
...props
}: FileUploadPrimitive.ItemPreviewImageProps) => (
<FileUploadPrimitive.ItemPreviewImage
className={cn(
"size-10 shrink-0 border border-border bg-muted object-cover rounded-[inherit]",
className,
)}
data-slot="file-upload-item-preview-image"
{...props}
/>
);
export const FileUploadContext = FileUploadPrimitive.Context;
export const FileUploadRootProvider = ({
className,
children,
...props
}: FileUploadPrimitive.RootProviderProps) => (
<FileUploadPrimitive.RootProvider
className={cn("flex w-full flex-col gap-2", className)}
data-slot="file-upload-root-provider"
{...props}
>
{children}
<FileUploadPrimitive.HiddenInput />
</FileUploadPrimitive.RootProvider>
);
Update import aliases to match your project setup.
Usage
import * as FileUpload from "@/components/ui/file-upload"Read exported parts in src/components/ui/file-upload.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Avatar Circle
Tap or drop image
Add your avatar
PNG, JPG up to 2MB
import { UserRoundIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemPreview,
FileUploadItemPreviewImage,
FileUploadLabel,
} from "@/components/ui/file-upload";
import { cn } from "@/lib/utils";
const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
const FileUploadAvatarCircleDemo = () => (
<FileUpload
accept={{ "image/png": [".png"], "image/jpeg": [".jpg", ".jpeg"] }}
className="w-full max-w-xs flex-col items-center gap-4"
maxFileSize={AVATAR_MAX_BYTES}
maxFiles={1}
>
<FileUploadLabel className="sr-only">Avatar photo</FileUploadLabel>
<div className="relative size-48 shrink-0">
<FileUploadContext>
{({ acceptedFiles }) => {
if (acceptedFiles.length <= 0) {
return (
<FileUploadDropzone
className={cn(
"cursor-pointer flex size-48 flex-col items-center justify-center gap-2 rounded-full border-2 border-dashed border-input bg-muted/20 p-4 transition-colors",
"hover:bg-muted/35 data-dragging:border-primary data-dragging:bg-primary/5",
)}
>
<UserRoundIcon className="size-12 text-muted-foreground" />
<span className="px-2 text-center text-muted-foreground text-xs leading-tight">
Tap or drop image
</span>
</FileUploadDropzone>
);
}
return (
<FileUploadItemGroup
className={cn(
"absolute inset-0 m-0 flex items-center justify-center p-0",
)}
>
{acceptedFiles.map((file) => (
<FileUploadItem
key={`${file.name}-${file.size}`}
className="relative size-full max-h-48 max-w-48 border-0 bg-transparent p-0 shadow-none"
file={file}
>
<FileUploadItemPreview
className="size-full overflow-hidden rounded-full border-0"
type="image/*"
>
<FileUploadItemPreviewImage className="size-full max-h-none max-w-none border-0 object-cover" />
</FileUploadItemPreview>
<FileUploadItemDeleteTrigger
aria-label={`Remove ${file.name}`}
className="absolute top-5 right-3 z-10 rounded-full bg-background p-1 hover:bg-muted"
>
<XIcon className="stroke-[2.5]" />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))}
</FileUploadItemGroup>
);
}}
</FileUploadContext>
</div>
<FileUploadContext>
{({ acceptedFiles }) => (
<div className="flex flex-col items-center gap-1 text-center">
<p className="font-semibold text-base text-foreground leading-tight">
{acceptedFiles.length > 0 ? "Avatar uploaded" : "Add your avatar"}
</p>
<p className="text-muted-foreground text-sm leading-snug">
PNG, JPG up to 2MB
</p>
</div>
)}
</FileUploadContext>
</FileUpload>
);
export default FileUploadAvatarCircleDemo;
Avatar Soft
Square-ish drop target
import { ImageIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadItemPreview,
FileUploadItemPreviewImage,
FileUploadItemSizeText,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadAvatarSoftDemo = () => (
<FileUpload
accept={{ "image/*": [".png", ".jpg", ".jpeg", ".webp"] }}
className="max-w-xs"
maxFiles={1}
>
<FileUploadLabel>Portrait (4px radius preview)</FileUploadLabel>
<FileUploadDropzone className="min-h-24 rounded-lg border-solid">
<ImageIcon className="size-7 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Square-ish drop target
</span>
</FileUploadDropzone>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem
key={`${file.name}-${file.size}`}
className="border-0 bg-transparent p-0 shadow-none"
file={file}
>
<FileUploadItemPreview type="image/*" className="size-20">
<FileUploadItemPreviewImage className="" />
</FileUploadItemPreview>
<div className="flex min-w-0 flex-1 flex-col justify-center gap-1">
<FileUploadItemName />
<FileUploadItemSizeText />
</div>
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadAvatarSoftDemo;
Try a large file to see rejectedFiles
import { UploadIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadRejectedDemo = () => (
<FileUpload className="max-w-md" maxFileSize={500} maxFiles={2}>
<FileUploadLabel>Rejections (< 500 bytes)</FileUploadLabel>
<FileUploadDropzone>
<UploadIcon className="size-6 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Try a large file to see rejectedFiles
</span>
</FileUploadDropzone>
<FileUploadContext>
{({ rejectedFiles }) =>
rejectedFiles.length > 0 ? (
<ul className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-destructive text-xs">
{rejectedFiles.map((rej) => (
<li key={`${rej.file.name}-${rej.file.size}`}>
{rej.file.name}: {rej.errors.join(", ")}
</li>
))}
</ul>
) : null
}
</FileUploadContext>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadRejectedDemo;
Image Preview + PDF Icon
Upload images or PDF files
import { FileIcon, FileTextIcon, UploadIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadItemPreview,
FileUploadItemPreviewImage,
FileUploadItemSizeText,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadImagePreviewPdfIconDemo = () => (
<FileUpload className="max-w-md" maxFiles={5}>
<FileUploadLabel>Image preview with PDF icon</FileUploadLabel>
<FileUploadDropzone>
<UploadIcon className="size-6 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Upload images or PDF files
</span>
</FileUploadDropzone>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemPreview type="image/*">
<FileUploadItemPreviewImage />
</FileUploadItemPreview>
<FileUploadItemPreview type="application/pdf">
<FileTextIcon className="text-rose-600" />
</FileUploadItemPreview>
<FileUploadItemPreview type=".*">
<FileIcon />
</FileUploadItemPreview>
<FileUploadItemName />
<FileUploadItemSizeText />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadImagePreviewPdfIconDemo;
Accepted Files
PNG, JPG, WEBP, and PDF
import { FileIcon, FileTextIcon, UploadIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadItemPreview,
FileUploadItemPreviewImage,
FileUploadItemSizeText,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadAcceptedFilesDemo = () => (
<FileUpload
accept={{
"application/pdf": [".pdf"],
"image/jpeg": [".jpg", ".jpeg"],
"image/png": [".png"],
"image/webp": [".webp"],
}}
className="max-w-md"
maxFiles={4}
>
<FileUploadLabel>Accepted files only</FileUploadLabel>
<FileUploadDropzone>
<UploadIcon className="size-6 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
PNG, JPG, WEBP, and PDF
</span>
</FileUploadDropzone>
<FileUploadContext>
{({ rejectedFiles }) =>
rejectedFiles.length > 0 ? (
<ul className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-destructive text-xs">
{rejectedFiles.map((rejectedFile) => (
<li key={`${rejectedFile.file.name}-${rejectedFile.file.size}`}>
{rejectedFile.file.name}: {rejectedFile.errors.join(", ")}
</li>
))}
</ul>
) : null
}
</FileUploadContext>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemPreview type="image/*">
<FileUploadItemPreviewImage />
</FileUploadItemPreview>
<FileUploadItemPreview type="application/pdf">
<FileTextIcon className="text-rose-600" />
</FileUploadItemPreview>
<FileUploadItemPreview type=".*">
<FileIcon />
</FileUploadItemPreview>
<FileUploadItemName />
<FileUploadItemSizeText />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadAcceptedFilesDemo;
Dropzone
Drag and drop files hereor click to browse
import { FileIcon, UploadIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadItemPreview,
FileUploadItemPreviewImage,
FileUploadItemSizeText,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadDropzoneDemo = () => (
<FileUpload className="max-w-md" maxFiles={5}>
<FileUploadLabel>Dropzone</FileUploadLabel>
<FileUploadDropzone>
<UploadIcon className="size-6 text-muted-foreground" />
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">Drag and drop files here</span>
<span className="text-muted-foreground text-xs">
or click to browse
</span>
</div>
</FileUploadDropzone>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemPreview type="image/*">
<FileUploadItemPreviewImage />
</FileUploadItemPreview>
<FileUploadItemPreview type=".*">
<FileIcon />
</FileUploadItemPreview>
<FileUploadItemName />
<FileUploadItemSizeText />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadDropzoneDemo;
Clear Trigger
import { PaperclipIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadClearTrigger,
FileUploadContext,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadLabel,
FileUploadTrigger,
} from "@/components/ui/file-upload";
const FileUploadClearTriggerDemo = () => (
<FileUpload className="max-w-md" maxFiles={5}>
<FileUploadLabel>Clear trigger</FileUploadLabel>
<div className="flex flex-wrap gap-2">
<FileUploadTrigger className="inline-flex items-center gap-2 rounded-md border px-3 py-2 font-medium text-sm transition-colors hover:bg-muted">
<PaperclipIcon className="size-4" />
Choose file(s)
</FileUploadTrigger>
<FileUploadClearTrigger className="rounded-md px-3 py-2 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground">
Clear files
</FileUploadClearTrigger>
</div>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadClearTriggerDemo;
Directory Upload
import { FileIcon, FolderIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadItemSizeText,
FileUploadLabel,
FileUploadTrigger,
} from "@/components/ui/file-upload";
const FileUploadDirectoryUploadDemo = () => (
<FileUpload className="max-w-md" directory>
<FileUploadLabel>Directory upload</FileUploadLabel>
<FileUploadTrigger className="inline-flex items-center gap-2 rounded-md border px-3 py-2 font-medium text-sm transition-colors hover:bg-muted">
<FolderIcon className="size-4" />
Select folder
</FileUploadTrigger>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem
key={`${file.webkitRelativePath}-${file.size}`}
file={file}
>
<FileIcon className="size-4 shrink-0 text-muted-foreground" />
<FileUploadItemName>
{file.webkitRelativePath || file.name}
</FileUploadItemName>
<FileUploadItemSizeText />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadDirectoryUploadDemo;
Pasting Files
import { ClipboardIcon, XIcon } from "lucide-react";
import {
FileUploadContext,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadItemPreview,
FileUploadItemPreviewImage,
FileUploadItemSizeText,
FileUploadLabel,
FileUploadRootProvider,
useFileUpload,
} from "@/components/ui/file-upload";
const FileUploadPastingFilesDemo = () => {
const fileUpload = useFileUpload({ accept: "image/*", maxFiles: 3 });
return (
<FileUploadRootProvider className="max-w-md" value={fileUpload}>
<FileUploadLabel className="inline-flex items-center gap-2">
<ClipboardIcon className="size-4" />
Paste files
</FileUploadLabel>
<textarea
className="min-h-24 w-full rounded-md border bg-background px-3 py-2 text-sm"
onPaste={(event) => {
fileUpload.setClipboardFiles(event.clipboardData);
}}
placeholder="Paste an image here (Ctrl/Cmd + V)"
/>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemPreview type="image/*">
<FileUploadItemPreviewImage />
</FileUploadItemPreview>
<FileUploadItemName />
<FileUploadItemSizeText />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUploadRootProvider>
);
};
export default FileUploadPastingFilesDemo;
Rejected
Try a large file to see rejectedFiles
import { UploadIcon, XIcon } from "lucide-react";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadLabel,
} from "@/components/ui/file-upload";
const FileUploadRejectedDemo = () => (
<FileUpload className="max-w-md" maxFileSize={500} maxFiles={2}>
<FileUploadLabel>Rejections (< 500 bytes)</FileUploadLabel>
<FileUploadDropzone>
<UploadIcon className="size-6 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Try a large file to see rejectedFiles
</span>
</FileUploadDropzone>
<FileUploadContext>
{({ rejectedFiles }) =>
rejectedFiles.length > 0 ? (
<ul className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-destructive text-xs">
{rejectedFiles.map((rej) => (
<li key={`${rej.file.name}-${rej.file.size}`}>
{rej.file.name}: {rej.errors.join(", ")}
</li>
))}
</ul>
) : null
}
</FileUploadContext>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
);
export default FileUploadRejectedDemo;
Root Provider
0 accepted · dragging: no
import { XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
FileUploadLabel,
FileUploadRootProvider,
useFileUpload,
} from "@/components/ui/file-upload";
const FileUploadRootProviderDemo = () => {
const fileUpload = useFileUpload({ maxFiles: 3 });
return (
<FileUploadRootProvider className="max-w-md items-start" value={fileUpload}>
<FileUploadLabel>RootProvider + useFileUpload</FileUploadLabel>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
type="button"
variant="outline"
onClick={() => {
fileUpload.openFilePicker();
}}
>
Open picker
</Button>
<Button
size="sm"
type="button"
variant="secondary"
onClick={() => {
fileUpload.clearFiles();
}}
>
Clear files
</Button>
</div>
<p className="text-muted-foreground text-xs">
{fileUpload.acceptedFiles.length} accepted · dragging:{" "}
{fileUpload.dragging ? "yes" : "no"}
</p>
<FileUploadItemGroup>
{fileUpload.acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))}
</FileUploadItemGroup>
</FileUploadRootProvider>
);
};
export default FileUploadRootProviderDemo;
With Field
Max 2 files, 2KB each (invalid Field wrapper for demo)
import { FileTextIcon, XIcon } from "lucide-react";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
} from "@/components/ui/file-upload";
const FileUploadWithFieldDemo = () => (
<Field className="max-w-md" invalid>
<FieldLabel>Evidence upload</FieldLabel>
<FileUpload maxFiles={2} maxFileSize={2000}>
<FileUploadDropzone className="min-h-28">
<FileTextIcon className="size-6 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Max 2 files, 2KB each (invalid Field wrapper for demo)
</span>
</FileUploadDropzone>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem key={`${file.name}-${file.size}`} file={file}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
<FieldError>Attach at least one document to continue.</FieldError>
</Field>
);
export default FileUploadWithFieldDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
FileUploadItemDeleteTrigger
| Prop | Type | Description |
|---|---|---|
| variant? | ButtonVariant | Button style for the delete control (default ghost). |
| size? | ButtonSize | Button size for the delete control (default icon-xs). |
See the ARK UI documentation for the full API.