The EditorEmojiMenu component is used to display a menu of emoji suggestions when typing the : character in the editor. Emojis are inserted as text characters or custom nodes depending on the extension configuration. It must be used inside an Editor component's default slot to have access to the editor instance.
Type : followed by an emoji name to search and insert emojis.
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'
const value = ref(`# Emoji Menu
Type : to insert emojis and select from the list of available emojis.`)
const items: EditorEmojiMenuItem[] = gitHubEmojis.filter(
(emoji) => !emoji.name.startsWith('regional_indicator_')
)
// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = false ? () => document.body : undefined
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
:extensions="[Emoji]"
content-type="markdown"
placeholder="Type : to add emojis..."
class="w-full min-h-21"
>
<UEditorEmojiMenu :editor="editor" :items="items" :append-to="appendToBody" />
</UEditor>
</template>
Use the items prop to define the available emojis. Each item should include the emoji character, name, shortcodes, and optional tags.
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji } from '@tiptap/extension-emoji'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Type : to see popular emojis.' }]
}, {
type: 'paragraph'
}]
})
const emojiItems: EditorEmojiMenuItem[] = [{
name: 'smile',
emoji: 'π',
shortcodes: ['smile'],
tags: ['happy', 'joy', 'pleased']
}, {
name: 'heart',
emoji: 'β€οΈ',
shortcodes: ['heart'],
tags: ['love', 'like']
}, {
name: 'thumbsup',
emoji: 'π',
shortcodes: ['thumbsup', '+1'],
tags: ['approve', 'ok']
}, {
name: 'fire',
emoji: 'π₯',
shortcodes: ['fire'],
tags: ['hot', 'burn']
}, {
name: 'rocket',
emoji: 'π',
shortcodes: ['rocket'],
tags: ['ship', 'launch']
}, {
name: 'eyes',
emoji: 'π',
shortcodes: ['eyes'],
tags: ['look', 'watch']
}, {
name: 'tada',
emoji: 'π',
shortcodes: ['tada'],
tags: ['party', 'celebration']
}, {
name: 'thinking',
emoji: 'π€',
shortcodes: ['thinking'],
tags: ['hmm', 'think', 'consider']
}]
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
:extensions="[Emoji]"
content-type="markdown"
placeholder="Type : to add emojis..."
>
<UEditorEmojiMenu :editor="editor" :items="emojiItems" />
</UEditor>
</template>
Each item supports these properties:
| Property | Description |
|---|---|
name | The emoji name used for searching (required) |
emoji | The emoji character to insert |
shortcodes | Array of shortcode strings for search (e.g., ['smile', 'happy']) |
tags | Array of tags for additional search terms |
group | Optional group name for organization |
fallbackImage | Fallback image URL for custom emojis |
gitHubEmojis export from @tiptap/extension-emoji for a comprehensive emoji set with over 1800 emojis.Use the char prop to change the trigger character. Defaults to :.
<template>
<UEditorEmojiMenu :editor="editor" :items="items" char=";" />
</template>
Use the options prop to customize the positioning behavior using Floating UI options.
<template>
<UEditorEmojiMenu
:editor="editor"
:items="items"
:options="{
placement: 'bottom-start',
offset: 4
}"
/>
</template>
Use the GitHub emoji set from TipTap for a comprehensive collection.
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Search through 1800+ GitHub emojis. Try typing :smile:, :rocket:, or :heart:' }]
}, {
type: 'paragraph'
}]
})
const emojiItems: EditorEmojiMenuItem[] = gitHubEmojis.filter(emoji => !emoji.name.startsWith('regional_indicator_'))
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
:extensions="[Emoji]"
content-type="markdown"
placeholder="Type : to add emojis..."
>
<UEditorEmojiMenu :editor="editor" :items="emojiItems" />
</UEditor>
</template>
import { gitHubEmojis } from '@tiptap/extension-emoji'
const items = gitHubEmojis
Create a custom emoji set for your specific use case.
<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji } from '@tiptap/extension-emoji'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'This editor has a custom set of work-related emojis.' }]
}, {
type: 'paragraph'
}]
})
const emojiItems: EditorEmojiMenuItem[] = [{
name: 'approved',
emoji: 'β
',
shortcodes: ['check', 'approved'],
tags: ['done', 'complete', 'yes'],
group: 'status'
}, {
name: 'in_progress',
emoji: 'β³',
shortcodes: ['hourglass', 'waiting'],
tags: ['progress', 'pending'],
group: 'status'
}, {
name: 'blocked',
emoji: 'π«',
shortcodes: ['blocked', 'no'],
tags: ['stop', 'forbidden'],
group: 'status'
}, {
name: 'bug',
emoji: 'π',
shortcodes: ['bug'],
tags: ['error', 'issue'],
group: 'work'
}, {
name: 'feature',
emoji: 'β¨',
shortcodes: ['sparkles', 'new'],
tags: ['feature', 'enhancement'],
group: 'work'
}, {
name: 'docs',
emoji: 'π',
shortcodes: ['books', 'documentation'],
tags: ['docs', 'readme'],
group: 'work'
}, {
name: 'meeting',
emoji: 'π
',
shortcodes: ['calendar', 'meeting'],
tags: ['schedule', 'appointment'],
group: 'schedule'
}, {
name: 'deadline',
emoji: 'β°',
shortcodes: ['clock', 'alarm'],
tags: ['deadline', 'urgent'],
group: 'schedule'
}]
</script>
<template>
<UEditor
v-slot="{ editor }"
v-model="value"
:extensions="[Emoji]"
content-type="markdown"
placeholder="Type : to add work emojis..."
>
<UEditorEmojiMenu :editor="editor" :items="emojiItems" />
</UEditor>
</template>
| Prop | Default | Type |
|---|---|---|
editor | Editor | |
char | ':' | stringThe trigger character (e.g., '/', '@', ':') |
pluginKey | 'emojiMenu' | stringPlugin key to identify this menu |
items | EditorEmojiMenuItem[] | EditorEmojiMenuItem[][]The items to display (can be a flat array or grouped)
| |
limit | 42 | numberMaximum number of items to display |
options | { strategy: 'absolute', placement: 'bottom-start', offset: 8, shift: { padding: 8 } } | FloatingUIOptionsThe options for positioning the menu. Those are passed to Floating UI and include options for the placement, offset, flip, shift, size, autoPlacement, hide, and inline middleware.
|
appendTo | HTMLElement | (): HTMLElementThe DOM element to append the menu to. Default is the editor's parent element. Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues. | |
ui | { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } |
export default defineAppConfig({
ui: {
editorEmojiMenu: {
slots: {
content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted p-1.5 text-xs gap-1.5',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'shrink-0 size-5 flex items-center justify-center text-base',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '2xs',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
},
variants: {
active: {
true: {
item: 'text-highlighted before:bg-elevated/75',
itemLeadingIcon: 'text-default'
},
false: {
item: [
'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
'transition-colors'
]
}
}
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
editorEmojiMenu: {
slots: {
content: 'min-w-48 max-w-60 max-h-96 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-dropdown-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted p-1.5 text-xs gap-1.5',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'shrink-0 size-5 flex items-center justify-center text-base',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '2xs',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
},
variants: {
active: {
true: {
item: 'text-highlighted before:bg-elevated/75',
itemLeadingIcon: 'text-default'
},
false: {
item: [
'text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'text-dimmed group-data-highlighted:not-group-data-disabled:text-default',
'transition-colors'
]
}
}
}
}
}
})
]
})