Emoji & Icon Picker
One picker for an emoji or a Lucide icon that stores the choice as a single short string, like emoji:🚀 or icon:rocket.
Preview
Usage
The field stores the serialized string, so a plain z.string() does the job.
import { AutoForm } from '@vitnode/core/components/form/auto-form'
import { AutoFormEmojiIcon } from '@vitnode/core/components/form/fields/emoji-icon'
import { EMOJI_ICON_MAX_LENGTH } from '@vitnode/core/lib/emoji-icon'
const formSchema = z.object({
prefix: z.string().max(EMOJI_ICON_MAX_LENGTH).default(''),
})<AutoForm
formSchema={formSchema}
fields={[
{
id: 'prefix',
component: (props) => (
<AutoFormEmojiIcon {...props} allowRemove label="Prefix" />
),
},
]}
/>The value
type EmojiIconValue =
{ type: 'emoji'; value: string } | { type: 'icon'; value: string }An emoji value is the character (🚀). An icon value is a kebab-case
Lucide name (shield-check), readable in the
database and portable to any Lucide renderer.
For storage, serialize it to emoji:🚀 or icon:rocket. It fits a
varchar(64) column.
import {
parseEmojiIcon,
serializeEmojiIcon,
} from '@vitnode/core/lib/emoji-icon'
serializeEmojiIcon({ type: 'icon', value: 'rocket' })
parseEmojiIcon('emoji:🚀')
parseEmojiIcon('icon:../../etc/passwd')These return "icon:rocket", { type: 'emoji', value: '🚀' } and undefined.
parseEmojiIcon is also the validator: anything that isn't exactly one emoji or
one icon name comes back undefined.
Validate on the server too
The picker only produces valid values. A request body can say anything, so parse it before you write it:
prefix: serializeEmojiIcon(parseEmojiIcon(body.prefix)) || nullRendering a stored value
EmojiIcon renders a parsed value: a glyph for an emoji, an SVG for an icon.
An invalid value renders nothing.
import { EmojiIcon } from '@vitnode/core/components/ui/emoji-icon'
;<EmojiIcon value={parseEmojiIcon(role.prefix)} />Only icon names? Use DynamicIcon. Icons
rendered on the server arrive in the first HTML, and the browser never ships the
whole icon library. That needs one line in getRouter(), already in the
scaffold:
import { setupLucideIconSsr } from '@vitnode/core/tanstack/icons'
setupLucideIconSsr({ router })Without it, DynamicIcon still works but loads icons in the browser after the
page renders.
Both are aria-hidden, since they decorate nearby text. An icon-only control
needs its own aria-label.
Role prefixes
A role's Prefix field (General tab of its edit dialog) uses this picker. The
prefix shows before the role name and before every member's name wherever
RoleFormat or UserFormat renders. It arrives as role.prefix
(null | string), and both components parse it for you.
<UserFormat format user={user} />Performance and keyboard
- Only the trigger ships with your page. Emoji data loads when the popover opens, and the ~1750 icons load only when someone opens the Icon tab.
- The icon grid is virtualized, so its size doesn't hurt scrolling.
- The grid is one tab stop. Arrows move (up and down jump a row), Home / End go to the row's ends, Enter or Space picks.
- The emoji picker here and in the editor toolbar share grid size, search and skin tone switch.
Props
EmojiIconPicker
Prop
Type
Other button props go to the trigger. AutoFormEmojiIcon takes the same props
except value and onChange.
EmojiPicker and IconPicker
The bare panels, with no popover or trigger. Drop them into a dialog, sidebar or toolbar.
import { EmojiPicker } from '@vitnode/core/components/ui/emoji-picker'
import { IconPicker } from '@vitnode/core/components/ui/icon-picker'
;<IconPicker onSelect={setIcon} value={icon} />Prop
Type