Composable, accessible parts for building any upload interface, dropzones, lists, items, progress and actions.
"use client"
import { useUploader } from "@uploadcn/react"
import { demoAdapter, useSampleFiles } from "@/examples/_demo"
import {
Upload,
UploadDropzone,
UploadDropzoneDescription,
UploadDropzoneHeader,
UploadDropzoneMedia,
UploadDropzoneTitle,
UploadItem,
UploadItemActions,
UploadItemContent,
UploadItemDescription,
UploadItemMedia,
UploadItemProgress,
UploadItemTitle,
UploadList,
} from "@/components/ui/upload"
export default function UploadDemo() {
const uploader = useUploader({ adapter: demoAdapter, maxFiles: 8 })
useSampleFiles(uploader)
return (
<Upload uploader={uploader}>
<UploadDropzone>
<UploadDropzoneHeader>
<UploadDropzoneMedia variant="icon" />
<UploadDropzoneTitle>Drop files here</UploadDropzoneTitle>
<UploadDropzoneDescription>
or click to browse · up to 8 files
</UploadDropzoneDescription>
</UploadDropzoneHeader>
</UploadDropzone>
<UploadList>
<UploadItem>
<UploadItemMedia />
<UploadItemContent>
<UploadItemTitle />
<UploadItemDescription />
</UploadItemContent>
<UploadItemActions />
<UploadItemProgress />
</UploadItem>
</UploadList>
</Upload>
)
}
Installation
npx shadcn@latest add @uploadcn/uploadUsage
import { s3Adapter } from "@uploadcn/core"
import {
Upload,
UploadDropzone,
UploadDropzoneDescription,
UploadDropzoneHeader,
UploadDropzoneMedia,
UploadDropzoneTitle,
UploadItem,
UploadItemActions,
UploadItemContent,
UploadItemDescription,
UploadItemMedia,
UploadItemProgress,
UploadItemTitle,
UploadList,
} from "@/components/ui/upload"<Upload adapter={s3Adapter({ endpoint: "/api/upload" })} maxFiles={8}>
<UploadDropzone>
<UploadDropzoneHeader>
<UploadDropzoneMedia variant="icon" />
<UploadDropzoneTitle>Drop files here</UploadDropzoneTitle>
<UploadDropzoneDescription>or click to browse</UploadDropzoneDescription>
</UploadDropzoneHeader>
</UploadDropzone>
<UploadList>
<UploadItem>
<UploadItemMedia />
<UploadItemContent>
<UploadItemTitle />
<UploadItemDescription />
</UploadItemContent>
<UploadItemActions />
<UploadItemProgress />
</UploadItem>
</UploadList>
</Upload>Composition
Upload holds the uploader. Everything else is a part you place where you want it.
Uploadholds the uploader, options or an `uploader` propUploadDropzonedrag, drop, click, Enter / SpaceUploadDropzoneHeaderUploadDropzoneMediaUploadDropzoneTitleUploadDropzoneDescription
UploadDropzoneContentbuttons, e.g. UploadTrigger
UploadTriggeropens the file pickerUploadOverlayturns any content into a drop targetUploadSlota named single-file slot, e.g. “ID front”UploadListrenders its children once per fileUploadItemUploadItemMediathumbnail or file-type iconUploadItemContentUploadItemTitlefile nameUploadItemDescriptionsize · progress · speed · error
UploadItemActionspause, resume, retry, cancel, removeUploadItemProgress
UploadHeader / UploadFooterUploadSummary“3 files · 12.4 MB · 45% · 8s left”
UploadProgressall files combinedUploadEmpty / UploadStart / UploadClear
UploadList renders its children as a template for every file and provides that file
to the parts inside. There's no render function or prop drilling. Each row re-renders
only when its own file changes.
Parts have sensible defaults and accept children to override them:
<UploadItemTitle /> {/* the file name */}
<UploadItemTitle>Invoice #1042</UploadItemTitle>
<UploadItemActions /> {/* the applicable actions */}
<UploadItemActions>
<UploadItemRetry />
<UploadItemRemove />
</UploadItemActions>Dropzone
Variant
Use variant to change the surface: default (dashed), muted or outline.
"use client"
import { FileTextIcon, ImageIcon } from "lucide-react"
import { demoAdapter } from "@/examples/_demo"
import {
Upload,
UploadDropzone,
UploadDropzoneDescription,
UploadDropzoneHeader,
UploadDropzoneMedia,
UploadDropzoneTitle,
} from "@/components/ui/upload"
export default function UploadDropzoneVariants() {
return (
<div className="grid w-full gap-4 sm:grid-cols-3">
<Upload adapter={demoAdapter}>
<UploadDropzone size="sm">
<UploadDropzoneHeader>
<UploadDropzoneMedia variant="icon" />
<UploadDropzoneTitle>Default</UploadDropzoneTitle>
<UploadDropzoneDescription>Dashed border</UploadDropzoneDescription>
</UploadDropzoneHeader>
</UploadDropzone>
</Upload>
<Upload adapter={demoAdapter}>
<UploadDropzone variant="muted" size="sm">
<UploadDropzoneHeader>
<UploadDropzoneMedia variant="icon">
<ImageIcon />
</UploadDropzoneMedia>
<UploadDropzoneTitle>Muted</UploadDropzoneTitle>
<UploadDropzoneDescription>
Filled surface
</UploadDropzoneDescription>
</UploadDropzoneHeader>
</UploadDropzone>
</Upload>
<Upload adapter={demoAdapter}>
<UploadDropzone variant="outline" size="sm">
<UploadDropzoneHeader>
<UploadDropzoneMedia>
<FileTextIcon />
</UploadDropzoneMedia>
<UploadDropzoneTitle>Outline</UploadDropzoneTitle>
<UploadDropzoneDescription>Card surface</UploadDropzoneDescription>
</UploadDropzoneHeader>
</UploadDropzone>
</Upload>
</div>
)
}
Orientation
orientation="horizontal" puts the content side by side, handy for compact forms.
Use clickable={false} when the dropzone contains its own UploadTrigger.
"use client"
import { PaperclipIcon } from "lucide-react"
import { demoAdapter } from "@/examples/_demo"
import {
Upload,
UploadDropzone,
UploadDropzoneContent,
UploadDropzoneDescription,
UploadDropzoneHeader,
UploadDropzoneMedia,
UploadDropzoneTitle,
UploadQueue,
UploadTrigger,
} from "@/components/ui/upload"
export default function UploadDropzoneHorizontal() {
return (
<Upload adapter={demoAdapter}>
<UploadDropzone orientation="horizontal" clickable={false}>
<UploadDropzoneHeader>
<UploadDropzoneMedia variant="icon">
<PaperclipIcon />
</UploadDropzoneMedia>
<div className="flex flex-col gap-0.5">
<UploadDropzoneTitle>Attachments</UploadDropzoneTitle>
<UploadDropzoneDescription>
Drag files here or browse
</UploadDropzoneDescription>
</div>
</UploadDropzoneHeader>
<UploadDropzoneContent>
<UploadTrigger>Browse files</UploadTrigger>
</UploadDropzoneContent>
</UploadDropzone>
<UploadQueue />
</Upload>
)
}
Size
size is sm, default or lg.
<UploadDropzone size="sm">…</UploadDropzone>Item
Variant
outline (default), muted, default (no surface), and tile for grids.
"use client"
import { useUploader } from "@uploadcn/react"
import { demoAdapter, useSampleFiles } from "@/examples/_demo"
import {
Upload,
UploadItem,
UploadItemActions,
UploadItemContent,
UploadItemDescription,
UploadItemMedia,
UploadItemProgress,
UploadItemTitle,
UploadList,
} from "@/components/ui/upload"
const VARIANTS = ["outline", "muted", "default"] as const
export default function UploadItemVariants() {
const uploader = useUploader({ adapter: demoAdapter })
useSampleFiles(uploader, "documents")
return (
<Upload uploader={uploader} className="gap-6">
{VARIANTS.map((variant) => (
<section key={variant} className="flex flex-col gap-2">
<h3 className="text-xs font-medium text-muted-foreground">
{variant}
</h3>
<UploadList>
<UploadItem variant={variant}>
<UploadItemMedia />
<UploadItemContent>
<UploadItemTitle />
<UploadItemDescription />
</UploadItemContent>
<UploadItemActions />
<UploadItemProgress />
</UploadItem>
</UploadList>
</section>
))}
</Upload>
)
}
Size
default, sm and xs. Media, text and actions scale with the item.
"use client"
import { useUploader } from "@uploadcn/react"
import { demoAdapter, useSampleFiles } from "@/examples/_demo"
import {
Upload,
UploadItem,
UploadItemActions,
UploadItemContent,
UploadItemDescription,
UploadItemMedia,
UploadItemProgress,
UploadItemTitle,
UploadList,
} from "@/components/ui/upload"
const SIZES = ["default", "sm", "xs"] as const
export default function UploadItemSizes() {
const uploader = useUploader({ adapter: demoAdapter })
useSampleFiles(uploader)
return (
<Upload uploader={uploader} className="gap-6">
{SIZES.map((size) => (
<section key={size} className="flex flex-col gap-2">
<h3 className="text-xs font-medium text-muted-foreground">{size}</h3>
<UploadList>
<UploadItem size={size}>
<UploadItemMedia />
<UploadItemContent>
<UploadItemTitle />
<UploadItemDescription />
</UploadItemContent>
<UploadItemActions />
<UploadItemProgress />
</UploadItem>
</UploadList>
</section>
))}
</Upload>
)
}
Tiles
Combine UploadList variant="grid" with UploadItem variant="tile" and
UploadItemMedia variant="cover".
"use client"
import { useUploader } from "@uploadcn/react"
import { demoAdapter, useSampleFiles } from "@/examples/_demo"
import {
Upload,
UploadDropzone,
UploadDropzoneHeader,
UploadDropzoneMedia,
UploadDropzoneTitle,
UploadItem,
UploadItemActions,
UploadItemContent,
UploadItemMedia,
UploadItemProgress,
UploadItemStatus,
UploadList,
} from "@/components/ui/upload"
export default function UploadGrid() {
const uploader = useUploader({ adapter: demoAdapter, accept: "image/*" })
useSampleFiles(uploader, "images")
return (
<Upload uploader={uploader}>
<UploadList variant="grid">
<UploadItem variant="tile">
<UploadItemMedia variant="cover" />
<UploadItemActions />
<UploadItemContent>
<UploadItemStatus />
<UploadItemProgress />
</UploadItemContent>
</UploadItem>
</UploadList>
<UploadDropzone size="sm">
<UploadDropzoneHeader>
<UploadDropzoneMedia />
<UploadDropzoneTitle>Add more photos</UploadDropzoneTitle>
</UploadDropzoneHeader>
</UploadDropzone>
</Upload>
)
}
Chips
UploadList variant="inline" with extra-small items makes attachment chips.
"use client"
import { useUploader } from "@uploadcn/react"
import { PlusIcon } from "lucide-react"
import { demoAdapter, useSampleFiles } from "@/examples/_demo"
import {
Upload,
UploadItem,
UploadItemActions,
UploadItemContent,
UploadItemMedia,
UploadItemProgress,
UploadItemRemove,
UploadItemTitle,
UploadList,
UploadTrigger,
} from "@/components/ui/upload"
export default function UploadChips() {
const uploader = useUploader({ adapter: demoAdapter })
useSampleFiles(uploader)
return (
<Upload uploader={uploader}>
<UploadList variant="inline">
<UploadItem
variant="muted"
size="xs"
className="w-44 rounded-full pe-1"
>
<UploadItemMedia className="rounded-full" />
<UploadItemContent>
<UploadItemTitle />
</UploadItemContent>
<UploadItemActions>
<UploadItemRemove className="rounded-full" />
</UploadItemActions>
<UploadItemProgress className="absolute inset-x-3 bottom-0 h-0.5 w-auto" />
</UploadItem>
</UploadList>
<UploadTrigger variant="ghost" className="self-start rounded-full">
<PlusIcon data-icon="inline-start" />
Attach
</UploadTrigger>
</Upload>
)
}
Trigger
UploadTrigger renders an outline Button. Pass render to use any element.
"use client"
import { UploadIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { demoAdapter } from "@/examples/_demo"
import {
Upload,
UploadQueue,
UploadTrigger,
} from "@/components/ui/upload"
export default function UploadTriggerExample() {
return (
<Upload adapter={demoAdapter}>
<div className="flex flex-wrap items-center gap-2">
<UploadTrigger render={<Button />}>
<UploadIcon data-icon="inline-start" />
Upload
</UploadTrigger>
<UploadTrigger variant="outline" size="default">
Choose files
</UploadTrigger>
<UploadTrigger render={<Button variant="link" />}>
or browse your computer
</UploadTrigger>
</div>
<UploadQueue variant="muted" size="sm" />
</Upload>
)
}
Overlay
UploadOverlay makes any content a drop target without making it a button. A frosted
layer with a label covers the content only while files are dragged over it, so text
stays selectable and links stay clickable.
"use client"
import { FileTextIcon } from "lucide-react"
import { demoAdapter } from "@/examples/_demo"
import {
Upload,
UploadOverlay,
UploadQueue,
} from "@/components/ui/upload"
export default function UploadOverlayExample() {
return (
<Upload adapter={demoAdapter} className="max-w-lg">
<UploadOverlay
className="rounded-xl border bg-card p-5"
label="Drop to attach"
description="Files are added to this note"
>
<article className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-sm font-medium">
<FileTextIcon className="size-4 text-muted-foreground" />
Launch checklist
</div>
<p className="text-sm text-muted-foreground">
Drag files from your desktop onto this card. The content stays
selectable and clickable, the overlay only appears while you drag.
</p>
</article>
</UploadOverlay>
<UploadQueue size="sm" variant="muted" />
</Upload>
)
}
<UploadOverlay
label="Drop to attach"
description="Files are added to this note"
>
<Note />
</UploadOverlay>Slots
UploadSlot is a named, single-file slot inside a shared <Upload>, for forms
that need specific documents. Each slot has its own picker and drop target, replaces
its own file, and tags it with meta.slot so your backend knows which is which.
Read a slot's file with useUploadSlot(name).
"use client"
import { HomeIcon, IdCardIcon } from "lucide-react"
import { Upload, UploadSlot } from "@/components/ui/upload"
export default function UploadSlotsExample() {
return (
<Upload className="max-w-md">
<UploadSlot
name="id"
label="Photo ID"
description="Passport or driver's license"
accept="image/*,application/pdf"
icon={<IdCardIcon />}
required
/>
<UploadSlot
name="address"
label="Proof of address"
description="Utility bill or bank statement"
accept="application/pdf,image/*"
icon={<HomeIcon />}
/>
</Upload>
)
}
<Upload>
<UploadSlot
name="id"
label="Photo ID"
accept="image/*,application/pdf"
required
/>
<UploadSlot name="address" label="Proof of address" />
</Upload>Header, footer and summary
UploadHeader and UploadFooter lay out titles and queue actions. UploadSummary
describes the queue in one line and renders nothing while it's empty.
"use client"
import { useUploader } from "@uploadcn/react"
import { demoAdapter, useSampleFiles } from "@/examples/_demo"
import {
Upload,
UploadClear,
UploadDropzone,
UploadDropzoneDescription,
UploadDropzoneTitle,
UploadFooter,
UploadHeader,
UploadProgress,
UploadQueue,
UploadSummary,
UploadTrigger,
} from "@/components/ui/upload"
export default function UploadCardExample() {
const uploader = useUploader({ adapter: demoAdapter, maxFiles: 10 })
useSampleFiles(uploader)
return (
<Upload
uploader={uploader}
className="max-w-lg rounded-xl border bg-card p-4 shadow-xs"
>
<UploadHeader>
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium">Attachments</h3>
<UploadSummary />
</div>
<UploadTrigger size="sm" variant="outline">
Browse
</UploadTrigger>
</UploadHeader>
<UploadProgress />
<UploadDropzone size="sm" variant="muted">
<UploadDropzoneTitle>Drop more files</UploadDropzoneTitle>
<UploadDropzoneDescription>Up to 10 files</UploadDropzoneDescription>
</UploadDropzone>
<UploadQueue size="sm" />
<UploadFooter>
<UploadClear>Clear finished</UploadClear>
</UploadFooter>
</Upload>
)
}
Accessibility
UploadDropzoneis a button: it's focusable and opens the picker with Enter or Space, the alternative to dragging.UploadItemProgressis aprogressbarlabelled with the file name.- A polite live region announces added, finished, failed and rejected files.
- Every action has a name such as “Retry upload of report.pdf”.
- Motion respects
prefers-reduced-motion.
API Reference
Upload
The root. Accepts every uploader option,
adapter, accept, maxSize, maxFiles, concurrency, retry, transform,
process, persistence…, or an uploader from useUploader.
Prop
Type
UploadDropzone
Prop
Type
Data attributes: data-dragging, data-drag-reject, data-disabled.
UploadDropzoneMedia
Prop
Type
UploadList
Prop
Type
UploadItem
Prop
Type
Data attributes: data-status (uploading, success, error…), data-variant, data-size.
UploadItemMedia
Prop
Type
UploadItemProgress
Prop
Type
UploadOverlay
Prop
Type
UploadSlot
Prop
Type
UploadSummary
Pass children to replace the text. Without children it renders nothing for an empty queue.
Actions
UploadItemPause, UploadItemResume, UploadItemRetry, UploadItemCancel and
UploadItemRemove render only when they apply, pause only for resumable adapters.
They accept Button props (variant, size) and forceMount.
Headless
The same parts without styles live in @uploadcn/react, see the
custom UI example.