Home / Articles / Blog

Fuse.jsSearchIcon Selector

Building Icon Selector Modal

October 9 2025 letter

Shrey Vijayvargiya

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.

text
npm i lucide-react react-icons

Choice 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

text
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;
  1. Import required icons, or probably all from the icon library
  2. Create an object with icons with a name
  3. Render them using the map method
  4. 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.

text
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

External Links (1)
Weekly letter

Subscribe to iHateReading

Our once-a-week newsletter on programming, jobs, AI, and building products online.

Our once a week newsletter on Programming, Jobs, AI, and Business