How to Add Custom Emoji Picker Support to Tiptap Editor in React
You can add custom emoji picker support to the Tiptap editor by replacing the default picker component in RichTextEmoji.tsx with a custom React component that implements the onEmojiSelect callback, allowing you to insert any emoji string at the cursor position without modifying the core extension logic.
The hunghg255/reactjs-tiptap-editor repository implements emoji functionality through a decoupled architecture that separates the Tiptap extension logic from the React UI layer. Adding custom emoji picker support requires only swapping the presentation component in the toolbar button wrapper while maintaining the underlying insertion mechanism defined in src/extensions/Emoji/Emoji.ts.
Architecture Overview
The editor’s emoji system consists of three decoupled layers that enable easy customization:
src/extensions/Emoji/Emoji.ts– The Tiptap extension definition that declares the emoji node with EXTENSION_PRIORITY_HIGHEST (priority200) and exposes the toolbar button configuration.src/extensions/Emoji/components/RichTextEmoji.tsx– The button wrapper that renders aPopovercontaining the emoji picker UI and handles theonSelectEmojicallback.src/components/ui/emoji-picker.tsx– The default picker implementation (usingfrimousse) that renders the searchable emoji grid.
Because the picker UI is isolated from the extension core, you can replace emoji-picker.tsx with any custom component that accepts an onEmojiSelect prop receiving an object with an emoji string property.
Step-by-Step Implementation
1. Create a Custom Picker Component
Create a new file at src/components/ui/custom-emoji-picker.tsx that exposes the required callback interface. The component must accept an onEmojiSelect prop that receives { emoji: string } when a user selects an emoji.
import React, { useState } from 'react';
import { MY_EMOJI_LIST } from '@/constants/my-emoji-list';
interface CustomEmojiPickerProps {
className?: string;
onEmojiSelect: (payload: { emoji: string }) => void;
}
export const CustomEmojiPicker = ({
className,
onEmojiSelect
}: CustomEmojiPickerProps) => {
const [query, setQuery] = useState('');
const filteredEmojis = MY_EMOJI_LIST.filter(e =>
e.name.toLowerCase().includes(query.toLowerCase()),
);
return (
<div className={`p-2 w-64 ${className}`}>
<input
type="text"
placeholder="Search emojis..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full p-2 mb-2 border rounded"
/>
<div className="grid grid-cols-8 gap-1">
{filteredEmojis.map(({ emoji, name }) => (
<button
key={name}
onClick={() => onEmojiSelect({ emoji })}
className="p-1 text-xl hover:bg-gray-100 rounded"
title={name}
>
{emoji}
</button>
))}
</div>
</div>
);
};
2. Wire the Custom Picker into the Toolbar
Modify src/extensions/Emoji/components/RichTextEmoji.tsx to import your custom picker and replace the default <EmojiPicker /> component. The EmojiPickerWrap function handles the popover state and passes the selected emoji to the onSelectEmoji callback, which executes the Tiptap chain command.
import React, { useState } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components';
import { CustomEmojiPicker } from '@/components/ui/custom-emoji-picker';
interface IProps {
onSelectEmoji: (emoji: string) => void;
children: React.ReactNode;
disabled?: boolean;
}
function EmojiPickerWrap({ onSelectEmoji, children, disabled }: IProps) {
const [isOpen, setIsOpen] = useState(false);
const onOpenChange = (open: boolean) => {
if (disabled) return;
setIsOpen(open);
};
return (
<Popover onOpenChange={onOpenChange} open={isOpen}>
<PopoverTrigger asChild>
{children}
</PopoverTrigger>
<PopoverContent className="!richtext-w-fit !richtext-p-0">
<CustomEmojiPicker
className="!richtext-h-[342px]"
onEmojiSelect={({ emoji }) => {
onSelectEmoji(emoji);
setIsOpen(false);
}}
/>
</PopoverContent>
</Popover>
);
}
export default EmojiPickerWrap;
3. Provide a Custom Emoji Data Source
Define your emoji list in a separate file (e.g., src/constants/my-emoji-list.ts) to replace or extend the default EMOJI_LIST from playground/src/emojis.ts. Each entry must contain a name and emoji property.
// src/constants/my-emoji-list.ts
export const MY_EMOJI_LIST = [
{ name: 'heart', emoji: '❤️' },
{ name: 'fire', emoji: '🔥' },
{ name: 'brand-logo', emoji: '🅱️' },
{ name: 'product-star', emoji: '⭐' },
// Add custom SVG references or brand-specific icons here
];
Why This Integration Works
The architectural decoupling in reactjs-tiptap-editor makes custom picker integration seamless for three reasons:
- Isolated UI layer: The
RichTextEmojicomponent only requires anonSelectEmojicallback. The underlying extension (Emoji.ts) executeseditor.chain().focus().insertContentAt($anchor.pos, emoji).run()regardless of which UI produced the emoji string. - Highest extension priority: The emoji node is registered with priority
200(EXTENSION_PRIORITY_HIGHEST), ensuring it renders before other inline nodes and maintains consistent cursor positioning after insertion. - Flexible data contracts: The picker receives a flat array of
{ name, emoji }objects, allowing you to substituteEMOJI_LISTwith API-fetched data, filtered subsets, or brand-specific icon sets without touching the extension core.
Summary
- Decoupled architecture: The emoji UI in
RichTextEmoji.tsxis separate from the Tiptap extension logic inEmoji.ts, enabling drop-in picker replacements. - Required callback interface: Custom pickers must implement
onEmojiSelect: (payload: { emoji: string }) => voidto communicate with the editor. - Zero core modifications: You can swap the picker component and emoji data source without changing the extension definition or priority settings.
- Custom data support: Replace
EMOJI_LISTfromplayground/src/emojis.tswith your own dataset to control available emojis.
Frequently Asked Questions
Can I use a third-party emoji picker library like Emoji Mart?
Yes. Import any React emoji picker library into a wrapper component that translates its selection event to the onEmojiSelect({ emoji: string }) interface expected by RichTextEmoji.tsx. As long as your wrapper calls onEmojiSelect with the emoji string, the Tiptap extension will insert it at the cursor position without requiring changes to src/extensions/Emoji/Emoji.ts.
Do I need to modify the Tiptap extension core to change the emoji picker appearance?
No. The extension core in src/extensions/Emoji/Emoji.ts only defines the node type and toolbar button configuration. The picker UI is rendered entirely within src/extensions/Emoji/components/RichTextEmoji.tsx and src/components/ui/emoji-picker.tsx, allowing you to modify or replace the visual layer independently of the document model.
How do I restrict which emojis appear in the picker?
Create a filtered array in your custom picker component or define a new constant file (e.g., src/constants/limited-emojis.ts) that exports only the emoji objects you want to display. Import this array instead of the default EMOJI_LIST in your custom picker. The extension will accept any string passed to onEmojiSelect, regardless of whether it exists in a predefined list.
Why is the emoji extension priority set to highest (200)?
The EXTENSION_PRIORITY_HIGHEST setting ensures the emoji node is processed before other inline nodes during the document rendering pipeline. This guarantees that emoji insertion at the cursor position takes precedence over other inline decorations or marks, preventing layout conflicts and ensuring consistent behavior when the picker inserts content via insertContentAt.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →