# How to Add Custom Emoji Picker Support to Tiptap Editor in React

> Learn how to add custom emoji picker support to your Tiptap editor in React. Replace the default picker with your own React component to insert emojis seamlessly. Explore the example repository for guidance.

- Repository: [Hung Hoang/reactjs-tiptap-editor](https://github.com/hunghg255/reactjs-tiptap-editor)
- Tags: how-to-guide
- Published: 2026-03-03

---

**You can add custom emoji picker support to the Tiptap editor by replacing the default picker component in [`RichTextEmoji.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/Emoji/Emoji.ts)** – The Tiptap extension definition that declares the emoji node with **EXTENSION_PRIORITY_HIGHEST** (priority `200`) and exposes the toolbar button configuration.
- **[`src/extensions/Emoji/components/RichTextEmoji.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/Emoji/components/RichTextEmoji.tsx)** – The button wrapper that renders a `Popover` containing the emoji picker UI and handles the `onSelectEmoji` callback.
- **[`src/components/ui/emoji-picker.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/components/ui/emoji-picker.tsx)** – The default picker implementation (using `frimousse`) that renders the searchable emoji grid.

Because the picker UI is isolated from the extension core, you can replace [`emoji-picker.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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.

```tsx
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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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.

```tsx
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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/constants/my-emoji-list.ts)) to replace or extend the default `EMOJI_LIST` from [`playground/src/emojis.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/playground/src/emojis.ts). Each entry must contain a `name` and `emoji` property.

```ts
// 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 `RichTextEmoji` component only requires an `onSelectEmoji` callback. The underlying extension ([`Emoji.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/Emoji.ts)) executes `editor.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 substitute `EMOJI_LIST` with API-fetched data, filtered subsets, or brand-specific icon sets without touching the extension core.

## Summary

- **Decoupled architecture**: The emoji UI in [`RichTextEmoji.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/RichTextEmoji.tsx) is separate from the Tiptap extension logic in [`Emoji.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/Emoji.ts), enabling drop-in picker replacements.
- **Required callback interface**: Custom pickers must implement `onEmojiSelect: (payload: { emoji: string }) => void` to 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_LIST` from [`playground/src/emojis.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/playground/src/emojis.ts) with 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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/Emoji/components/RichTextEmoji.tsx) and [`src/components/ui/emoji-picker.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`.