Icon Picker
A React component for icon picker.
Component
Note that, this component requires some dependencies like react-icons
, Shadcn UI
and lodash
.
Create a file called IconItem.tsx
with this snippet.
import React from 'react';
import { iconMap } from '@/lib/icons';
import { clsx } from 'clsx';
function formatIconName(iconName: string): string {
return iconName
.replace(/^Fa/, '')
.replace(/([a-z])([A-Z])/g, '$1 $2');
}
const IconItem = React.memo(function IconItem({
iconName,
isActive,
onClick,
showTitle = false,
}: {
iconName: string;
isActive: boolean;
onClick: () => void;
showTitle?: boolean;
}) {
const IconComponent = iconMap[iconName as keyof typeof iconMap];
if (!IconComponent) return null;
return (
<button
type="button"
title={formatIconName(iconName)}
className={clsx(
"group flex flex-col items-center justify-center cursor-pointer transition duration-200 ease-in-out",
showTitle ? "rounded-lg p-3" : "rounded-full h-14 w-14",
isActive
? "bg-zinc-700/80 text-white shadow-md"
: "text-zinc-400 hover:bg-zinc-800/70 hover:text-white"
)}
onClick={(e) => {
e.preventDefault();
onClick();
}}
aria-label={`Select ${iconName}`}
>
<div className="flex flex-col items-center space-y-2">
<div className="text-2xl">
<IconComponent />
</div>
{showTitle && <span
className={clsx(
"text-xs font-medium transition-colors truncate w-[60px]",
isActive ? "text-white" : "text-zinc-400 group-hover:text-zinc-200"
)}
>
{formatIconName(iconName)}
</span>}
</div>
</button>
);
});
Create a another file called IconPicker.tsx
with this snippet and import it in your component.
import { debounce } from 'lodash';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@components/ui/tooltip';
import IconItem from '@components/IconItem';
import Icon from '@components/Icon';
import { allIcons, iconMap } from '@/lib/icons';
import { clsx } from 'clsx';
const IconPicker = ({ initiallyLoadedIcons = 20, defaultIcon = 'FaQuestion' }: { initiallyLoadedIcons?: number, defaultIcon?: string }) => {
const [activeIcon, setActiveIcon] = useState(defaultIcon);
const [popoverOpen, setPopoverOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [loadedIcons, setLoadedIcons] = useState<number>(initiallyLoadedIcons);
const [isLoading, setIsLoading] = useState(false);
const listRef = useRef<HTMLDivElement>(null);
// Debounced search handler
const debouncedSetQuery = useMemo(
() =>
debounce((value: string) => {
setSearchQuery(value);
setLoadedIcons(initiallyLoadedIcons); // Reset icons when searching
}, 250),
[initiallyLoadedIcons]
);
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetQuery(e.target.value);
}, [debouncedSetQuery]);
const filteredIcons = useMemo(() => {
return searchQuery
? allIcons.filter((icon) =>
icon.toLowerCase().includes(searchQuery.toLowerCase())
)
: allIcons;
}, [searchQuery]);
const visibleIcons = useMemo(
() => filteredIcons.slice(0, loadedIcons),
[filteredIcons, loadedIcons]
);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
if (isLoading) return;
const target = e.target as HTMLDivElement;
const nearBottom =
target.scrollTop + target.clientHeight >= target.scrollHeight - 50;
if (nearBottom && loadedIcons < filteredIcons.length) {
setIsLoading(true);
setTimeout(() => {
setLoadedIcons((prev) => Math.min(prev + 15, filteredIcons.length));
setIsLoading(false);
}, 100);
}
};
// Cleanup debounce on unmount
useEffect(() => {
return () => debouncedSetQuery.cancel();
}, [debouncedSetQuery]);
return (
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<div className="flex items-center justify-center h-96">
<Tooltip delayDuration={600}>
<TooltipTrigger asChild>
<PopoverTrigger
type="button"
className="text-2xl transition-colors duration-200 hover:text-white/90 hover:bg-zinc-700/60 p-3 rounded-full"
>
{activeIcon ? <Icon icon={activeIcon} /> : <Icon icon='RxValueNone' />}
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Change icon</TooltipContent>
</Tooltip>
</div>
<PopoverContent className="!p-0 !border-none !w-auto !rounded-xl">
<div className="icon-picker w-full max-w-lg p-4 bg-zinc-900 text-white border border-zinc-700 rounded-lg shadow-lg">
<input
type="text"
placeholder="Search icons..."
autoComplete="off"
onChange={handleSearch}
className="w-full py-2 px-4 mb-4 border border-zinc-600 bg-zinc-800 text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div
ref={listRef}
className="icon-list no-scrollbar grid grid-cols-4 gap-2 overflow-y-auto overflow-x-hidden max-h-72 scrollbar-thin scrollbar-thumb-zinc-600 scrollbar-track-zinc-800"
onScroll={handleScroll}
>
{visibleIcons.map((iconName) => (
<IconItem
key={iconName}
iconName={iconName}
isActive={iconName === activeIcon}
showTitle
onClick={() => {
setActiveIcon(iconName)
setPopoverOpen(false)
}}
/>
))}
{loadedIcons < filteredIcons.length && (
<div className="col-span-4 flex items-center justify-center py-4">
{isLoading ? (
<div className="text-xs text-zinc-500 animate-pulse">
Loading...
</div>
) : (
<div className="h-4" />
)}
</div>
)}
</div>
</div>
</PopoverContent>
</Popover>
);
};
export default IconPicker;
Important, I know this is a vague solution but copy iconMap
data by .
Icon Component
And finally, create Icon.tsx
.
import { iconMap } from "@/lib/icons";
import { RxValueNone } from "react-icons/rx";
function Icon({ icon }: { icon: string }) {
const checkIconAvailability = () => {
const Icon = iconMap[icon as keyof typeof iconMap];
if (!Icon) {
return <RxValueNone />; // Fallback
}
return <Icon />;
};
return (
<>
{checkIconAvailability()}
</>
)
}
export default Icon;
Bonus
Paste this code into your main css file to hide scrollbar.
@layer utilities {
/* For Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* For IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
Props for IconPicker
These are the props for the IconPicker
component. Customize them according to your needs.
Prop | Type | Default | Description |
---|---|---|---|
initiallyLoadedIcons | number | 20 | Number of icons loaded initially |
defaultIcon | string | FaQuestion | Default icon to be selected |
Props for the IconItem
component.
Prop | Type | Default | Description |
---|---|---|---|
iconName | string | - | Name of the icon |
isActive | boolean | - | Whether the icon is active or not |
onClick | () => void | - | Function to be called when the icon is clicked |
showTitle | boolean | false | Whether to show the title or not |
Made by Dhvanit. Visit my portfolio.