# File Upload

> A dropzone with a full upload queue, progress summary and batch actions.

A shadcn/ui block from the [@uploadcn](https://uploadcn.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uploadcn/file-upload).

- Page, with a live preview: https://shadcn.exchange/r/uploadcn/file-upload
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uploadcn/file-upload-light-db209483fc7c3bdc.png
- Registry item: https://uploadcn.dev/r/new-york-v4/file-upload.json
- Categories: upload
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://uploadcn.dev/r/new-york-v4/file-upload.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@uploadcn/core`, `@uploadcn/react`
- Registry items: `@uploadcn/upload`

## Files

### `registry/default/blocks/file-upload.tsx`

```tsx
"use client"

import * as React from "react"
import {
  formatBytes,
  type UploadAdapter,
  type Uploader,
  type UploadItem as UploadItemData,
  type ValidationOptions,
} from "@uploadcn/core"
import {
  useUploadContext,
  useUploadProgress,
  type UploadRootProps,
} from "@uploadcn/react"

import {
  Upload,
  UploadClear,
  UploadDropzone,
  UploadDropzoneDescription,
  UploadDropzoneHeader,
  UploadDropzoneMedia,
  UploadDropzoneTitle,
  UploadItem,
  UploadItemActions,
  UploadItemContent,
  UploadItemDescription,
  UploadItemMedia,
  UploadItemProgress,
  UploadItemTitle,
  UploadList,
  UploadProgress,
  UploadStart,
} from "@/registry/default/ui/upload"

interface FileUploadProps<TResult> extends Pick<
  ValidationOptions,
  "accept" | "maxSize" | "maxFiles" | "image" | "media"
> {
  /** Where files go. Defaults to the `/api/upload` route (S3/R2). */
  adapter?: UploadAdapter<TResult>
  /** Use an uploader you created with `useUploader` instead of options. */
  uploader?: Uploader<TResult>
  /** When `false`, files wait for the Upload button. Default `true`. */
  autoUpload?: boolean
  multiple?: boolean
  disabled?: boolean
  title?: React.ReactNode
  description?: React.ReactNode
  onSuccess?: (item: UploadItemData<TResult>) => void
  onComplete?: (items: UploadItemData<TResult>[]) => void
  className?: string
}

function describeLimits({
  accept,
  maxSize,
  maxFiles,
}: Pick<ValidationOptions, "accept" | "maxSize" | "maxFiles">) {
  const parts: string[] = []
  if (accept) {
    const tokens = typeof accept === "string" ? accept.split(",") : accept
    parts.push(
      tokens
        .map((token) => token.trim())
        .map((token) =>
          token.endsWith("/*")
            ? `${token.slice(0, -2)}s`
            : token.startsWith(".")
              ? token.slice(1).toUpperCase()
              : (token.split("/")[1]?.toUpperCase() ?? token)
        )
        .join(", ")
    )
  }
  if (maxSize) parts.push(`up to ${formatBytes(maxSize)}`)
  if (maxFiles) parts.push(`max ${maxFiles} file${maxFiles === 1 ? "" : "s"}`)
  return parts.join(" · ")
}

/** A dropzone, a queue with per-file progress and a summary footer. */
function FileUpload<TResult = unknown>({
  adapter,
  uploader,
  title = "Drop files here or click to browse",
  description,
  className,
  ...options
}: FileUploadProps<TResult>) {
  return (
    <Upload
      {...((uploader
        ? { uploader }
        : {
            adapter,
            ...options,
          }) as UploadRootProps<TResult>)}
      className={className}
    >
      <UploadDropzone>
        <UploadDropzoneHeader>
          <UploadDropzoneMedia variant="icon" />
          <UploadDropzoneTitle>{title}</UploadDropzoneTitle>
          <UploadDropzoneDescription>
            {description ?? (describeLimits(options) || "Any file type")}
          </UploadDropzoneDescription>
        </UploadDropzoneHeader>
      </UploadDropzone>
      <UploadList>
        <UploadItem>
          <UploadItemMedia />
          <UploadItemContent>
            <UploadItemTitle />
            <UploadItemDescription />
          </UploadItemContent>
          <UploadItemActions />
          <UploadItemProgress />
        </UploadItem>
      </UploadList>
      <FileUploadSummary />
    </Upload>
  )
}

function FileUploadSummary() {
  const { uploader } = useUploadContext()
  const summary = useUploadProgress(uploader)
  if (summary.total < 2 && summary.counts.idle === 0) return null
  return (
    <div className="flex items-center gap-3 pt-1 text-xs text-muted-foreground tabular-nums">
      <div className="flex min-w-0 flex-1 flex-col gap-1.5">
        <div className="flex justify-between gap-2">
          <span>
            {summary.counts.success} of {summary.total} uploaded
          </span>
          <span>
            {formatBytes(summary.loaded)} / {formatBytes(summary.size)}
          </span>
        </div>
        <UploadProgress />
      </div>
      <UploadClear>Clear</UploadClear>
      <UploadStart>Upload</UploadStart>
    </div>
  )
}

export { FileUpload, type FileUploadProps }
```
