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" 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"
    :extensions="[TextAlign.configure({ types: ['heading', 'paragraph'] })]"
    class="w-full min-h-21"
  >
    <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" class="w-full min-h-21">
    <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" class="w-full min-h-21">
    <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

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 = [[{
  kind: 'mark',
  mark: 'bold',
  icon: 'i-lucide-bold'
}, {
  kind: 'mark',
  mark: 'italic',
  icon: 'i-lucide-italic'
}, {
  slot: 'link' as const
}]] satisfies EditorToolbarItem[][]
</script>

<template>
  <UEditor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    class="w-full min-h-21"
  >
    <UEditorToolbar :editor="editor" :items="toolbarItems">
      <template #link>
        <EditorLinkPopover :editor="editor" auto-open />
      </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>

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"
    class="w-full min-h-21"
  >
    <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