October 9 2025, letter
I've witnessed Notion and other advanced editors, and each one of them basically gives me a way to change the file/folder icons in the sidebar menu
I was wondering how I can make that very quickly without any images and store it in the databases.
The very first thought was to bring some SVG images from Figma or an online resource, upload each on firebase storage or AWS or supabase storage, then use a nodejs or hono app API to serve images using some name stored in the database as well
But this entire process is time-consuming and resource-intensive
Also, we already have existing icon libraries for React, mainly React-Icons and lucide-react, and one good way to render an icon selector dropdown is to use these already existing icon libraries.
npm i lucide-react react-iconsChoice is yours, and below is the code on how I am selecting importing icons from lucide-react and rendering those icons using an array object inside fixed fixed-width and height modal
import React, { useState, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Folder,
FolderOpen,
FileText,
File,
FileImage,
FileCode,
FileVideo,
FileAudio,
FileSpreadsheet,
FileArchive,
FileCheck,
FileX,
FilePlus,
FileMinus,
FileEdit,
FileSearch,
FileHeart,
FileClock,
FileWarning,
FileQuestion,
FileSliders,
FileBarChart,
FilePieChart,
FileLineChart,
Database,
Activity,
Server,
Cloud,
Upload,
Share2,
Unlock,
Shield,
Settings,
Cog,
Wrench,
Hammer,
PenTool,
Zap,
CloudLightningIcon,
Star,
Bookmark,
Tag,
Text,
Award,
Crown,
Gem,
Diamond,
Circle,
Square,
Triangle,
Hexagon,
Octagon,
EllipsisVertical,
RectangleEllipsis,
Package,
Container,
ShoppingBasket,
Briefcase,
Badge,
Key,
Lock,
Box,
LucideFolderOpen,
FolderPlus,
FolderEdit,
FolderHeart,
FolderClock,
FileWarningIcon,
FolderMinus,
FolderSearch,
FileQuestionIcon,
Info,
Settings2,
CogIcon,
WrenchIcon,
CloudLightning,
} from "lucide-react";
const IconSelector = ({
isOpen,
onClose,
onSelectIcon,
colors,
position = { x: 0, y: 0 },
}) => {
const [searchQuery, setSearchQuery] = useState("");
const icons = [
{ name: "Folder", component: Folder },
{ name: "FolderOpen", component: FolderOpen },
{ name: "LucideFolderOpen", component: LucideFolderOpen },
{ name: "FolderPlus", component: FolderPlus },
{ name: "FolderMinus", component: FolderMinus },
{ name: "FolderEdit", component: FolderEdit },
{ name: "FolderSearch", component: FolderSearch },
{ name: "FolderHeart", component: FolderHeart },
{ name: "FolderClock", component: FolderClock },
{ name: "FileWarning", component: FileWarningIcon },
{ name: "FileQuestion", component: FileQuestionIcon },
{ name: "Info", component: Info },
{ name: "Settings2", component: Settings2 },
{ name: "CogIcon", component: CogIcon },
{ name: "WrenchIcon", component: WrenchIcon },
{ name: "CloudLightning", component: CloudLightning },
{ name: "FileText", component: FileText },
{ name: "File", component: File },
{ name: "FileImage", component: FileImage },
{ name: "FileCode", component: FileCode },
{ name: "FileVideo", component: FileVideo },
{ name: "FileAudio", component: FileAudio },
{ name: "FileSpreadsheet", component: FileSpreadsheet },
{ name: "FileArchive", component: FileArchive },
{ name: "FileCheck", component: FileCheck },
{ name: "FileX", component: FileX },
{ name: "FilePlus", component: FilePlus },
{ name: "FileMinus", component: FileMinus },
{ name: "FileEdit", component: FileEdit },
{ name: "FileSearch", component: FileSearch },
{ name: "FileHeart", component: FileHeart },
{ name: "FileClock", component: FileClock },
{ name: "FileWarning", component: FileWarning },
{ name: "FileQuestion", component: FileQuestion },
{ name: "FileSliders", component: FileSliders },
{ name: "FileBarChart", component: FileBarChart },
{ name: "FilePieChart", component: FilePieChart },
{ name: "FileLineChart", component: FileLineChart },
{ name: "Activity", component: Activity },
{ name: "Database", component: Database },
{ name: "Server", component: Server },
{ name: "Cloud", component: Cloud },
{ name: "Download", component: Upload },
{ name: "Share", component: Share2 },
{ name: "Lock", component: Lock },
{ name: "Unlock", component: Unlock },
{ name: "Shield", component: Shield },
{ name: "Key", component: Key },
{ name: "Settings", component: Settings },
{ name: "Cog", component: Cog },
{ name: "Wrench", component: Wrench },
{ name: "Hammer", component: Hammer },
{ name: "Tool", component: PenTool },
{ name: "Zap", component: Zap },
{ name: "Lightning", component: CloudLightningIcon },
{ name: "Star", component: Star },
{ name: "Bookmark", component: Bookmark },
{ name: "Tag", component: Tag },
{ name: "Label", component: Text },
{ name: "Badge", component: Badge },
{ name: "Award", component: Award },
{ name: "Trophy", component: Crown },
{ name: "Gem", component: Gem },
{ name: "Diamond", component: Diamond },
{ name: "Circle", component: Circle },
{ name: "Square", component: Square },
{ name: "Triangle", component: Triangle },
{ name: "Hexagon", component: Hexagon },
{ name: "Octagon", component: Octagon },
{ name: "Ellipse", component: EllipsisVertical },
{ name: "Rectangle", component: RectangleEllipsis },
{ name: "Box", component: Box },
{ name: "Package", component: Package },
{ name: "Container", component: Container },
{ name: "Basket", component: ShoppingBasket },
{ name: "Briefcase", component: Briefcase },
];
const handleIconClick = (iconName, IconComponent) => {
onSelectIcon(iconName, IconComponent);
onClose();
};
const handleClose = () => {
setSearchQuery("");
onClose();
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-zinc-50/20 bg-opacity-20 z-40"
onClick={handleClose}
/>
{/* Positioned Modal */}
<motion.div
initial={{ scale: 0.8, opacity: 0, y: -10 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0, y: -10 }}
className={`fixed z-50 ${colors.card} rounded-xl shadow-xl border ${colors.border} overflow-hidden`}
style={{
left: `${position.x}px`,
top: `${position.y}px`,
width: "400px",
height: "320px",
maxWidth: "90vw",
maxHeight: "80vh",
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-zinc-200">
<h3 className={`text-lg font-semibold ${colors.text}`}>
Select Icon
</h3>
<button
onClick={handleClose}
className={`p-1 ${colors.buttonSecondary} rounded-xl transition-colors hover:${colors.hover}`}
>
<svg
className={`w-4 h-4 ${colors.textMuted}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Icons Grid */}
<div
className="p-4 overflow-y-auto"
style={{ height: "calc(100% - 140px)" }}
>
<div className="flex flex-wrap gap-2 items-center">
{filteredIcons.length > 0 ? (
filteredIcons.map((icon, index) => {
const IconComponent = icon.component;
return (
<motion.button
key={index}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() =>
handleIconClick(icon.name, IconComponent)
}
className={`w-fit p-2 rounded-xl transition-colors hover:${colors.hover} flex items-center justify-center group`}
title={icon.name}
>
<IconComponent
className={`w-5 h-5 group-hover:${colors.text} transition-colors`}
/>
</motion.button>
);
})
) : (
<div
className={`w-full text-center py-8 ${colors.textMuted}`}
>
<svg
className="w-8 h-8 mx-auto mb-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709"
/>
</svg>
<p className="text-sm">No icons found</p>
<p className="text-xs mt-1">Try a different search term</p>
</div>
)}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default IconSelector;
- Import required icons, or probably all from the icon library
- Create an object with icons with a name
- Render them using the map method
- Add an onclick event that provides the icon and name as a callback when clicked
The next part is to add a search input. This basically helps to quickly find the icon using name and some alphabet, and don't worry, I've a JavaScript one method solution for adding search, and if you are an avid reader of iHateReading, you might find fuse.js as well a choice to add search functionality.
import React, { useState, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Fuse from "fuse.js";
import {
Folder,
FolderOpen,
FileText,
File,
FileImage,
FileCode,
FileVideo,
FileAudio,
FileSpreadsheet,
FileArchive,
FileCheck,
FileX,
FilePlus,
FileMinus,
FileEdit,
FileSearch,
FileHeart,
FileClock,
FileWarning,
FileQuestion,
FileSliders,
FileBarChart,
FilePieChart,
FileLineChart,
Database,
Activity,
Server,
Cloud,
Upload,
Share2,
Unlock,
Shield,
Settings,
Cog,
Wrench,
Hammer,
PenTool,
Zap,
CloudLightningIcon,
Star,
Bookmark,
Tag,
Text,
Award,
Crown,
Gem,
Diamond,
Circle,
Square,
Triangle,
Hexagon,
Octagon,
EllipsisVertical,
RectangleEllipsis,
Package,
Container,
ShoppingBasket,
Briefcase,
Badge,
Key,
Lock,
Box,
LucideFolderOpen,
FolderPlus,
FolderEdit,
FolderHeart,
FolderClock,
FileWarningIcon,
FolderMinus,
FolderSearch,
FileQuestionIcon,
Info,
Settings2,
CogIcon,
WrenchIcon,
CloudLightning,
} from "lucide-react";
const IconSelector = ({
isOpen,
onClose,
onSelectIcon,
colors,
position = { x: 0, y: 0 },
}) => {
const [searchQuery, setSearchQuery] = useState("");
const icons = [
{ name: "Folder", component: Folder },
{ name: "FolderOpen", component: FolderOpen },
{ name: "LucideFolderOpen", component: LucideFolderOpen },
{ name: "FolderPlus", component: FolderPlus },
{ name: "FolderMinus", component: FolderMinus },
{ name: "FolderEdit", component: FolderEdit },
{ name: "FolderSearch", component: FolderSearch },
{ name: "FolderHeart", component: FolderHeart },
{ name: "FolderClock", component: FolderClock },
{ name: "FileWarning", component: FileWarningIcon },
{ name: "FileQuestion", component: FileQuestionIcon },
{ name: "Info", component: Info },
{ name: "Settings2", component: Settings2 },
{ name: "CogIcon", component: CogIcon },
{ name: "WrenchIcon", component: WrenchIcon },
{ name: "CloudLightning", component: CloudLightning },
{ name: "FileText", component: FileText },
{ name: "File", component: File },
{ name: "FileImage", component: FileImage },
{ name: "FileCode", component: FileCode },
{ name: "FileVideo", component: FileVideo },
{ name: "FileAudio", component: FileAudio },
{ name: "FileSpreadsheet", component: FileSpreadsheet },
{ name: "FileArchive", component: FileArchive },
{ name: "FileCheck", component: FileCheck },
{ name: "FileX", component: FileX },
{ name: "FilePlus", component: FilePlus },
{ name: "FileMinus", component: FileMinus },
{ name: "FileEdit", component: FileEdit },
{ name: "FileSearch", component: FileSearch },
{ name: "FileHeart", component: FileHeart },
{ name: "FileClock", component: FileClock },
{ name: "FileWarning", component: FileWarning },
{ name: "FileQuestion", component: FileQuestion },
{ name: "FileSliders", component: FileSliders },
{ name: "FileBarChart", component: FileBarChart },
{ name: "FilePieChart", component: FilePieChart },
{ name: "FileLineChart", component: FileLineChart },
{ name: "Activity", component: Activity },
{ name: "Database", component: Database },
{ name: "Server", component: Server },
{ name: "Cloud", component: Cloud },
{ name: "Download", component: Upload },
{ name: "Share", component: Share2 },
{ name: "Lock", component: Lock },
{ name: "Unlock", component: Unlock },
{ name: "Shield", component: Shield },
{ name: "Key", component: Key },
{ name: "Settings", component: Settings },
{ name: "Cog", component: Cog },
{ name: "Wrench", component: Wrench },
{ name: "Hammer", component: Hammer },
{ name: "Tool", component: PenTool },
{ name: "Zap", component: Zap },
{ name: "Lightning", component: CloudLightningIcon },
{ name: "Star", component: Star },
{ name: "Bookmark", component: Bookmark },
{ name: "Tag", component: Tag },
{ name: "Label", component: Text },
{ name: "Badge", component: Badge },
{ name: "Award", component: Award },
{ name: "Trophy", component: Crown },
{ name: "Gem", component: Gem },
{ name: "Diamond", component: Diamond },
{ name: "Circle", component: Circle },
{ name: "Square", component: Square },
{ name: "Triangle", component: Triangle },
{ name: "Hexagon", component: Hexagon },
{ name: "Octagon", component: Octagon },
{ name: "Ellipse", component: EllipsisVertical },
{ name: "Rectangle", component: RectangleEllipsis },
{ name: "Box", component: Box },
{ name: "Package", component: Package },
{ name: "Container", component: Container },
{ name: "Basket", component: ShoppingBasket },
{ name: "Briefcase", component: Briefcase },
];
// Configure Fuse.js for fuzzy search
const fuse = useMemo(() => {
return new Fuse(icons, {
keys: ["name"],
threshold: 0.3,
includeScore: true,
});
}, []);
// Filter icons based on search query
const filteredIcons = useMemo(() => {
if (!searchQuery.trim()) {
return icons;
}
const results = fuse.search(searchQuery);
return results.map((result) => result.item);
}, [searchQuery, fuse]);
const handleIconClick = (iconName, IconComponent) => {
onSelectIcon(iconName, IconComponent);
onClose();
};
const handleClose = () => {
setSearchQuery("");
onClose();
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-zinc-50/20 bg-opacity-20 z-40"
onClick={handleClose}
/>
{/* Positioned Modal */}
<motion.div
initial={{ scale: 0.8, opacity: 0, y: -10 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0, y: -10 }}
className={`fixed z-50 ${colors.card} rounded-xl shadow-xl border ${colors.border} overflow-hidden`}
style={{
left: `${position.x}px`,
top: `${position.y}px`,
width: "400px",
height: "320px",
maxWidth: "90vw",
maxHeight: "80vh",
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-zinc-200">
<h3 className={`text-lg font-semibold ${colors.text}`}>
Select Icon
</h3>
<button
onClick={handleClose}
className={`p-1 ${colors.buttonSecondary} rounded-xl transition-colors hover:${colors.hover}`}
>
<svg
className={`w-4 h-4 ${colors.textMuted}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Search Input */}
<div className="p-4 border-b border-zinc-200">
<div
className={`flex gap-1 items-center justify-start ${colors.input} ${colors.text} rounded-xl hover:bg-zinc-50 px-2`}
>
<svg
className={` w-4 h-4 ${colors.textMuted}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search icons..."
className={`w-full px py-2 ${colors.input} rounded-xl border-none hover:bg-zinc-50 hover:dark:bg-zinc-800 ${colors.textPlaceholder} focus:outline-none transition-colors`}
autoFocus
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className={`absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 ${colors.textMuted} hover:${colors.text} transition-colors`}
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
</div>
{/* Icons Grid */}
<div
className="p-4 overflow-y-auto"
style={{ height: "calc(100% - 140px)" }}
>
<div className="flex flex-wrap gap-2 items-center">
{filteredIcons.length > 0 ? (
filteredIcons.map((icon, index) => {
const IconComponent = icon.component;
return (
<motion.button
key={index}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() =>
handleIconClick(icon.name, IconComponent)
}
className={`w-fit p-2 rounded-xl transition-colors hover:${colors.hover} flex items-center justify-center group`}
title={icon.name}
>
<IconComponent
className={`w-5 h-5 group-hover:${colors.text} transition-colors`}
/>
</motion.button>
);
})
) : (
<div
className={`w-full text-center py-8 ${colors.textMuted}`}
>
<svg
className="w-8 h-8 mx-auto mb-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709"
/>
</svg>
<p className="text-sm">No icons found</p>
<p className="text-xs mt-1">Try a different search term</p>
</div>
)}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default IconSelector;
In this, I've added the Fuse.js search method and one more thing, we are using the framer-motion npm module to add the modal on the exact position given by the parent component or passed as props.
And that would be enough for today, see you in the next one
Shrey