# File upload

> An asset-upload card with a drag-and-drop dropzone and a progress-tracked upload queue.

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

- Page, with a live preview: https://shadcn.exchange/r/quill/file-upload
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/quill/file-upload-light-d9b4eb9e9e8f4bb4.png
- Registry item: https://www.quilldesignsystem.com/r/file-upload.json
- Categories: form
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.quilldesignsystem.com/r/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

- Registry items: `card`, `button`, `progress`, `separator`, `https://www.quilldesignsystem.com/r/icon.json`

## Notes from the registry

An asset-upload card with a drag-and-drop dropzone and a progress-tracked upload queue. Do: Make the dropzone a real, focusable <button> that also opens a file picker on click/Enter — never make drag-and-drop the only way in. Don't: Ship a dropzone that only responds to a pointer drag gesture. Do: Give each in-progress file its own progress bar, so users can see exactly how far each individual upload has gotten. Don't: Show one shared progress bar for the whole queue — users can't tell which file it belongs to or how far any single file has gotten. Use contact-form when you need a single attachment inside a broader form, not a dedicated upload queue as the whole card.

## Files

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

```tsx
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Icon } from '@/components/ui/icon'
import { Progress } from '@/components/ui/progress'
import { Separator } from '@/components/ui/separator'

const files = [
  { name: 'brand-lockups.zip', size: '4.2 MB', progress: 100, state: 'done' },
  { name: 'declaration.webp', size: '189 KB', progress: 100, state: 'done' },
  { name: 'field-notes-issue-001.pdf', size: '2.8 MB', progress: 64, state: 'uploading' },
] as const

/** An asset-upload card with a drag-and-drop dropzone and a progress-tracked upload queue. */
export function FileUpload() {
  return (
    <Card className="w-[440px]">
      <CardHeader>
        <CardTitle>Upload assets</CardTitle>
        <CardDescription>SVG, PNG, PDF or ZIP — up to 25 MB each.</CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        <button
          type="button"
          className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-input bg-transparent px-6 py-10 text-center transition-colors hover:bg-muted focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
        >
          <Icon name="cloud_upload" size={28} className="text-muted-foreground" aria-hidden />
          <span className="text-sm font-medium text-foreground">
            Drag files here or click to browse
          </span>
          <span className="text-xs text-muted-foreground">Your files stay private.</span>
        </button>
        <Separator />
        <ul className="flex flex-col gap-3" aria-label="Upload queue">
          {files.map((f) => (
            <li key={f.name} className="flex items-center gap-3">
              <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
                <Icon name={f.state === 'done' ? 'check' : 'draft'} size={18} aria-hidden />
              </span>
              <div className="flex min-w-0 flex-1 flex-col gap-1">
                <div className="flex items-baseline justify-between gap-2">
                  <span className="truncate text-sm font-medium text-foreground">{f.name}</span>
                  <span className="shrink-0 text-xs text-muted-foreground">{f.size}</span>
                </div>
                {f.state === 'uploading' && (
                  <Progress value={f.progress} aria-label={`Uploading ${f.name}`} />
                )}
              </div>
              <Button variant="ghost" size="icon-sm" aria-label={`Remove ${f.name}`}>
                <Icon name="close" size={16} />
              </Button>
            </li>
          ))}
        </ul>
      </CardContent>
      <CardFooter className="justify-end gap-2">
        <Button variant="ghost">Cancel</Button>
        <Button>Done</Button>
      </CardFooter>
    </Card>
  )
}
```
