๐จ
UI Library
A collection of beautiful, reusable React components and CSS snippets built with Tailwind CSS and Framer Motion.
Dropdown Menus
Contextual menus for displaying lists of actions or links.
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>
);
}