# Logo Upload

> A settings card for a workspace logo with light and dark previews.

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

- Page, with a live preview: https://shadcn.exchange/r/uploadcn/logo-upload
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uploadcn/logo-upload-light-f3fa93049694d63a.png
- Registry item: https://uploadcn.dev/r/new-york-v4/logo-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/logo-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`, `cn`
- Registry items: `@uploadcn/upload`, `card`

## Files

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

```tsx
"use client"

import * as React from "react"
import {
  type UploadAdapter,
  type Uploader,
  type UploadItem as UploadItemData,
} from "@uploadcn/core"
import {
  UploadItemContext,
  useUploadContext,
  useUploadSelector,
  type UploadRootProps,
} from "@uploadcn/react"
import { cn } from "cn"

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Upload,
  UploadDropzone,
  UploadItemMedia,
  UploadItemProgress,
  UploadItemRemove,
  UploadItemStatus,
  UploadTrigger,
} from "@/registry/default/ui/upload"

interface LogoUploadProps<TResult> {
  adapter?: UploadAdapter<TResult>
  /** Use an uploader you created with `useUploader` instead of options. */
  uploader?: Uploader<TResult>
  /** Shown in the placeholder. Default `"Acme"`. */
  name?: string
  title?: string
  description?: string
  /** Default 2 MB. */
  maxSize?: number
  onSuccess?: (item: UploadItemData<TResult>) => void
  className?: string
}

/**
 * A settings card for a workspace or company logo, with a live preview
 * on light and dark backgrounds.
 */
function LogoUpload<TResult = unknown>({
  adapter,
  uploader,
  name = "Acme",
  title = "Logo",
  description = "Shown in the sidebar, emails and invoices.",
  maxSize = 2 * 1000 * 1000,
  onSuccess,
  className,
}: LogoUploadProps<TResult>) {
  return (
    <Upload
      {...((uploader
        ? { uploader }
        : {
            adapter,
            accept: "image/png,image/svg+xml,image/webp,image/jpeg",
            maxFiles: 1,
            maxSize,
            onSuccess,
          }) as UploadRootProps<TResult>)}
      multiple={false}
      className={className}
    >
      <Card>
        <CardHeader>
          <CardTitle>{title}</CardTitle>
          <CardDescription>{description}</CardDescription>
        </CardHeader>
        <LogoBody name={name} />
        <CardFooter className="border-t text-xs text-muted-foreground">
          Square SVG or PNG, at least 256 × 256 px. Max {maxSize / 1000 / 1000}{" "}
          MB.
        </CardFooter>
      </Card>
    </Upload>
  )
}

function Placeholder({
  name,
  className,
}: {
  name: string
  className?: string
}) {
  return (
    <span
      className={cn(
        "flex size-full items-center justify-center text-lg font-semibold",
        className
      )}
    >
      {name.slice(0, 2).toUpperCase()}
    </span>
  )
}

function LogoBody({ name }: { name: string }) {
  const { uploader } = useUploadContext()
  const item = useUploadSelector(uploader, (state) =>
    state.items.find((entry) => entry.status !== "rejected")
  )
  const rejected = useUploadSelector(uploader, (state) =>
    state.items.find((entry) => entry.status === "rejected")
  )

  const preview = (tone: "light" | "dark") => (
    <div
      className={cn(
        "flex size-16 items-center justify-center overflow-hidden rounded-xl border p-2.5",
        tone === "light"
          ? "bg-card text-card-foreground"
          : "border-transparent bg-foreground text-background"
      )}
    >
      {item ? (
        <UploadItemMedia className="size-full border-0 bg-transparent [&_img]:object-contain" />
      ) : (
        <Placeholder name={name} />
      )}
    </div>
  )

  return (
    <UploadItemContext.Provider value={item ?? null}>
      <CardContent className="flex flex-col gap-4 sm:flex-row sm:items-center">
        <UploadDropzone
          variant="muted"
          size="sm"
          aria-label="Logo preview. Drop an image or click to choose one"
          className="w-auto shrink-0 flex-row gap-2 p-2"
        >
          {preview("light")}
          {preview("dark")}
        </UploadDropzone>
        <div className="flex min-w-0 flex-1 flex-col gap-2">
          {item ? (
            <>
              <div className="flex min-w-0 items-center justify-between gap-2">
                <span className="truncate text-sm font-medium">
                  {item.name}
                </span>
                <UploadItemStatus />
              </div>
              <UploadItemProgress />
            </>
          ) : (
            <p
              className={cn(
                "text-sm text-muted-foreground",
                rejected && "text-destructive"
              )}
            >
              {rejected?.issues[0]?.message ??
                "Drop a file on the preview, or choose one."}
            </p>
          )}
          <div className="flex gap-2">
            <UploadTrigger variant="outline" size="sm">
              {item ? "Replace" : "Upload logo"}
            </UploadTrigger>
            {item ? (
              <UploadItemRemove variant="ghost" size="sm">
                Remove
              </UploadItemRemove>
            ) : null}
          </div>
        </div>
      </CardContent>
    </UploadItemContext.Provider>
  )
}

export { LogoUpload, type LogoUploadProps }
```
