Vue 3 components and composables for building rich text editors with tiptap
—
Interactive menu components that float above or beside editor content, providing contextual editing tools and actions.
A floating menu that appears when text is selected, providing quick access to formatting options.
/**
* Floating bubble menu that appears on text selection
* Uses Vue Teleport to render outside component tree
*/
const BubbleMenu: DefineComponent<{
/** Plugin identifier for registration */
pluginKey?: {
type: PropType<BubbleMenuPluginProps['pluginKey']>;
default: 'bubbleMenu';
};
/** Editor instance (required) */
editor: {
type: PropType<Editor>;
required: true;
};
/** Delay in milliseconds before updating position */
updateDelay?: {
type: PropType<number>;
default: undefined;
};
/** Delay in milliseconds before updating on resize */
resizeDelay?: {
type: PropType<number>;
default: undefined;
};
/** Additional plugin options */
options?: {
type: PropType<BubbleMenuPluginOptions>;
default: () => ({});
};
/** Custom function to determine when menu should show */
shouldShow?: {
type: PropType<(props: { editor: Editor; view: EditorView; state: EditorState; oldState?: EditorState; from: number; to: number; }) => boolean>;
default: null;
};
}>;
interface BubbleMenuPluginOptions {
/** Custom positioning element */
element?: HTMLElement;
/** Custom tippyjs options */
tippyOptions?: Record<string, any>;
/** Custom placement */
placement?: 'top' | 'bottom' | 'left' | 'right';
}Key Features:
Usage Examples:
<template>
<div v-if="editor">
<BubbleMenu :editor="editor" :updateDelay="100">
<div class="bubble-menu">
<button
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
Bold
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
Italic
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
Strike
</button>
</div>
</BubbleMenu>
<EditorContent :editor="editor" />
</div>
</template>
<script setup>
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
const editor = useEditor({
content: '<p>Select some text to see the bubble menu!</p>',
extensions: [StarterKit],
});
</script>
<style>
.bubble-menu {
display: flex;
background: white;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
overflow: hidden;
}
.bubble-menu button {
border: none;
background: none;
padding: 0.5rem;
cursor: pointer;
}
.bubble-menu button.is-active {
background: #e5e7eb;
}
</style>Custom shouldShow Logic:
<template>
<BubbleMenu
:editor="editor"
:shouldShow="customShouldShow"
>
<div class="custom-bubble-menu">
<!-- Menu content -->
</div>
</BubbleMenu>
</template>
<script setup>
const customShouldShow = ({ editor, view, state, from, to }) => {
// Only show for text selections (not node selections)
if (from === to) return false;
// Only show if selection contains bold text
return editor.isActive('bold');
};
</script>A floating menu that appears on empty lines, providing quick access to block-level formatting options.
/**
* Floating menu that appears on empty lines
* Uses Vue Teleport to render outside component tree
*/
const FloatingMenu: DefineComponent<{
/** Plugin identifier for registration */
pluginKey?: {
type: null;
default: 'floatingMenu';
};
/** Editor instance (required) */
editor: {
type: PropType<Editor>;
required: true;
};
/** Additional plugin options */
options?: {
type: PropType<FloatingMenuPluginOptions>;
default: () => ({});
};
/** Custom function to determine when menu should show */
shouldShow?: {
type: PropType<(props: { editor: Editor; view: EditorView; state: EditorState; oldState?: EditorState; }) => boolean>;
default: null;
};
}>;
interface FloatingMenuPluginOptions {
/** Custom positioning element */
element?: HTMLElement;
/** Custom tippyjs options */
tippyOptions?: Record<string, any>;
/** Custom placement */
placement?: 'top' | 'bottom' | 'left' | 'right';
}Key Features:
Usage Examples:
<template>
<div v-if="editor">
<FloatingMenu :editor="editor">
<div class="floating-menu">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
H1
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">
H2
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
Bullet List
</button>
<button @click="editor.chain().focus().toggleOrderedList().run()">
Ordered List
</button>
</div>
</FloatingMenu>
<EditorContent :editor="editor" />
</div>
</template>
<script setup>
import { useEditor, EditorContent, FloatingMenu } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
const editor = useEditor({
content: '<p></p>',
extensions: [StarterKit],
});
</script>
<style>
.floating-menu {
display: flex;
background: white;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
overflow: hidden;
}
.floating-menu button {
border: none;
background: none;
padding: 0.5rem;
cursor: pointer;
}
</style>Custom shouldShow for FloatingMenu:
<template>
<FloatingMenu
:editor="editor"
:shouldShow="customShouldShow"
>
<div class="floating-menu">
<!-- Menu content -->
</div>
</FloatingMenu>
</template>
<script setup>
const customShouldShow = ({ editor, view, state }) => {
const { selection } = state;
const { $anchor, empty } = selection;
// Only show on empty paragraphs
if (!empty) return false;
// Check if current node is a paragraph
if ($anchor.parent.type.name !== 'paragraph') return false;
// Only show if paragraph is empty
return $anchor.parent.textContent === '';
};
</script>Advanced Styling:
<template>
<BubbleMenu
:editor="editor"
:options="{
placement: 'top',
tippyOptions: {
duration: 100,
animation: 'shift-away'
}
}"
>
<div class="menu-container">
<!-- Menu buttons -->
</div>
</BubbleMenu>
</template>
<style>
.menu-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
padding: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
}
</style>Multiple Menu Integration:
<template>
<div v-if="editor">
<!-- Bubble menu for text formatting -->
<BubbleMenu :editor="editor" pluginKey="textBubbleMenu">
<div class="text-menu">
<button @click="toggleBold">Bold</button>
<button @click="toggleItalic">Italic</button>
</div>
</BubbleMenu>
<!-- Floating menu for block elements -->
<FloatingMenu :editor="editor" pluginKey="blockFloatingMenu">
<div class="block-menu">
<button @click="addHeading">Heading</button>
<button @click="addList">List</button>
</div>
</FloatingMenu>
<EditorContent :editor="editor" />
</div>
</template>interface BubbleMenuPluginProps {
pluginKey: string | PluginKey;
editor: Editor;
element: HTMLElement;
updateDelay?: number;
resizeDelay?: number;
options?: Record<string, any>;
shouldShow?: ((props: {
editor: Editor;
view: EditorView;
state: EditorState;
oldState?: EditorState;
from: number;
to: number;
}) => boolean) | null;
}
interface FloatingMenuPluginProps {
pluginKey: string | PluginKey;
editor: Editor;
element: HTMLElement;
options?: Record<string, any>;
shouldShow?: ((props: {
editor: Editor;
view: EditorView;
state: EditorState;
oldState?: EditorState;
}) => boolean) | null;
}Install with Tessl CLI
npx tessl i tessl/npm-tiptap--vue-3