Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Media Upload
Media upload dialog with drag-drop zone, file type filtering, image preview grid, upload progress, and bulk management
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Upload media with confidence. This React media upload dialog provides a complete file upload experience with drag-and-drop zone, file type validation, thumbnail preview grid, individual upload progress bars, and bulk removal options. Built with shadcn/ui Dialog, Button, Progress, Badge, and ScrollArea components using Tailwind CSS, users upload multiple files with visual feedback and easy management. Drag-drop upload, preview thumbnails, progress tracking—perfect for content management systems, social media apps, portfolio builders, or any Next.js application where media upload UX directly impacts user engagement and content quality.
"use client";import { useState, useCallback } from "react";import { Upload, Image, X, Check, AlertCircle, File, Trash2,} from "lucide-react";import { Button } from "@/components/ui/button";import { Badge } from "@/components/ui/badge";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Progress } from "@/components/ui/progress";import { ScrollArea } from "@/components/ui/scroll-area";import { cn } from "@/lib/utils";type FileStatus = "pending" | "uploading" | "complete" | "error";type UploadFile = { id: string; name: string; size: number; type: string; preview?: string; progress: number; status: FileStatus; error?: string;};const ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MBexport const title = "React Dialog Block Media Upload";export default function DialogMediaUpload() { const [open, setOpen] = useState(false); const [files, setFiles] = useState<UploadFile[]>([]); const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; const validateFile = (file: File): string | null => { if (!ACCEPTED_TYPES.includes(file.type)) { return "File type not supported"; } if (file.size > MAX_FILE_SIZE) { return "File too large (max 10MB)"; } return null; }; const addFiles = useCallback((newFiles: FileList | File[]) => { const fileArray = Array.from(newFiles); const uploadFiles: UploadFile[] = fileArray.map((file) => { const error = validateFile(file); return { id: Math.random().toString(36).substr(2, 9), name: file.name, size: file.size, type: file.type, preview: file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined, progress: 0, status: error ? "error" : "pending", error: error || undefined, }; }); setFiles((prev) => [...prev, ...uploadFiles]); }, []); const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files) { addFiles(e.dataTransfer.files); } }; const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files) { addFiles(e.target.files); } }; const removeFile = (fileId: string) => { setFiles((prev) => { const file = prev.find((f) => f.id === fileId); if (file?.preview) { URL.revokeObjectURL(file.preview); } return prev.filter((f) => f.id !== fileId); }); }; const clearAll = () => { files.forEach((file) => { if (file.preview) { URL.revokeObjectURL(file.preview); } }); setFiles([]); }; const simulateUpload = async () => { setIsUploading(true); const pendingFiles = files.filter((f) => f.status === "pending"); for (const file of pendingFiles) { setFiles((prev) => prev.map((f) => f.id === file.id ? { ...f, status: "uploading" as FileStatus } : f ) ); // Simulate progress for (let progress = 0; progress <= 100; progress += 10) { await new Promise((resolve) => setTimeout(resolve, 100)); setFiles((prev) => prev.map((f) => (f.id === file.id ? { ...f, progress } : f)) ); } setFiles((prev) => prev.map((f) => f.id === file.id ? { ...f, status: "complete" as FileStatus } : f ) ); } setIsUploading(false); }; const handleClose = () => { setOpen(false); setTimeout(() => { clearAll(); setIsUploading(false); }, 200); }; const pendingCount = files.filter((f) => f.status === "pending").length; const completeCount = files.filter((f) => f.status === "complete").length; const errorCount = files.filter((f) => f.status === "error").length; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Upload className="mr-2 h-4 w-4" /> Upload Media </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <Image className="h-5 w-5" /> Upload Media </DialogTitle> <DialogDescription> Drag and drop images or click to browse. Max 10MB per file. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> <div onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} className={cn( "border-2 border-dashed rounded-lg p-6 text-center transition-colors cursor-pointer", isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-muted-foreground/50" )} onClick={() => document.getElementById("file-input")?.click()} > <Upload className={cn( "h-8 w-8 mx-auto mb-2", isDragging ? "text-primary" : "text-muted-foreground" )} /> <p className="text-sm font-medium"> {isDragging ? "Drop files here" : "Drag files here or click to browse"} </p> <p className="text-xs text-muted-foreground mt-1"> JPEG, PNG, WebP, GIF up to 10MB </p> <input id="file-input" type="file" accept={ACCEPTED_TYPES.join(",")} multiple onChange={handleFileInput} className="hidden" /> </div> {files.length > 0 && ( <> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <span className="text-sm font-medium"> {files.length} file{files.length !== 1 ? "s" : ""} </span> {completeCount > 0 && ( <Badge variant="secondary" className="text-xs"> {completeCount} uploaded </Badge> )} {errorCount > 0 && ( <Badge variant="destructive" className="text-xs"> {errorCount} failed </Badge> )} </div> <Button variant="ghost" size="sm" className="text-xs text-destructive hover:text-destructive" onClick={clearAll} > <Trash2 className="h-3 w-3 mr-1" /> Clear all </Button> </div> <ScrollArea className="h-[200px] -mx-6 px-6"> <div className="space-y-2"> {files.map((file) => ( <div key={file.id} className={cn( "flex items-center gap-3 p-2 rounded-lg border", file.status === "error" && "border-destructive/50 bg-destructive/5", file.status === "complete" && "border-primary/50 bg-primary/5" )} > {file.preview ? ( <div className="h-12 w-12 rounded-md overflow-hidden bg-muted shrink-0"> <img src={file.preview} alt={file.name} className="h-full w-full object-cover" /> </div> ) : ( <div className="h-12 w-12 rounded-md bg-muted flex items-center justify-center shrink-0"> <File className="h-6 w-6 text-muted-foreground" /> </div> )} <div className="flex-1 min-w-0"> <p className="text-sm font-medium truncate">{file.name}</p> <p className="text-xs text-muted-foreground"> {formatFileSize(file.size)} </p> {file.status === "uploading" && ( <Progress value={file.progress} className="h-1 mt-1" /> )} {file.status === "error" && ( <p className="text-xs text-destructive mt-0.5">{file.error}</p> )} </div> <div className="shrink-0"> {file.status === "complete" ? ( <Check className="h-5 w-5 text-primary" /> ) : file.status === "error" ? ( <AlertCircle className="h-5 w-5 text-destructive" /> ) : file.status === "uploading" ? ( <span className="text-xs text-muted-foreground"> {file.progress}% </span> ) : ( <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => removeFile(file.id)} > <X className="h-4 w-4" /> </Button> )} </div> </div> ))} </div> </ScrollArea> </> )} </div> <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" onClick={handleClose}> {completeCount === files.length && files.length > 0 ? "Done" : "Cancel"} </Button> {pendingCount > 0 && ( <Button onClick={simulateUpload} disabled={isUploading}> {isUploading ? "Uploading..." : `Upload ${pendingCount} file${pendingCount !== 1 ? "s" : ""}`} </Button> )} </DialogFooter> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-media-upload.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-media-upload.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-media-upload.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-media-upload.jsonRelated blocks you will also like
React Dialog Block Data Import
File upload wizard
React Dialog Block Add Writer Profile
Avatar upload
React Dialog Block File Conflict
File management
React Dialog Block Export Data
File handling
React Dialog Block Success Confirmation
Upload confirmation
React Dialog Block Destructive Warning
Delete confirmation