The EditorMentionMenu component is used to display a menu of user suggestions when typing the @ character in the editor. Mentions are inserted as inline elements that can be styled and linked. It must be used inside an Editor component's default slot to have access to the editor instance.
Type @ followed by a name to search and insert mentions.
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref(`# Mention Menu
Type @ to mention someone and select from the list of available users.`)
const items: EditorMentionMenuItem[] = [
{
label: 'benjamincanac',
avatar: {
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
}
},
{
label: 'atinux',
avatar: {
src: 'https://avatars.githubusercontent.com/u/904724?v=4'
}
},
{
label: 'danielroe',
avatar: {
src: 'https://avatars.githubusercontent.com/u/28706372?v=4'
}
},
{
label: 'pi0',
avatar: {
src: 'https://avatars.githubusercontent.com/u/5158436?v=4'
}
}
]
// 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"
content-type="markdown"
placeholder="Type @ to mention someone..."
class="w-full min-h-21"
>
<UEditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
</UEditor>
</template>
Use the items prop to define the available mentions. Each item can include a label, avatar, icon, and description.
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Mention a team member or channel: ' }]
}]
})
const mentionItems: EditorMentionMenuItem[] = [{
label: 'benjamincanac',
description: 'Benjamin Canac',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
}, {
label: 'atinux',
description: 'Sébastien Chopin',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
}, {
label: 'general',
description: 'General channel',
icon: 'i-lucide-hash'
}, {
label: 'engineering',
description: 'Engineering team',
icon: 'i-lucide-users'
}]
</script>
<template>
<UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Type @ to mention...">
<UEditorMentionMenu :editor="editor" :items="mentionItems" />
</UEditor>
</template>
Each item supports these properties:
| Property | Description |
|---|---|
label | The mention text that gets inserted (required) |
avatar | Avatar props for displaying a user image |
icon | Icon to display if no avatar is provided |
description | Optional description shown below the label |
disabled | Whether the item can be selected |
Use the char prop to change the trigger character. Defaults to @.
<template>
<UEditorMentionMenu :editor="editor" :items="channels" char="#" />
</template>
# for channels or tags, + for adding team members, etc.Use the options prop to customize the positioning behavior using Floating UI options.
<template>
<UEditorMentionMenu
:editor="editor"
:items="items"
:options="{
placement: 'bottom-start',
offset: 4
}"
/>
</template>
Add descriptions to provide more context about each user or entity.
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Tag team members: ' }]
}]
})
const mentionItems: EditorMentionMenuItem[] = [{
label: 'benjamincanac',
description: 'Co-founder',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
}, {
label: 'atinux',
description: 'Co-founder',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
}, {
label: 'danielroe',
description: 'Framework Lead',
avatar: { src: 'https://avatars.githubusercontent.com/u/28706372?v=4' }
}, {
label: 'pi0',
description: 'UnJS Lead',
avatar: { src: 'https://avatars.githubusercontent.com/u/5158436?v=4' }
}, {
label: 'antfu',
description: 'Core Team',
avatar: { src: 'https://avatars.githubusercontent.com/u/11247099?v=4' }
}]
</script>
<template>
<UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Type @ to mention someone...">
<UEditorMentionMenu :editor="editor" :items="mentionItems" />
</UEditor>
</template>
You can fetch mention suggestions dynamically based on the user's query.
<script setup lang="ts">
import type { EditorMentionMenuItem } from '@nuxt/ui'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Type @ to mention a GitHub user (simulated fetch).' }]
}]
})
// Simulate fetched data
const allUsers: EditorMentionMenuItem[] = [{
label: 'benjamincanac',
description: 'Benjamin Canac',
avatar: { src: 'https://avatars.githubusercontent.com/u/739984?v=4' }
}, {
label: 'atinux',
description: 'Sébastien Chopin',
avatar: { src: 'https://avatars.githubusercontent.com/u/904724?v=4' }
}, {
label: 'danielroe',
description: 'Daniel Roe',
avatar: { src: 'https://avatars.githubusercontent.com/u/28706372?v=4' }
}, {
label: 'pi0',
description: 'Pooya Parsa',
avatar: { src: 'https://avatars.githubusercontent.com/u/5158436?v=4' }
}, {
label: 'antfu',
description: 'Anthony Fu',
avatar: { src: 'https://avatars.githubusercontent.com/u/11247099?v=4' }
}, {
label: 'romhml',
description: 'Romain Hamel',
avatar: { src: 'https://avatars.githubusercontent.com/u/4546096?v=4' }
}]
const mentionItems = ref<EditorMentionMenuItem[]>(allUsers)
</script>
<template>
<UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Type @ to mention...">
<UEditorMentionMenu :editor="editor" :items="mentionItems" />
</UEditor>
</template>
| Prop | Default | Type |
|---|---|---|
editor | Editor | |
char | '@' | stringThe trigger character (e.g., '/', '@', ':') |
pluginKey | 'mentionMenu' | stringPlugin key to identify this menu |
items | EditorMentionMenuItem[] | EditorMentionMenuItem[][]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: {
editorMentionMenu: {
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: {
editorMentionMenu: {
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'
]
}
}
}
}
}
})
]
})