🎨

UI Knihovna

Kolekce nádherných, znovupoužitelných React komponent a CSS fragmentů vytvořená pomocí Tailwind CSS a Framer Motion.

Místní nabídky

Kontextová menu pro zobrazení seznamů akcí nebo odkazů.

Basic Action Menu

DropdownBasic.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; export function BasicDropdown() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); return ( <div className="relative inline-block text-left" ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)} className="flex items-center gap-2 px-4 py-2 bg-popover border border-border rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none" > <span className="font-medium text-sm">Options</span> <svg className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> <AnimatePresence> {isOpen && ( <motion.div initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -10, scale: 0.95 }} transition={{ duration: 0.15 }} className="absolute right-0 mt-2 w-48 origin-top-right bg-popover border border-border shadow-2xl rounded-xl divide-y divide-border overflow-hidden z-20" > <div className="py-1"> <a href="#" className="block px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Edit Item</a> <a href="#" className="block px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Duplicate</a> <a href="#" className="block px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Archive</a> </div> <div className="py-1"> <a href="#" className="block px-4 py-2.5 text-sm font-medium text-red-500 hover:bg-red-500/10 transition-colors">Delete Permanently</a> </div> </motion.div> )} </AnimatePresence> </div> ); }

Grouped Options

DropdownGrouped.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; export function GroupedDropdown() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); // ... same useEffect for click outside ... return ( <div className="relative inline-block text-left" ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)} className="flex items-center gap-2 px-4 py-2 bg-popover border border-border rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none" > <span className="font-medium text-sm">Create New</span> <svg className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> <AnimatePresence> {isOpen && ( <motion.div initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -10, scale: 0.95 }} transition={{ duration: 0.15 }} className="absolute right-0 mt-2 w-56 origin-top-right bg-popover border border-border shadow-2xl rounded-xl divide-y divide-border overflow-hidden z-20" > <div className="py-2"> <span className="block px-4 py-1 text-xs font-semibold text-muted uppercase tracking-wider">Document</span> <a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Blank Document</a> <a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">From Template</a> </div> <div className="py-2"> <span className="block px-4 py-1 text-xs font-semibold text-muted uppercase tracking-wider">Project</span> <a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">New Workspace</a> <a href="#" className="block px-4 py-2 text-sm text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors">Import Repository</a> </div> </motion.div> )} </AnimatePresence> </div> ); }

Multi-Select Menus

DropdownMultiSelect.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; export function MultiSelectDropdown() { const [isOpen, setIsOpen] = useState(false); const [selectedItems, setSelectedItems] = useState<string[]>(['Option 1']); const dropdownRef = useRef<HTMLDivElement>(null); // ... same useEffect for click outside ... const toggleSelection = (option: string) => { if (selectedItems.includes(option)) { setSelectedItems(selectedItems.filter(item => item !== option)); } else { setSelectedItems([...selectedItems, option]); } }; const options = ['Option 1', 'Option 2', 'Option 3', 'Option 4']; return ( <div className="relative inline-block text-left" ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)} className="flex items-center justify-between min-w-[200px] gap-2 px-4 py-2.5 bg-popover border border-border rounded-xl hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none" > <span className="font-medium text-sm text-muted"> {selectedItems.length ? `${selectedItems.length} Selected` : 'Select items...'} </span> <svg className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> <AnimatePresence> {isOpen && ( <motion.div initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -10, scale: 0.95 }} transition={{ duration: 0.15 }} className="absolute left-0 mt-2 w-full min-w-[200px] origin-top-left bg-popover border border-border shadow-2xl rounded-xl overflow-hidden z-20 py-1" > {options.map((option) => ( <label key={option} className="flex items-center gap-3 px-4 py-2.5 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 transition-colors"> <div className={`flex items-center justify-center w-4 h-4 rounded border transition-colors ${selectedItems.includes(option) ? 'bg-accent border-accent text-white' : 'border-border bg-transparent'}`}> {selectedItems.includes(option) && ( <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /> </svg> )} </div> <span className="text-sm font-medium text-foreground">{option}</span> </label> ))} </motion.div> )} </AnimatePresence> </div> ); }

User Profile Menu

DropdownProfile.tsxLanguage: tsx
import { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; export function UserProfileDropdown() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); // ... same useEffect for click outside ... return ( <div className="relative inline-block text-left" ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)} className="flex items-center gap-3 px-2 py-2 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors focus:ring-2 focus:ring-accent focus:outline-none" > <div className="w-9 h-9 rounded-full bg-gradient-to-tr from-accent to-purple-500 flex items-center justify-center text-white font-bold text-sm"> DW </div> <svg className={`w-4 h-4 text-muted transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> <AnimatePresence> {isOpen && ( <motion.div initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -10, scale: 0.95 }} transition={{ duration: 0.15 }} className="absolute right-0 mt-2 w-64 origin-top-right bg-popover border border-border shadow-2xl rounded-xl divide-y divide-border overflow-hidden z-20" > <div className="px-4 py-3 flex items-center gap-3"> <div className="w-10 h-10 rounded-full bg-gradient-to-tr from-accent to-purple-500 flex items-center justify-center text-white font-bold text-sm shrink-0"> DW </div> <div className="overflow-hidden"> <p className="text-sm font-semibold text-foreground truncate">Dowy Designer</p> <p className="text-xs text-muted truncate">[email protected]</p> </div> </div> <div className="py-1"> <a href="#" className="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors"> <svg className="w-4 h-4 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg> View Profile </a> <a href="#" className="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors"> <svg className="w-4 h-4 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37... (truncated visually)" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg> Settings </a> </div> <div className="py-1"> <a href="#" className="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-red-500 hover:bg-red-500/10 transition-colors"> <svg className="w-4 h-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg> Sign out </a> </div> </motion.div> )} </AnimatePresence> </div> ); }