# Gallery Upload

> A photo gallery uploader that resizes images to WebP before upload.

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

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

## Install

```bash
npx shadcn@latest add https://uploadcn.dev/r/new-york-v4/gallery-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/gallery-upload.tsx`

```tsx
"use client"

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

import { IconPlaceholder } from "@/components/icon-placeholder"
import {
  Upload,
  UploadDropzone,
  UploadDropzoneDescription,
  UploadDropzoneHeader,
  UploadDropzoneMedia,
  UploadDropzoneTitle,
  UploadEmpty,
  UploadItem,
  UploadItemActions,
  UploadItemContent,
  UploadItemMedia,
  UploadItemProgress,
  UploadItemTitle,
  UploadList,
  UploadProgress,
  UploadTrigger,
} from "@/registry/default/ui/upload"

const optimize = imageTransform({
  maxWidth: 2560,
  maxHeight: 2560,
  type: "image/webp",
})

interface GalleryUploadProps<TResult> {
  adapter?: UploadAdapter<TResult>
  /** Use an uploader you created with `useUploader` instead of options. */
  uploader?: Uploader<TResult>
  maxFiles?: number
  /** Downscale and convert photos to WebP before uploading. Default `true`. */
  optimize?: boolean
  onComplete?: (items: UploadItemData<TResult>[]) => void
  className?: string
}

/** A photo gallery: the whole card is a drop target, photos upload four at a time. */
function GalleryUpload<TResult = unknown>({
  adapter,
  uploader,
  maxFiles = 50,
  optimize: shouldOptimize = true,
  onComplete,
  className,
}: GalleryUploadProps<TResult>) {
  return (
    <Upload
      {...((uploader
        ? { uploader }
        : {
            adapter,
            accept: "image/*",
            maxFiles,
            concurrency: 4,
            transform: shouldOptimize ? optimize : undefined,
            onComplete,
          }) as UploadRootProps<TResult>)}
      className={className}
    >
      <UploadDropzone
        variant="outline"
        clickable={false}
        size="sm"
        className="items-stretch"
      >
        <GalleryHeader />
        <UploadEmpty>
          <UploadDropzoneHeader className="mx-auto py-8">
            <UploadDropzoneMedia variant="icon">
              <IconPlaceholder
                lucide="ImagesIcon"
                tabler="IconLibraryPhoto"
                hugeicons="Album01Icon"
                phosphor="ImagesIcon"
                remixicon="RiGalleryLine"
              />
            </UploadDropzoneMedia>
            <UploadDropzoneTitle>Drag photos anywhere here</UploadDropzoneTitle>
            <UploadDropzoneDescription>
              Resized and converted to WebP before upload
            </UploadDropzoneDescription>
          </UploadDropzoneHeader>
        </UploadEmpty>
        <UploadList className="grid-cols-2 sm:grid-cols-3" variant="grid">
          <UploadItem variant="tile" className="aspect-4/3">
            <UploadItemMedia variant="cover" />
            <UploadItemActions />
            <UploadItemContent>
              <UploadItemTitle />
              <GallerySavings />
              <UploadItemProgress />
            </UploadItemContent>
          </UploadItem>
        </UploadList>
      </UploadDropzone>
    </Upload>
  )
}

function GalleryHeader() {
  const { uploader } = useUploadContext()
  const summary = useUploadProgress(uploader)
  return (
    <div className="flex flex-col gap-3">
      <div className="flex items-center justify-between gap-3 text-left">
        <div className="flex flex-col gap-0.5">
          <UploadDropzoneTitle>Gallery</UploadDropzoneTitle>
          <UploadDropzoneDescription>
            {summary.total === 0
              ? `Up to 50 photos`
              : `${summary.counts.success} of ${summary.total} photos uploaded`}
          </UploadDropzoneDescription>
        </div>
        <UploadTrigger>
          <IconPlaceholder
            lucide="ImagesIcon"
            tabler="IconLibraryPhoto"
            hugeicons="Album01Icon"
            phosphor="ImagesIcon"
            remixicon="RiGalleryLine"
            data-icon="inline-start"
          />
          Add photos
        </UploadTrigger>
      </div>
      {summary.isUploading ? <UploadProgress /> : null}
    </div>
  )
}

function GallerySavings() {
  const item = useUploadItem()
  if (item.file === item.originalFile) return null
  const saved = 1 - item.file.size / item.originalFile.size
  if (saved <= 0) return null
  return (
    <span className="text-xs text-muted-foreground tabular-nums">
      {Math.round(saved * 100)}% smaller
    </span>
  )
}

export { GalleryUpload, type GalleryUploadProps }
```
