# Avatar Photo Uploader

> Avatar uploader with a circular image preview, change and remove buttons, and a format hint, previewing the picked file via an object URL.

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

- Page, with a live preview: https://shadcn.exchange/r/7ovr/file-upload-5
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/file-upload-5-light-8d8673e8654c3638.png
- Registry item: https://7ovr.com/r/file-upload-5.json
- Categories: file-upload
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/file-upload-5.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: `@base-ui/react`
- Registry items: `button`

## Files

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

```tsx
"use client"

import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

export default function FileUploadBlock() {
  const [preview, setPreview] = useState<string | null>(
    "https://i.pravatar.cc/160?img=45"
  )
  const inputRef = useRef<HTMLInputElement>(null)
  const objectUrlRef = useRef<string | null>(null)

  function setObjectUrl(url: string | null) {
    if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current)
    objectUrlRef.current = url
    setPreview(url)
  }

  function handleFile(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0]
    if (file) setObjectUrl(URL.createObjectURL(file))
  }

  return (
    <section className="flex w-full items-center justify-center bg-background px-6 py-16 text-foreground">
      <div className="w-full max-w-sm rounded-xl border border-border bg-card p-6">
        <h2 className="font-heading text-sm font-semibold tracking-tight">
          Profile photo
        </h2>
        <p className="mt-1 text-xs text-muted-foreground">
          This will be shown on your profile and in comments.
        </p>

        <div className="mt-6 flex items-center gap-5">
          <span className="flex size-20 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-muted">
            {preview ? (
              <img
                src={preview}
                alt="Avatar preview"
                className="size-full object-cover grayscale"
              />
            ) : (
              <IconPlaceholder
                lucide="User"
                tabler="IconUser"
                hugeicons="UserIcon"
                phosphor="User"
                remixicon="RiUser3Line"
                className="size-8 text-muted-foreground"
                aria-hidden="true"
              />
            )}
          </span>

          <div className="flex flex-col gap-2">
            <div className="flex gap-2">
              <Button
                variant="outline"
                size="sm"
                onClick={() => inputRef.current?.click()}
              >
                <IconPlaceholder
                  lucide="Upload"
                  tabler="IconUpload"
                  hugeicons="UploadIcon"
                  phosphor="Upload"
                  remixicon="RiUploadLine"
                  data-icon="inline-start"
                  aria-hidden="true"
                />
                Change
              </Button>
              {preview && (
                <Button
                  variant="destructive"
                  size="sm"
                  onClick={() => setObjectUrl(null)}
                  aria-label="Remove photo"
                >
                  <IconPlaceholder
                    lucide="Trash"
                    tabler="IconTrash"
                    hugeicons="Delete02Icon"
                    phosphor="Trash"
                    remixicon="RiDeleteBinLine"
                    className="size-4"
                    aria-hidden="true"
                  />
                </Button>
              )}
            </div>
            <p className="text-xs text-muted-foreground">
              JPG, PNG or GIF. Max 2&nbsp;MB.
            </p>
          </div>

          <input
            ref={inputRef}
            type="file"
            accept="image/*"
            className="hidden"
            onChange={handleFile}
          />
        </div>
      </div>
    </section>
  )
}
```
