# UploadThing Button

> A simple upload button with loading states and file validation. Self-contained component that works with any upload backend.

A shadcn/ui block from the [@elements](https://www.tryelements.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/elements/uploadthing-button).

- Page, with a live preview: https://shadcn.exchange/r/elements/uploadthing-button
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/elements/uploadthing-button-light-0c6d50ee2c8401e9.png
- Registry item: https://www.tryelements.dev/r/uploadthing-button.json
- Categories: uploadthing
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.tryelements.dev/r/uploadthing-button.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: `@uploadthing/react`, `uploadthing`

## Notes from the registry

Self-contained upload button with progress indication, file validation, and customizable accept types. Wire up to any upload backend via the onUpload prop.

## Files

### `registry/default/blocks/uploadthing/uploadthing-button/components/elements/uploadthing-button.tsx`

```tsx
"use client";

import { useCallback, useRef, useState } from "react";
import { cn } from "@/lib/utils";

interface UploadThingButtonProps {
  onUpload?: (files: File[]) => Promise<void>;
  onSelect?: (files: File[]) => void;
  accept?: string;
  multiple?: boolean;
  maxFiles?: number;
  maxSize?: number;
  disabled?: boolean;
  className?: string;
  children?: React.ReactNode;
}

function UploadIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
      <polyline points="17 8 12 3 7 8" />
      <line x1="12" x2="12" y1="3" y2="15" />
    </svg>
  );
}

function LoadingSpinner({ className }: { className?: string }) {
  return (
    <svg
      className={cn("animate-spin", className)}
      viewBox="0 0 24 24"
      fill="none"
    >
      <circle
        className="opacity-25"
        cx="12"
        cy="12"
        r="10"
        stroke="currentColor"
        strokeWidth="4"
      />
      <path
        className="opacity-75"
        fill="currentColor"
        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
      />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <polyline points="20 6 9 17 4 12" />
    </svg>
  );
}

export function UploadThingButton({
  onUpload,
  onSelect,
  accept = "image/*",
  multiple = false,
  maxFiles = 1,
  maxSize = 4 * 1024 * 1024,
  disabled = false,
  className,
  children,
}: UploadThingButtonProps) {
  const inputRef = useRef<HTMLInputElement>(null);
  const [isUploading, setIsUploading] = useState(false);
  const [uploadComplete, setUploadComplete] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleClick = () => {
    if (!disabled && !isUploading) {
      inputRef.current?.click();
    }
  };

  const handleFileChange = useCallback(
    async (e: React.ChangeEvent<HTMLInputElement>) => {
      const fileList = e.target.files;
      if (!fileList || fileList.length === 0) return;

      const files = Array.from(fileList).slice(0, maxFiles);
      setError(null);
      setUploadComplete(false);

      for (const file of files) {
        if (file.size > maxSize) {
          setError(`File too large. Max size: ${Math.round(maxSize / 1024 / 1024)}MB`);
          e.target.value = "";
          return;
        }
      }

      onSelect?.(files);

      if (onUpload) {
        try {
          setIsUploading(true);
          await onUpload(files);
          setUploadComplete(true);
          setTimeout(() => setUploadComplete(false), 2000);
        } catch (err) {
          setError(err instanceof Error ? err.message : "Upload failed");
        } finally {
          setIsUploading(false);
        }
      }

      e.target.value = "";
    },
    [maxFiles, maxSize, onSelect, onUpload]
  );

  return (
    <div data-slot="uploadthing-button" className={cn("inline-flex flex-col items-start gap-1", className)}>
      <button
        type="button"
        onClick={handleClick}
        disabled={disabled || isUploading}
        className={cn(
          "inline-flex items-center justify-center gap-2 px-4 py-2 rounded-md",
          "bg-primary text-primary-foreground font-medium text-sm",
          "transition-all duration-200",
          "hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
          "disabled:opacity-50 disabled:pointer-events-none",
          uploadComplete && "bg-green-600 hover:bg-green-600"
        )}
      >
        {isUploading ? (
          <>
            <LoadingSpinner className="w-4 h-4" />
            <span>Uploading...</span>
          </>
        ) : uploadComplete ? (
          <>
            <CheckIcon className="w-4 h-4" />
            <span>Uploaded!</span>
          </>
        ) : (
          <>
            <UploadIcon className="w-4 h-4" />
            <span>{children || "Choose File"}</span>
          </>
        )}
      </button>

      <input
        ref={inputRef}
        type="file"
        accept={accept}
        multiple={multiple}
        onChange={handleFileChange}
        disabled={disabled || isUploading}
        className="sr-only"
        aria-label="Upload file"
      />

      {error && (
        <p className="text-xs text-destructive">{error}</p>
      )}
    </div>
  );
}
```
