EditorSuggestionMenu

GitHub
A command menu that displays formatting and action suggestions when typing the / character in the editor.

Usage

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>
The menu supports keyboard navigation (arrow keys, enter to select, escape to close) and filters items as you type.

Items

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:

PropertyDescription
kindEditor command type (heading, bulletList, blockquote, etc.)
labelDisplay text for the item
descriptionOptional description shown below the label
iconIcon displayed before the label
type: 'label'Creates a section header (non-selectable)
type: 'separator'Creates a visual divider between groups
Use labels and separators to organize commands into logical groups for better discoverability.

Trigger character

Use the char prop to change the trigger character. Defaults to /.

<template>
  <UEditorSuggestionMenu :editor="editor" :items="items" char=">" />
</template>
Common alternatives include > for block commands or + for insertions.

Options

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>

Examples

With sections

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>

With descriptions

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>
Descriptions are especially useful for less common formatting options or custom commands.

API

Props

Prop Default Type
editorEditor
char'/' string

The trigger character (e.g., '/', '@', ':')

pluginKey'suggestionMenu' string

Plugin key to identify this menu

items EditorSuggestionMenuItem<EditorCustomHandlers>[] | EditorSuggestionMenuItem<EditorCustomHandlers>[][]

The items to display (can be a flat array or grouped)

limit42 number

Maximum number of items to display

options{ strategy: 'absolute', placement: 'bottom-start', offset: 8, shift: { padding: 8 } } FloatingUIOptions

The 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 | (): HTMLElement

The 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; }

Theme

app.config.ts
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'
            ]
          }
        }
      }
    }
  }
})
vite.config.ts
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'
                ]
              }
            }
          }
        }
      }
    })
  ]
})

Changelog

No recent changes