Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Add Comment
Add comment dialog with rich text input, @mentions, file attachments, and emoji support
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Add comments with rich formatting and mentions. This React add comment dialog provides textarea with @mention autocomplete, file attachment upload with preview, emoji picker integration, and comment preview before posting. Built with shadcn/ui Dialog, Textarea, Button, Avatar, Command, and Badge components using Tailwind CSS, users write detailed comments with context. Type message, mention teammates, attach files—perfect for project management, document collaboration, social features, or any Next.js application requiring threaded discussions with rich commenting capabilities.
"use client";import { useState, useRef, useCallback } from "react";import { MessageSquare, Paperclip, Smile, AtSign, X, Image as ImageIcon, File, Send, Loader2,} from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Textarea } from "@/components/ui/textarea";import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";import { Badge } from "@/components/ui/badge";import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList,} from "@/components/ui/command";import { Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover";import { cn } from "@/lib/utils";type User = { id: string; name: string; avatar?: string; initials: string;};type Attachment = { id: string; name: string; type: "image" | "file"; size: string; preview?: string;};const teamMembers: User[] = [ { id: "1", name: "Alice Johnson", initials: "AJ" }, { id: "2", name: "Bob Smith", initials: "BS" }, { id: "3", name: "Carol Williams", initials: "CW" }, { id: "4", name: "David Brown", initials: "DB" }, { id: "5", name: "Emma Davis", initials: "ED" },];const emojis = ["👍", "❤️", "🎉", "🚀", "💡", "✅", "👀", "🔥"];export const title = "React Dialog Block Add Comment";export default function DialogAddComment() { const [open, setOpen] = useState(false); const [comment, setComment] = useState(""); const [attachments, setAttachments] = useState<Attachment[]>([]); const [showMentions, setShowMentions] = useState(false); const [mentionSearch, setMentionSearch] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const fileInputRef = useRef<HTMLInputElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null); const handleCommentChange = (value: string) => { setComment(value); // Check for @ mention trigger const lastAtIndex = value.lastIndexOf("@"); if (lastAtIndex !== -1) { const textAfterAt = value.slice(lastAtIndex + 1); if (!textAfterAt.includes(" ")) { setMentionSearch(textAfterAt); setShowMentions(true); return; } } setShowMentions(false); }; const handleMentionSelect = (user: User) => { const lastAtIndex = comment.lastIndexOf("@"); const newComment = comment.slice(0, lastAtIndex) + `@${user.name} `; setComment(newComment); setShowMentions(false); textareaRef.current?.focus(); }; const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const files = e.target.files; if (!files) return; Array.from(files).forEach((file) => { const isImage = file.type.startsWith("image/"); const newAttachment: Attachment = { id: Math.random().toString(36).slice(2), name: file.name, type: isImage ? "image" : "file", size: `${(file.size / 1024).toFixed(1)} KB`, }; if (isImage) { const reader = new FileReader(); reader.onload = (e) => { newAttachment.preview = e.target?.result as string; setAttachments((prev) => [...prev, newAttachment]); }; reader.readAsDataURL(file); } else { setAttachments((prev) => [...prev, newAttachment]); } }); e.target.value = ""; }; const removeAttachment = (id: string) => { setAttachments((prev) => prev.filter((a) => a.id !== id)); }; const addEmoji = (emoji: string) => { setComment((prev) => prev + emoji); textareaRef.current?.focus(); }; const handleSubmit = async () => { if (!comment.trim() && attachments.length === 0) return; setIsSubmitting(true); await new Promise((resolve) => setTimeout(resolve, 1000)); setIsSubmitting(false); setOpen(false); setComment(""); setAttachments([]); }; const filteredMembers = teamMembers.filter((m) => m.name.toLowerCase().includes(mentionSearch.toLowerCase()) ); return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <MessageSquare className="mr-2 h-4 w-4" /> Add Comment </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <MessageSquare className="h-5 w-5" /> Add Comment </DialogTitle> <DialogDescription> Share your thoughts. Use @mentions to notify team members. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {/* Comment Input */} <div className="relative"> <Textarea ref={textareaRef} placeholder="Write a comment... Use @ to mention someone" value={comment} onChange={(e) => handleCommentChange(e.target.value)} rows={4} className="resize-none pr-10" /> {/* Mention Dropdown */} {showMentions && filteredMembers.length > 0 && ( <div className="absolute left-0 right-0 top-full mt-1 z-10"> <Command className="rounded-lg border shadow-md"> <CommandList> <CommandEmpty>No members found.</CommandEmpty> <CommandGroup heading="Team Members"> {filteredMembers.map((member) => ( <CommandItem key={member.id} onSelect={() => handleMentionSelect(member)} className="cursor-pointer" > <Avatar className="h-6 w-6 mr-2"> <AvatarImage src={member.avatar} /> <AvatarFallback className="text-xs"> {member.initials} </AvatarFallback> </Avatar> <span>{member.name}</span> </CommandItem> ))} </CommandGroup> </CommandList> </Command> </div> )} </div> {/* Attachments Preview */} {attachments.length > 0 && ( <div className="flex flex-wrap gap-2"> {attachments.map((attachment) => ( <div key={attachment.id} className="relative group rounded-lg border overflow-hidden" > {attachment.type === "image" && attachment.preview ? ( <img src={attachment.preview} alt={attachment.name} className="h-16 w-16 object-cover" /> ) : ( <div className="h-16 w-16 flex flex-col items-center justify-center p-2"> <File className="h-6 w-6 text-muted-foreground" /> <span className="text-[10px] text-muted-foreground truncate w-full text-center"> {attachment.name.slice(0, 8)}... </span> </div> )} <button onClick={() => removeAttachment(attachment.id)} className="absolute top-0.5 right-0.5 h-5 w-5 rounded-full bg-black/50 flex items-center justify-center text-white opacity-0 group-hover:opacity-100 transition-opacity" > <X className="h-3 w-3" /> </button> </div> ))} </div> )} {/* Action Bar */} <div className="flex items-center gap-2"> <input ref={fileInputRef} type="file" multiple onChange={handleFileUpload} className="hidden" accept="image/*,.pdf,.doc,.docx,.txt" /> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => fileInputRef.current?.click()} > <Paperclip className="h-4 w-4" /> </Button> <Popover> <PopoverTrigger asChild> <Button variant="ghost" size="icon" className="h-8 w-8"> <Smile className="h-4 w-4" /> </Button> </PopoverTrigger> <PopoverContent className="w-auto p-2" align="start"> <div className="flex gap-1"> {emojis.map((emoji) => ( <button key={emoji} onClick={() => addEmoji(emoji)} className="h-8 w-8 flex items-center justify-center rounded hover:bg-muted text-lg" > {emoji} </button> ))} </div> </PopoverContent> </Popover> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => { setComment((prev) => prev + "@"); textareaRef.current?.focus(); }} > <AtSign className="h-4 w-4" /> </Button> <span className="flex-1" /> <span className="text-xs text-muted-foreground"> {comment.length}/1000 </span> </div> </div> <DialogFooter> <Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting} > Cancel </Button> <Button onClick={handleSubmit} disabled={(!comment.trim() && attachments.length === 0) || isSubmitting} > {isSubmitting ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Posting... </> ) : ( <> <Send className="mr-2 h-4 w-4" /> Post Comment </> )} </Button> </DialogFooter> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-add-comment.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-add-comment.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-add-comment.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-add-comment.jsonRelated blocks you will also like
React Dialog Block Quick Reply
Quick responses
React Dialog Block Quick Note
Note taking
React Dialog Block Feedback Form
User feedback
React Dialog Block Report Content
Content reporting
React Dialog Block Contact Picker
Select mentions
React Dialog Block Media Upload
File uploads