The EditorEmojiMenu component displays a menu of emoji suggestions when typing the : character in the editor and inserts the selected emoji. It works alongside the @tiptap/extension-emoji package to provide emoji support.
useEditorMenu composable built on top of TipTap's Suggestion utility to filter items as you type and support keyboard navigation (arrow keys, enter to select, escape to close).<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>
@tiptap/extension-emoji package is not installed by default, you need to install it separately.Use the items prop as an array of objects with the following properties:
name: stringemoji: stringshortcodes?: string[]tags?: string[]group?: stringfallbackImage?: string<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@nuxt/ui'
import { Emoji } from '@tiptap/extension-emoji'
const value = ref(`Type : to see a custom emoji set.
You can also install the \`@tiptap/extension-emoji\` extension to use a comprehensive set with over 1800 emojis.`)
const items: 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..."
class="w-full min-h-26"
>
<UEditorEmojiMenu :editor="editor" :items="items" />
</UEditor>
</template>
items prop to create separated groups of items.Use the char prop to change the trigger character. Defaults to :.
<template>
<UEditor v-slot="{ editor }">
<UEditorEmojiMenu :editor="editor" :items="items" char=";" />
</UEditor>
</template>
Use the options prop to customize the positioning behavior using Floating UI options.
<template>
<UEditor v-slot="{ editor }">
<UEditorEmojiMenu
:editor="editor"
:items="items"
:options="{
placement: 'bottom-start',
offset: 4
}"
/>
</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)
| |
filterFields | ["name", "shortcodes", "tags"] | string[]Fields to filter items by. |
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'
]
}
}
}
}
}
})
]
})