EditorToolbar

GitHub
A customizable toolbar for editor actions that can be displayed as fixed, bubble, or floating menu.

Usage

The EditorToolbar component is used to display a toolbar of formatting buttons that automatically sync their active state with the editor content. It must be used inside an Editor component's default slot to have access to the editor instance.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: EditorToolbarItem[][] = [
  [
    {
      icon: 'i-lucide-heading',
      content: {
        align: 'start'
      },
      items: [
        {
          kind: 'heading',
          level: 1,
          icon: 'i-lucide-heading-1',
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          icon: 'i-lucide-heading-2',
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          icon: 'i-lucide-heading-3',
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          icon: 'i-lucide-heading-4',
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: 'i-lucide-bold'
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: 'i-lucide-italic'
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: 'i-lucide-underline'
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: 'i-lucide-strikethrough'
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: 'i-lucide-code'
    }
  ]
]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Start typing..."
    class="w-full min-h-21"
  >
    <UEditorToolbar :editor="editor" :items="items" layout="bubble" />
  </UEditor>
</template>

Items

Use the items prop as an array of objects with the following properties:

  • label?: string
  • icon?: string
  • color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"
  • variant?: "solid" | "outline" | "soft" | "ghost" | "link" | "subtle"
  • size?: "xs" | "sm" | "md" | "lg" | "xl"
  • kind?: "mark" | "textAlign" | "heading" | "link" | "image" | "blockquote" | "bulletList" | "orderedList" | "codeBlock" | "horizontalRule" | "paragraph" | "undo" | "redo" | "clearFormatting" | "duplicate" | "delete" | "moveUp" | "moveDown" | "suggestion" | "mention" | "emoji"
  • disabled?: boolean
  • loading?: boolean
  • active?: boolean
  • slot?: string
  • onClick?: (e: MouseEvent) => void
  • items?: EditorToolbarItem[] | EditorToolbarItem[][]
  • class?: any
The kind property references a handler defined in the Editor component. Handlers wrap TipTap commands and manage their state (active, disabled, etc.). The Editor provides default handlers for common actions (mark, heading, link, etc.), but you can add custom handlers using the handlers prop on the Editor component.
When using the kind property for editor-specific actions, additional properties may be required:
  • For kind: "mark": mark: "bold" | "italic" | "strike" | "code" | "underline"
  • For kind: "textAlign": align: "left" | "center" | "right" | "justify"
  • For kind: "heading": level: 1 | 2 | 3 | 4 | 5 | 6
  • For kind: "link": href?: string
  • For kind: "image": src?: string
  • For kind: "duplicate" | "delete" | "moveUp" | "moveDown": pos: number
  • For kind: "clearFormatting" | "suggestion": pos?: number

You can pass any property from the Button component such as color, variant, size, etc. but also active-color and active-variant as items with a kind property automatically sync their active state with the editor.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'
import TextAlign from '@tiptap/extension-text-align'

const value = ref(`# Nuxt UI Editor

This toolbar showcases **all available formatting options** using built-in handlers. Try the different controls to see them in action!

## Text Formatting

You can apply **bold**, *italic*, <u>underline</u>, ~~strikethrough~~, and \`inline code\` formatting to your text.

## Block Types

### Lists

Here's a bullet list:
- First item
- Second item
- Third item

And a numbered list:
1. Step one
2. Step two
3. Step three

### Blockquote

> This is a blockquote. Use it for highlighting important information or quotes from other sources.

### Code Block

\`\`\`
// Code blocks are perfect for technical content
function hello() {
  console.log('Hello, world!')
}
\`\`\`

---

Use the horizontal rule above to create visual separations in your content. Try all the toolbar controls to explore the full range of formatting options!`)

const toolbarItems: EditorToolbarItem[][] = [
  // History controls
  [{
    kind: 'undo',
    icon: 'i-lucide-undo'
  }, {
    kind: 'redo',
    icon: 'i-lucide-redo'
  }],
  // Block types
  [{
    icon: 'i-lucide-heading',
    content: {
      align: 'start'
    },
    items: [{
      type: 'label',
      label: 'Headings'
    }, {
      kind: 'heading',
      level: 1,
      icon: 'i-lucide-heading-1',
      label: 'Heading 1'
    }, {
      kind: 'heading',
      level: 2,
      icon: 'i-lucide-heading-2',
      label: 'Heading 2'
    }, {
      kind: 'heading',
      level: 3,
      icon: 'i-lucide-heading-3',
      label: 'Heading 3'
    }, {
      kind: 'heading',
      level: 4,
      icon: 'i-lucide-heading-4',
      label: 'Heading 4'
    }]
  }, {
    icon: 'i-lucide-list',
    content: {
      align: 'start'
    },
    items: [{
      kind: 'bulletList',
      icon: 'i-lucide-list',
      label: 'Bullet List'
    }, {
      kind: 'orderedList',
      icon: 'i-lucide-list-ordered',
      label: 'Ordered List'
    }]
  }, {
    kind: 'blockquote',
    icon: 'i-lucide-text-quote'
  }, {
    kind: 'codeBlock',
    icon: 'i-lucide-square-code'
  }, {
    kind: 'horizontalRule',
    icon: 'i-lucide-separator-horizontal'
  }],
  // Text formatting
  [{
    kind: 'mark',
    mark: 'bold',
    icon: 'i-lucide-bold'
  }, {
    kind: 'mark',
    mark: 'italic',
    icon: 'i-lucide-italic'
  }, {
    kind: 'mark',
    mark: 'underline',
    icon: 'i-lucide-underline'
  }, {
    kind: 'mark',
    mark: 'strike',
    icon: 'i-lucide-strikethrough'
  }, {
    kind: 'mark',
    mark: 'code',
    icon: 'i-lucide-code'
  }],
  // Link
  [{
    kind: 'link',
    icon: 'i-lucide-link'
  }],
  // Text alignment
  [{
    icon: 'i-lucide-align-justify',
    content: {
      align: 'end'
    },
    items: [{
      kind: 'textAlign',
      align: 'left',
      icon: 'i-lucide-align-left',
      label: 'Align Left'
    }, {
      kind: 'textAlign',
      align: 'center',
      icon: 'i-lucide-align-center',
      label: 'Align Center'
    }, {
      kind: 'textAlign',
      align: 'right',
      icon: 'i-lucide-align-right',
      label: 'Align Right'
    }, {
      kind: 'textAlign',
      align: 'justify',
      icon: 'i-lucide-align-justify',
      label: 'Align Justify'
    }]
  }]
]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Start typing..."
    :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] })]"
    class="min-h-64"
  >
    <UEditorToolbar :editor="editor" :items="toolbarItems" class="px-8" />
  </UEditor>
</template>
You can also pass an array of arrays to the items prop to create separated groups of items.
Each item can take an items array of objects with the same properties as the items prop to create a DropdownMenu.

Layout

Use the layout prop to change how the toolbar is displayed. Defaults to fixed.

LayoutDescription
fixedAlways visible toolbar, typically placed above the editor
bubbleContextual menu that appears when text is selected
floatingMenu that appears on empty lines or blocks
<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

defineProps<{
  layout: 'fixed' | 'bubble' | 'floating'
}>()

const value = ref(`Select this text to see the bubble menu appear.

Click on an empty line to see the floating menu.

`)

const items: EditorToolbarItem[][] = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Start typing...">
    <UEditorToolbar
      :editor="editor"
      :items="items"
      :layout="layout"
    />
  </UEditor>
</template>
The bubble and floating layouts use TipTap's BubbleMenu and FloatingMenu extensions. Check the TipTap documentation for advanced positioning options.

Should show

When using bubble or floating layouts, use the should-show prop to control when the toolbar appears. This function receives context about the editor state and returns a boolean.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`Select text that is longer than 10 characters to see the bubble menu.

Short text won't trigger the menu.`)

const toolbarItems: EditorToolbarItem[][] = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Start typing...">
    <UEditorToolbar
      :editor="editor"
      :items="toolbarItems"
      layout="bubble"
      :should-show="({ view, state }) => {
        const { selection } = state
        const { from, to } = selection
        const text = state.doc.textBetween(from, to)
        return view.hasFocus() && !selection.empty && text.length > 10
      }"
    />
  </UEditor>
</template>

Options

When using bubble or floating layouts, use the options prop to customize the positioning behavior using Floating UI options.

<template>
  <UEditorToolbar
    :editor="editor"
    :items="items"
    layout="bubble"
    :options="{
      placement: 'top',
      offset: 8,
      flip: { padding: 8 },
      shift: { padding: 8 }
    }"
  />
</template>

Color and variant

Use the color and variant props to customize the toolbar button styles.

<script setup lang="ts">
const editor = ref({})
const items = ref([
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: 'i-lucide-bold'
    }
  ]
])
</script>

<template>
  <UEditorToolbar color="primary" variant="soft" :editor="editor" :items="items" />
</template>

Use the active-color and active-variant props to customize the active state styling.

<script setup lang="ts">
const editor = ref({})
const items = ref([
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: 'i-lucide-bold'
    }
  ]
])
</script>

<template>
  <UEditorToolbar active-color="success" active-variant="solid" :editor="editor" :items="items" />
</template>

Size

Use the size prop to change the size of toolbar buttons. Defaults to sm.

<script setup lang="ts">
const editor = ref({})
const items = ref([
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: 'i-lucide-bold'
    }
  ]
])
</script>

<template>
  <UEditorToolbar size="md" :editor="editor" :items="items" />
</template>

Examples

Within a card

Place a fixed toolbar in a card header for a document-style editing interface.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`# Document Title

The toolbar is placed inside a card header, providing a document-style editing interface.

Try selecting text and using the formatting buttons above.`)

const toolbarItems: EditorToolbarItem[][] = [[{
  kind: 'undo',
  icon: 'i-lucide-undo'
}, {
  kind: 'redo',
  icon: 'i-lucide-redo'
}], [{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline'
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough'
}], [{
  icon: 'i-lucide-heading',
  items: [{
    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'
  }]
}, {
  kind: 'bulletList',
  icon: 'i-lucide-list'
}, {
  kind: 'orderedList',
  icon: 'i-lucide-list-ordered'
}, {
  kind: 'blockquote',
  icon: 'i-lucide-text-quote'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Start typing...">
    <div class="border border-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] overflow-hidden">
      <div class="bg-[var(--ui-bg-elevated)] border-b border-[var(--ui-border)] px-3 py-2">
        <UEditorToolbar :editor="editor" :items="toolbarItems" />
      </div>
      <div class="p-4" />
    </div>
  </UEditor>
</template>

Bubble menu on text selection

Use layout="bubble" to create a contextual toolbar that appears when text is selected.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`Select any text in this editor to see the **bubble menu** appear near your selection.

The menu automatically adjusts its position to stay visible on screen.`)

const toolbarItems: EditorToolbarItem[][] = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  kind: 'mark',
  mark: 'underline',
  icon: 'i-lucide-underline'
}, {
  kind: 'mark',
  mark: 'strike',
  icon: 'i-lucide-strikethrough'
}, {
  kind: 'mark',
  mark: 'code',
  icon: 'i-lucide-code'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Start typing...">
    <UEditorToolbar
      :editor="editor"
      :items="toolbarItems"
      layout="bubble"
    />
  </UEditor>
</template>
The bubble menu automatically positions itself near the selection and includes flip and shift middleware for optimal positioning.

Floating menu for empty blocks

Use layout="floating" to create a menu that appears on empty lines.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`Place your cursor on an empty line below to see the floating menu.


`)

const toolbarItems: EditorToolbarItem[][] = [[{
  kind: 'heading',
  level: 1,
  icon: 'i-lucide-heading-1'
}, {
  kind: 'heading',
  level: 2,
  icon: 'i-lucide-heading-2'
}, {
  kind: 'heading',
  level: 3,
  icon: 'i-lucide-heading-3'
}], [{
  kind: 'bulletList',
  icon: 'i-lucide-list'
}, {
  kind: 'orderedList',
  icon: 'i-lucide-list-ordered'
}, {
  kind: 'blockquote',
  icon: 'i-lucide-text-quote'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Start typing...">
    <UEditorToolbar
      :editor="editor"
      :items="toolbarItems"
      layout="floating"
    />
  </UEditor>
</template>
Combine with the should-show prop to control when the floating menu appears, such as only on empty paragraphs.

With custom button slots

You can use slots to customize specific toolbar items.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'
import EditorLinkPopover from '~/components/editor/EditorLinkPopover.vue'

const value = ref(`Select text and click the link button to add a link with the custom popover.

You can also edit existing links like [this one](https://ui.nuxt.com).`)

const toolbarItems: EditorToolbarItem[][] = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  slot: 'link'
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" placeholder="Start typing...">
    <UEditorToolbar :editor="editor" :items="toolbarItems">
      <template #link>
        <EditorLinkPopover :editor="editor" />
      </template>
    </UEditorToolbar>
  </UEditor>
</template>

Use the slot property on an item to specify a named slot, then provide a matching template:

<UEditorToolbar :editor="editor" :items="[[{ slot: 'link' }]]">
  <template #link>
    <MyCustomLinkButton :editor="editor" />
  </template>
</UEditorToolbar>

With dropdown menus

Create dropdown menus by adding an items property to toolbar items.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'
import TextAlign from '@tiptap/extension-text-align'

const value = ref('Use the dropdown menus to select heading levels or text alignment.')

const toolbarItems: EditorToolbarItem[][] = [[{
  icon: 'i-lucide-heading',
  items: [{
    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'
  }]
}, {
  icon: 'i-lucide-align-left',
  items: [{
    kind: 'textAlign',
    align: 'left',
    label: 'Align Left',
    icon: 'i-lucide-align-left'
  }, {
    kind: 'textAlign',
    align: 'center',
    label: 'Align Center',
    icon: 'i-lucide-align-center'
  }, {
    kind: 'textAlign',
    align: 'right',
    label: 'Align Right',
    icon: 'i-lucide-align-right'
  }]
}]]
</script>

<template>
  <UEditor v-slot="{ editor }" v-model="value" content-type="markdown" :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] })]" placeholder="Start typing...">
    <UEditorToolbar :editor="editor" :items="toolbarItems" />
  </UEditor>
</template>
Dropdowns automatically show the active child item's icon when an option is selected.

Image-specific toolbar

Create context-specific toolbars that appear only for certain node types.

<script setup lang="ts">
import type { EditorToolbarItem } from '@nuxt/ui'

const value = ref(`Click on the image below to see the image-specific toolbar:

![Nuxt UI Dashboard](https://ui.nuxt.com/assets/templates/nuxt/dashboard-dark.png)

The toolbar only appears when an image is selected.`)

const imageToolbarItems: EditorToolbarItem[][] = [[{
  icon: 'i-lucide-download',
  label: 'Download'
}, {
  icon: 'i-lucide-trash',
  label: 'Delete'
}]]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Start typing..."
  >
    <UEditorToolbar
      :editor="editor"
      :items="imageToolbarItems"
      layout="bubble"
      :should-show="({ editor, view }) => {
        return editor.isActive('image') && view.hasFocus()
      }"
    />
  </UEditor>
</template>

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

editorEditor
color'neutral' "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"

The color of the toolbar controls.

variant'ghost' "solid" | "outline" | "soft" | "subtle" | "ghost" | "link"

The variant of the toolbar controls.

activeColor'primary' "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"

The color of the active toolbar control.

activeVariant'soft' "solid" | "outline" | "soft" | "subtle" | "ghost" | "link"

The variant of the active toolbar control.

size'sm' "xs" | "sm" | "md" | "lg" | "xl"

The size of the toolbar controls.

items EditorToolbarItem<EditorCustomHandlers>[] | EditorToolbarItem<EditorCustomHandlers>[][]
layout'fixed' "fixed" | "floating" | "bubble"
ui { root?: ClassNameValue; base?: ClassNameValue; group?: ClassNameValue; separator?: ClassNameValue; }

Slots

Slot Type
default{}
item{ item: EditorToolbarItem<EditorCustomHandlers>; } & SlotPropsProps

Emits

Event Type

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    editorToolbar: {
      slots: {
        root: 'focus:outline-none',
        base: 'flex items-stretch gap-1.5',
        group: 'flex items-center gap-0.5',
        separator: 'w-px self-stretch bg-border'
      },
      variants: {
        layout: {
          bubble: {
            base: 'bg-default border border-default rounded-lg p-1'
          },
          floating: {
            base: 'bg-default border border-default rounded-lg p-1'
          },
          fixed: {
            base: ''
          }
        }
      }
    }
  }
})
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: {
        editorToolbar: {
          slots: {
            root: 'focus:outline-none',
            base: 'flex items-stretch gap-1.5',
            group: 'flex items-center gap-0.5',
            separator: 'w-px self-stretch bg-border'
          },
          variants: {
            layout: {
              bubble: {
                base: 'bg-default border border-default rounded-lg p-1'
              },
              floating: {
                base: 'bg-default border border-default rounded-lg p-1'
              },
              fixed: {
                base: ''
              }
            }
          }
        }
      }
    })
  ]
})

Changelog

No recent changes