Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Tag Manager
Tag manager dialog with tag list, create new tags, edit existing, delete with confirmation, and color selection
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Organize content with customizable tags efficiently. This React tag manager dialog provides a searchable list of tags with usage counts, inline creation with color picker, inline rename editing, and delete with confirmation. Built with shadcn/ui Dialog, AlertDialog, Input, Button, and ScrollArea components using Tailwind CSS, users manage their tagging taxonomy easily. Create tags, assign colors, organize content—perfect for note-taking apps, content management systems, project managers, or any Next.js application where users categorize and filter items with custom tags.
"use client";import { useState, useMemo } from "react";import { Tags, Plus, Search, Pencil, Trash2, Check, X } from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,} from "@/components/ui/alert-dialog";import { Input } from "@/components/ui/input";import { ScrollArea } from "@/components/ui/scroll-area";import { cn } from "@/lib/utils";type Tag = { id: string; name: string; color: string; count: number;};const tagColors = [ { name: "Red", value: "bg-red-500" }, { name: "Orange", value: "bg-orange-500" }, { name: "Yellow", value: "bg-yellow-500" }, { name: "Green", value: "bg-green-500" }, { name: "Blue", value: "bg-blue-500" }, { name: "Purple", value: "bg-purple-500" }, { name: "Pink", value: "bg-pink-500" }, { name: "Gray", value: "bg-gray-500" },];const initialTags: Tag[] = [ { id: "1", name: "Important", color: "bg-red-500", count: 12 }, { id: "2", name: "Work", color: "bg-blue-500", count: 28 }, { id: "3", name: "Personal", color: "bg-green-500", count: 15 }, { id: "4", name: "Ideas", color: "bg-yellow-500", count: 8 }, { id: "5", name: "Research", color: "bg-purple-500", count: 23 }, { id: "6", name: "Archive", color: "bg-gray-500", count: 45 },];export const title = "React Dialog Block Tag Manager";export default function DialogTagManager() { const [open, setOpen] = useState(false); const [tags, setTags] = useState<Tag[]>(initialTags); const [searchQuery, setSearchQuery] = useState(""); const [isCreating, setIsCreating] = useState(false); const [newTagName, setNewTagName] = useState(""); const [newTagColor, setNewTagColor] = useState(tagColors[0].value); const [editingId, setEditingId] = useState<string | null>(null); const [editingName, setEditingName] = useState(""); const [deleteTag, setDeleteTag] = useState<Tag | null>(null); const filteredTags = useMemo(() => { if (!searchQuery) return tags; return tags.filter((tag) => tag.name.toLowerCase().includes(searchQuery.toLowerCase()) ); }, [tags, searchQuery]); const handleCreateTag = () => { if (!newTagName.trim()) return; const newTag: Tag = { id: Date.now().toString(), name: newTagName.trim(), color: newTagColor, count: 0, }; setTags((prev) => [newTag, ...prev]); setNewTagName(""); setNewTagColor(tagColors[0].value); setIsCreating(false); }; const handleStartEdit = (tag: Tag) => { setEditingId(tag.id); setEditingName(tag.name); }; const handleSaveEdit = () => { if (!editingName.trim() || !editingId) return; setTags((prev) => prev.map((tag) => tag.id === editingId ? { ...tag, name: editingName.trim() } : tag ) ); setEditingId(null); setEditingName(""); }; const handleCancelEdit = () => { setEditingId(null); setEditingName(""); }; const handleDeleteConfirm = () => { if (!deleteTag) return; setTags((prev) => prev.filter((tag) => tag.id !== deleteTag.id)); setDeleteTag(null); }; return ( <> <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Tags className="mr-2 h-4 w-4" /> Manage Tags </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-sm"> <DialogHeader> <DialogTitle>Manage Tags</DialogTitle> <DialogDescription> Create, edit, and organize your tags. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {/* Search */} <div className="relative"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input placeholder="Search tags..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-9" /> </div> {/* Create New Tag */} {isCreating ? ( <div className="space-y-3 rounded-lg border p-3"> <div className="flex gap-2"> <Input placeholder="Tag name" value={newTagName} onChange={(e) => setNewTagName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreateTag()} autoFocus /> <Button size="icon" variant="ghost" onClick={() => setIsCreating(false)} > <X className="h-4 w-4" /> </Button> </div> <div className="flex items-center justify-between"> <div className="flex gap-1.5"> {tagColors.map((color) => ( <button key={color.value} onClick={() => setNewTagColor(color.value)} className={cn( "h-5 w-5 rounded-full transition-transform", color.value, newTagColor === color.value && "ring-2 ring-offset-2 ring-primary" )} title={color.name} /> ))} </div> <Button size="sm" onClick={handleCreateTag} disabled={!newTagName.trim()} > Create </Button> </div> </div> ) : ( <Button variant="outline" size="sm" className="w-full" onClick={() => setIsCreating(true)} > <Plus className="mr-2 h-4 w-4" /> New Tag </Button> )} {/* Tags List */} <ScrollArea className="h-[240px]"> {filteredTags.length === 0 ? ( <div className="flex flex-col items-center justify-center py-8 text-center"> <Tags className="h-8 w-8 text-muted-foreground mb-2" /> <p className="text-sm text-muted-foreground"> {searchQuery ? "No tags found" : "No tags yet"} </p> </div> ) : ( <div className="space-y-1"> {filteredTags.map((tag) => ( <div key={tag.id} className="flex items-center gap-2 rounded-lg p-2 hover:bg-muted/50 group" > {editingId === tag.id ? ( <div className="flex-1 flex items-center gap-2"> <div className={cn("h-3 w-3 rounded-full shrink-0", tag.color)} /> <Input value={editingName} onChange={(e) => setEditingName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSaveEdit(); if (e.key === "Escape") handleCancelEdit(); }} className="h-7 text-sm" autoFocus /> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleSaveEdit}> <Check className="h-3 w-3" /> </Button> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCancelEdit}> <X className="h-3 w-3" /> </Button> </div> ) : ( <> <div className={cn("h-3 w-3 rounded-full shrink-0", tag.color)} /> <span className="flex-1 text-sm">{tag.name}</span> <span className="text-xs text-muted-foreground">{tag.count}</span> <Button variant="ghost" size="icon" className="h-7 w-7 opacity-0 group-hover:opacity-100" onClick={() => handleStartEdit(tag)} > <Pencil className="h-3 w-3" /> </Button> <Button variant="ghost" size="icon" className="h-7 w-7 opacity-0 group-hover:opacity-100 text-destructive hover:text-destructive" onClick={() => setDeleteTag(tag)} > <Trash2 className="h-3 w-3" /> </Button> </> )} </div> ))} </div> )} </ScrollArea> </div> <DialogFooter> <Button onClick={() => setOpen(false)}>Done</Button> </DialogFooter> </DialogContent> </Dialog> {/* Delete Confirmation */} <AlertDialog open={!!deleteTag} onOpenChange={(open) => !open && setDeleteTag(null)}> <AlertDialogContent className="sm:max-w-sm"> <AlertDialogHeader> <AlertDialogTitle>Delete "{deleteTag?.name}"?</AlertDialogTitle> <AlertDialogDescription> This will remove the tag from {deleteTag?.count} items. This cannot be undone. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-white hover:bg-destructive/90" > Delete </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> </> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-tag-manager.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-tag-manager.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-tag-manager.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-tag-manager.jsonRelated blocks you will also like
React Dialog Block Add To Collection
Organize content
React Dialog Block Filter Sort
Filter by tags
React Dialog Block Color Picker
Color selection
React Dialog Block Bookmark Folder
Organization
React Dialog Block Confirm Delete
Delete confirmation
React Dialog Block Edit Record
Edit items