The EditorSuggestionMenu component is used to display a menu of formatting and action suggestions when typing a trigger character in the editor. It allows users to quickly insert blocks and formatting by typing a trigger character. It must be used inside an Editor component's default slot to have access to the editor instance.
Type / in the editor to open the suggestion menu.
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'
const value = ref(`# Suggestion Menu
Type / to open the suggestion menu and browse available formatting commands.`)
const items: EditorSuggestionMenuItem[][] = [
[
{
type: 'label',
label: 'Text'
},
{
kind: 'paragraph',
label: 'Paragraph',
icon: 'i-lucide-type'
},
{
kind: 'heading',
level: 1,
label: 'Heading 1',
icon: 'i-lucide-heading-1'
},
{
kind: 'heading',
level: 2,
label: 'Heading 2',
icon: 'i-lucide-heading-2'
},
{
kind: 'heading',
level: 3,
label: 'Heading 3',
icon: 'i-lucide-heading-3'
}
],
[
{
type: 'label',
label: 'Lists'
},
{
kind: 'bulletList',
label: 'Bullet List',
icon: 'i-lucide-list'
},
{
kind: 'orderedList',
label: 'Numbered List',
icon: 'i-lucide-list-ordered'
}
],
[
{
type: 'label',
label: 'Insert'
},
{
kind: 'blockquote',
label: 'Blockquote',
icon: 'i-lucide-text-quote'
},
{
kind: 'codeBlock',
label: 'Code Block',
icon: 'i-lucide-square-code'
},
{
kind: 'horizontalRule',
label: 'Divider',
icon: 'i-lucide-separator-horizontal'
}
]
]
// 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 / for commands..."
class="w-full min-h-21"
>
<UEditorSuggestionMenu :editor="editor" :items="items" :append-to="appendToBody" />
</UEditor>
</template>
Use the items prop to define the available commands in the menu. Items can be editor commands with a kind property or separators and labels.
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Type / to see organized command groups.' }]
}, {
type: 'paragraph'
}]
})
const suggestionItems: EditorSuggestionMenuItem[][] = [[{
type: 'label',
label: 'Text Styles'
}, {
kind: 'paragraph',
label: 'Paragraph',
icon: 'i-lucide-type'
}, {
kind: 'heading',
level: 1,
label: 'Heading 1',
icon: 'i-lucide-heading-1'
}, {
kind: 'heading',
level: 2,
label: 'Heading 2',
icon: 'i-lucide-heading-2'
}, {
kind: 'heading',
level: 3,
label: 'Heading 3',
icon: 'i-lucide-heading-3'
}], [{
type: 'label',
label: 'Lists'
}, {
kind: 'bulletList',
label: 'Bullet List',
icon: 'i-lucide-list'
}, {
kind: 'orderedList',
label: 'Numbered List',
icon: 'i-lucide-list-ordered'
}], [{
type: 'label',
label: 'Blocks'
}, {
kind: 'blockquote',
label: 'Blockquote',
icon: 'i-lucide-text-quote'
}, {
kind: 'codeBlock',
label: 'Code Block',
icon: 'i-lucide-square-code'
}, {
kind: 'horizontalRule',
label: 'Divider',
icon: 'i-lucide-separator-horizontal'
}]]
</script>
<template>
<UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Type / for commands...">
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
</UEditor>
</template>
Each item supports these properties:
| Property | Description |
|---|---|
kind | Editor command type (heading, bulletList, blockquote, etc.) |
label | Display text for the item |
description | Optional description shown below the label |
icon | Icon displayed before the label |
type: 'label' | Creates a section header (non-selectable) |
type: 'separator' | Creates a visual divider between groups |
Use the char prop to change the trigger character. Defaults to /.
<template>
<UEditorSuggestionMenu :editor="editor" :items="items" char=">" />
</template>
> for block commands or + for insertions.Use the options prop to customize the positioning behavior using Floating UI options.
<template>
<UEditorSuggestionMenu
:editor="editor"
:items="items"
:options="{
placement: 'bottom-start',
offset: 4
}"
/>
</template>
Create an organized suggestion menu with labeled sections and separators.
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Type / to see a fully customized suggestion menu.' }]
}, {
type: 'paragraph'
}]
})
const suggestionItems: EditorSuggestionMenuItem[][] = [[{
type: 'label',
label: '📝 Text Formatting'
}, {
kind: 'paragraph',
label: 'Normal Text',
description: 'Standard paragraph text',
icon: 'i-lucide-type'
}, {
kind: 'heading',
level: 1,
label: 'Large Heading',
description: 'Top level heading',
icon: 'i-lucide-heading-1'
}, {
kind: 'heading',
level: 2,
label: 'Medium Heading',
description: 'Section heading',
icon: 'i-lucide-heading-2'
}], [{
type: 'separator'
}], [{
type: 'label',
label: '📋 Lists & Structure'
}, {
kind: 'bulletList',
label: 'Bullet List',
description: 'Unordered list items',
icon: 'i-lucide-list'
}, {
kind: 'orderedList',
label: 'Number List',
description: 'Ordered list items',
icon: 'i-lucide-list-ordered'
}], [{
type: 'separator'
}], [{
type: 'label',
label: '🎨 Special Blocks'
}, {
kind: 'blockquote',
label: 'Quote',
description: 'Highlight a quote',
icon: 'i-lucide-text-quote'
}, {
kind: 'codeBlock',
label: 'Code',
description: 'Code with syntax',
icon: 'i-lucide-square-code'
}]]
</script>
<template>
<UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Type / for commands...">
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
</UEditor>
</template>
Add descriptions to help users understand what each command does.
<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@nuxt/ui'
const value = ref({
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Type / to see commands with helpful icons and descriptions.' }]
}, {
type: 'paragraph'
}]
})
const suggestionItems: EditorSuggestionMenuItem[][] = [[{
kind: 'paragraph',
label: 'Paragraph',
description: 'Regular text paragraph',
icon: 'i-lucide-type'
}, {
kind: 'heading',
level: 1,
label: 'Heading 1',
description: 'Large section heading',
icon: 'i-lucide-heading-1'
}, {
kind: 'heading',
level: 2,
label: 'Heading 2',
description: 'Medium section heading',
icon: 'i-lucide-heading-2'
}, {
kind: 'heading',
level: 3,
label: 'Heading 3',
description: 'Small section heading',
icon: 'i-lucide-heading-3'
}, {
kind: 'bulletList',
label: 'Bullet List',
description: 'Create an unordered list',
icon: 'i-lucide-list'
}, {
kind: 'orderedList',
label: 'Numbered List',
description: 'Create an ordered list',
icon: 'i-lucide-list-ordered'
}, {
kind: 'blockquote',
label: 'Quote',
description: 'Add a blockquote',
icon: 'i-lucide-text-quote'
}, {
kind: 'codeBlock',
label: 'Code Block',
description: 'Insert code snippet',
icon: 'i-lucide-square-code'
}, {
kind: 'horizontalRule',
label: 'Divider',
description: 'Add a horizontal line',
icon: 'i-lucide-separator-horizontal'
}]]
</script>
<template>
<UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Type / for commands...">
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
</UEditor>
</template>
| Prop | Default | Type |
|---|---|---|
editor | Editor | |
char | '/' | stringThe trigger character (e.g., '/', '@', ':') |
pluginKey | 'suggestionMenu' | stringPlugin key to identify this menu |
items | EditorSuggestionMenuItem<EditorCustomHandlers>[] | EditorSuggestionMenuItem<EditorCustomHandlers>[][]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: {
editorSuggestionMenu: {
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: {
editorSuggestionMenu: {
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'
]
}
}
}
}
}
})
]
})