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>
Use the items prop as an array of objects with the following properties:
label?: stringicon?: stringcolor?: "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?: booleanloading?: booleanactive?: booleanslot?: stringonClick?: (e: MouseEvent) => voiditems?: EditorToolbarItem[] | EditorToolbarItem[][]class?: anykind 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.kind property for editor-specific actions, additional properties may be required:kind: "mark": mark: "bold" | "italic" | "strike" | "code" | "underline"kind: "textAlign": align: "left" | "center" | "right" | "justify"kind: "heading": level: 1 | 2 | 3 | 4 | 5 | 6kind: "link": href?: stringkind: "image": src?: stringkind: "duplicate" | "delete" | "moveUp" | "moveDown": pos: numberkind: "clearFormatting" | "suggestion": pos?: numberYou 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>
items prop to create separated groups of items.items array of objects with the same properties as the items prop to create a DropdownMenu.Use the layout prop to change how the toolbar is displayed. Defaults to fixed.
| Layout | Description |
|---|---|
fixed | Always visible toolbar, typically placed above the editor |
bubble | Contextual menu that appears when text is selected |
floating | Menu 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>
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>
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>
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>
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>
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>
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>
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>
should-show prop to control when the floating menu appears, such as only on empty paragraphs.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>
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>
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:

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>
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
editor | Editor | |
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; } |
| Slot | Type |
|---|---|
default | {} |
item | { item: EditorToolbarItem<EditorCustomHandlers>; } & SlotPropsProps |
| Event | Type |
|---|
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: ''
}
}
}
}
}
})
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: ''
}
}
}
}
}
})
]
})