Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Bookmark Folder
Bookmark save dialog with folder selection, new folder creation, tag input, and notes field for organizing saved items
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Organize saved content effortlessly. This React bookmark dialog provides a complete save-to-folder interface with hierarchical folder selection, inline new folder creation, tag chips with autocomplete, and optional notes field. Built with shadcn/ui Dialog, Input, Button, Badge, RadioGroup, and ScrollArea components using Tailwind CSS, users save and categorize content with visual folder hierarchy and flexible tagging. Folder tree navigation, tag suggestions, quick save option—perfect for bookmarking apps, content curation platforms, reading list managers, or any Next.js application where organized content saving improves user productivity and retrieval.
"use client";import { useState } from "react";import { Bookmark, Folder, FolderPlus, ChevronRight, ChevronDown, X, Plus, Check,} 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 { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { ScrollArea } from "@/components/ui/scroll-area";import { Textarea } from "@/components/ui/textarea";import { cn } from "@/lib/utils";type FolderItem = { id: string; name: string; children?: FolderItem[];};const folders: FolderItem[] = [ { id: "1", name: "Reading List", children: [ { id: "1-1", name: "Articles" }, { id: "1-2", name: "Research Papers" }, ], }, { id: "2", name: "Work", children: [ { id: "2-1", name: "Projects" }, { id: "2-2", name: "References" }, { id: "2-3", name: "Tools" }, ], }, { id: "3", name: "Personal", children: [ { id: "3-1", name: "Recipes" }, { id: "3-2", name: "Travel" }, ], }, { id: "4", name: "Quick Save" },];const suggestedTags = [ "important", "read-later", "reference", "tutorial", "inspiration", "work", "personal",];const FolderTree = ({ items, level = 0, selectedId, expandedIds, onSelect, onToggle,}: { items: FolderItem[]; level?: number; selectedId: string; expandedIds: Set<string>; onSelect: (id: string) => void; onToggle: (id: string) => void;}) => { return ( <div className="space-y-0.5"> {items.map((folder) => { const hasChildren = folder.children && folder.children.length > 0; const isExpanded = expandedIds.has(folder.id); const isSelected = selectedId === folder.id; return ( <div key={folder.id}> <button onClick={() => { onSelect(folder.id); if (hasChildren) onToggle(folder.id); }} className={cn( "w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm transition-colors", isSelected ? "bg-primary text-primary-foreground" : "hover:bg-muted" )} style={{ paddingLeft: `${level * 16 + 8}px` }} > {hasChildren ? ( isExpanded ? ( <ChevronDown className="h-4 w-4 flex-shrink-0" /> ) : ( <ChevronRight className="h-4 w-4 flex-shrink-0" /> ) ) : ( <span className="w-4" /> )} <Folder className={cn( "h-4 w-4 flex-shrink-0", isSelected ? "text-primary-foreground" : "text-muted-foreground" )} /> <span className="truncate">{folder.name}</span> </button> {hasChildren && isExpanded && ( <FolderTree items={folder.children!} level={level + 1} selectedId={selectedId} expandedIds={expandedIds} onSelect={onSelect} onToggle={onToggle} /> )} </div> ); })} </div> );};export const title = "React Dialog Block Bookmark Folder";export default function DialogBookmarkFolder() { const [open, setOpen] = useState(false); const [saved, setSaved] = useState(false); const [selectedFolder, setSelectedFolder] = useState("4"); const [expandedFolders, setExpandedFolders] = useState<Set<string>>( new Set(["1", "2"]) ); const [tags, setTags] = useState<string[]>([]); const [tagInput, setTagInput] = useState(""); const [notes, setNotes] = useState(""); const [showNewFolder, setShowNewFolder] = useState(false); const [newFolderName, setNewFolderName] = useState(""); const handleToggleFolder = (id: string) => { setExpandedFolders((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const handleAddTag = (tag: string) => { const trimmed = tag.trim().toLowerCase(); if (trimmed && !tags.includes(trimmed)) { setTags([...tags, trimmed]); } setTagInput(""); }; const handleRemoveTag = (tag: string) => { setTags(tags.filter((t) => t !== tag)); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); handleAddTag(tagInput); } else if (e.key === "Backspace" && tagInput === "" && tags.length > 0) { setTags(tags.slice(0, -1)); } }; const handleSave = () => { console.log({ selectedFolder, tags, notes }); setSaved(true); }; const handleClose = () => { setOpen(false); setTimeout(() => { setSaved(false); setSelectedFolder("4"); setTags([]); setTagInput(""); setNotes(""); setShowNewFolder(false); setNewFolderName(""); }, 200); }; const getFolderName = (id: string): string => { const findFolder = (items: FolderItem[]): string | null => { for (const folder of items) { if (folder.id === id) return folder.name; if (folder.children) { const found = findFolder(folder.children); if (found) return found; } } return null; }; return findFolder(folders) || "Unknown"; }; const filteredSuggestions = suggestedTags.filter( (tag) => !tags.includes(tag) && tag.toLowerCase().includes(tagInput.toLowerCase()) ); return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Bookmark className="mr-2 h-4 w-4" /> Save Bookmark </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-md"> {saved ? ( <> <DialogHeader className="text-center"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 mb-4"> <Check className="h-6 w-6 text-primary" /> </div> <DialogTitle>Bookmark Saved</DialogTitle> <DialogDescription> Saved to "{getFolderName(selectedFolder)}" {tags.length > 0 && ` with ${tags.length} tag${tags.length > 1 ? "s" : ""}`}. </DialogDescription> </DialogHeader> <DialogFooter> <Button onClick={handleClose} className="w-full"> Done </Button> </DialogFooter> </> ) : ( <> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <Bookmark className="h-5 w-5" /> Save Bookmark </DialogTitle> <DialogDescription> Choose a folder and add tags to organize this bookmark. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> <div className="space-y-2"> <div className="flex items-center justify-between"> <Label>Folder</Label> <Button variant="ghost" size="sm" className="h-7 px-2 text-xs" onClick={() => setShowNewFolder(!showNewFolder)} > <FolderPlus className="mr-1 h-3 w-3" /> New Folder </Button> </div> {showNewFolder && ( <div className="flex gap-2"> <Input placeholder="Folder name" value={newFolderName} onChange={(e) => setNewFolderName(e.target.value)} className="h-8 text-sm" /> <Button size="sm" className="h-8" disabled={!newFolderName.trim()} onClick={() => { console.log("Creating folder:", newFolderName); setShowNewFolder(false); setNewFolderName(""); }} > Create </Button> </div> )} <ScrollArea className="h-[140px] rounded-md border"> <div className="p-2"> <FolderTree items={folders} selectedId={selectedFolder} expandedIds={expandedFolders} onSelect={setSelectedFolder} onToggle={handleToggleFolder} /> </div> </ScrollArea> </div> <div className="space-y-2"> <Label htmlFor="tags">Tags</Label> <div className="flex flex-wrap gap-1.5 p-2 rounded-md border min-h-[42px]"> {tags.map((tag) => ( <Badge key={tag} variant="secondary" className="gap-1 pr-1" > {tag} <button onClick={() => handleRemoveTag(tag)} className="ml-0.5 rounded-full hover:bg-muted-foreground/20 p-0.5" > <X className="h-3 w-3" /> </button> </Badge> ))} <Input id="tags" value={tagInput} onChange={(e) => setTagInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={tags.length === 0 ? "Add tags..." : ""} className="flex-1 min-w-[80px] border-0 h-6 p-0 text-sm focus-visible:ring-0 shadow-none" /> </div> {tagInput && filteredSuggestions.length > 0 && ( <div className="flex flex-wrap gap-1"> {filteredSuggestions.slice(0, 5).map((tag) => ( <button key={tag} onClick={() => handleAddTag(tag)} className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground" > <Plus className="h-3 w-3" /> {tag} </button> ))} </div> )} </div> <div className="space-y-2"> <Label htmlFor="notes">Notes (optional)</Label> <Textarea id="notes" placeholder="Add a note about this bookmark..." value={notes} onChange={(e) => setNotes(e.target.value)} className="h-20 resize-none" /> </div> </div> <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" onClick={handleClose}> Cancel </Button> <Button onClick={handleSave}> Save Bookmark </Button> </DialogFooter> </> )} </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-bookmark-folder.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-bookmark-folder.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-bookmark-folder.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-bookmark-folder.jsonRelated blocks you will also like
React Dialog Block Create Workspace
Create with folder structure
React Dialog Block File Conflict
File organization dialog
React Dialog Block Export Data
Save with options dialog
React Dialog Block Report Content
Categorized selection dialog
React Dialog Block Success Confirmation
Save confirmation feedback
React Dialog Block Keyboard Shortcuts
Organized list interface
Questions you might have
React Dialog Block Billing Address Form
Billing address form dialog with country selection, auto-formatted inputs, address validation, and save for future use option
React Dialog Block Breathing Exercise
Breathing exercise dialog with guided animations, multiple patterns, visual circle expansion, and session completion tracking