A powerful, feature-rich Vue 3 rich text editor component inspired by Notion, It delivers a complete editing experience with slash commands, context menus, drag-and-drop support, advanced formatting, enhanced tables, and image upload capabilities.
- Rich Text Formatting: Bold, italic, underline, strikethrough, code, and color formatting
- Advanced Block Types: Headings, blockquotes, code blocks, lists, and task lists
- Table Support: Create, edit, and manipulate tables with advanced cell & row operations, including interactive drag-to-add/remove rows.
- Image Handling: Drag-and-drop image uploads with customizable upload functions.
- File Handler: Smart paste handling for images and rich content
- Image Resizing & Alignment: Smoothly resize images with interactive handles and align them left, right, or center.
- Drag & Drop: Reorder content blocks with visual drag handles
- Slash Commands: Quick content insertion with "/" command menu
- Link Management: Smart link insertion and editing with bubble menu
- Text Alignment: Left, center, right, and justify alignment options
- Bubble Menu: Context-sensitive formatting toolbar
- Undo/Redo: Full history management with keyboard shortcuts
- Task Lists: Interactive checkboxes and task management
- Customizable: Flexible styling with CSS variables and custom themes
- TypeScript Support: Fully typed for enhanced developer experience
# npm npm install v-notion-editor # yarn yarn add v-notion-editor # pnpm pnpm add v-notion-editor
<script setup> import { ref } from 'vue' import { NotionEditor, NotionToolbar } from 'v-notion-editor' import 'v-notion-editor/style.css' const content = ref('<p>Start writing your content here...</p>') const handleImageUpload = async (files) => { // Implement your image upload logic const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result) reader.onerror = reject reader.readAsDataURL(file) }) } return await Promise.all(files.map(fileToBase64)) } </script> <template> <div> <NotionToolbar /> <NotionEditor v-model="content" :image-upload-options="{ onUpload: handleImageUpload, maxFileSize: 10 * 1024 * 1024, // 10MB quality: 0.85, }" /> </div> </template>
You must import the CSS styles:
import 'v-notion-editor/style.css'
The main editor component that provides the rich text editing interface.
Prop | Type | Default | Description |
---|---|---|---|
modelValue |
string |
'' |
The HTML content of the editor |
imageUploadOptions |
ImageUploadOptions |
{} |
Configuration for image upload handling |
Event | Parameters | Description |
---|---|---|
update:modelValue |
value: string |
Emitted when content changes |
onFocus |
- | Emitted when editor gains focus |
onBlur |
- | Emitted when editor loses focus |
interface ImageUploadOptions { onUpload?: (files: File[]) => Promise<string[]> maxFileSize?: number // in bytes, default: 10MB quality?: number // 0-1, default: 0.85 allowedTypes?: string[] // default: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'] onOpenFileDialog?: (multiple: boolean) => Promise<string[] | File[]> // custom file picker }
A comprehensive toolbar component with formatting options.
Prop | Type | Default | Description |
---|---|---|---|
visibleToolbars |
string[] |
All tools | Array of toolbar items to display |
headings
, bold
, italic
, underline
, strike
, code
, color
, link
, bulletList
, orderedList
, taskList
, blockquote
, table
, align
, image
, divider
, undo
, redo
<script setup> import { NotionEditor, NotionToolbar } from 'v-notion-editor' const customToolbars = ['headings', 'bold', 'italic', 'bulletList', 'orderedList', 'link', 'image'] </script> <template> <div> <NotionToolbar :visible-toolbars="customToolbars" /> <NotionEditor v-model="content" /> </div> </template>
<script setup> import { ref, onMounted } from 'vue' import { NotionEditor } from 'v-notion-editor' const editorRef = ref(null) const insertCustomContent = () => { const editor = editorRef.value?.editor if (editor) { editor.chain().focus().insertContent('<p>Custom inserted content!</p>').run() } } const getEditorContent = () => { const editor = editorRef.value?.editor if (editor) { console.log('HTML:', editor.getHTML()) console.log('JSON:', editor.getJSON()) console.log('Text:', editor.getText()) } } </script> <template> <div> <button @click="insertCustomContent">Insert Content</button> <button @click="getEditorContent">Get Content</button> <NotionEditor ref="editorRef" v-model="content" /> </div> </template>
To display editor content with the same styling in read-only mode, wrap your content with the v-notion-editor
class:
<script setup> import 'v-notion-editor/style.css' const savedContent = ref(` <h1>My Document Title</h1> <p>This content will have the same styling as in the editor.</p> <blockquote>Important note with proper formatting</blockquote> `) </script> <template> <!-- Read-only content with editor styling --> <div class="v-notion-editor"> <div v-html="savedContent" class="read-mode" /> </div> </template>
Shortcut | Action |
---|---|
Ctrl/Cmd + B |
Bold |
Ctrl/Cmd + I |
Italic |
Ctrl/Cmd + U |
Underline |
Ctrl/Cmd + Shift + S |
Strikethrough |
Ctrl/Cmd + E |
Code |
Ctrl/Cmd + K |
Link |
Ctrl/Cmd + Z |
Undo |
Ctrl/Cmd + Shift + Z |
Redo |
Ctrl/Cmd + Shift + L |
Bullet List |
Ctrl/Cmd + Shift + O |
Ordered List |
Ctrl/Cmd + Enter |
Hard Break |
/ |
Slash command menu |
Type /
to access quick content insertion:
Customize the editor appearance using CSS variables:
:root { /* Editor Base */ --editor-bg: #ffffff; --editor-color: #000000; --editor-primary: #dbeafe; --editor-primary-fg: #1d4ed8; --editor-secondary: #f7f6f4; --editor-secondary-dark: #c2c2bd; --editor-border-color: #ececea; --editor-btn-size: 1.7em; --editor-btn-icon-size: calc(var(--editor-btn-size) - 0.7em); --editor-border-radius: 0.5rem; --editor-shadow: 0 4px 80px -4px rgba(0, 0, 0, 0.088); --spacing-btn-size: var(--editor-btn-size); --radius: var(--editor-border-radius); /* Content Colors */ --editor-content-color-default: #37352f; --editor-content-color-gray: #989898; --editor-content-color-orange: #ea580c; --editor-content-color-yellow: #ecb802; --editor-content-color-green: #16a34a; --editor-content-color-blue: #2563eb; --editor-content-color-purple: #9333ea; --editor-content-color-pink: #f64f9f; --editor-content-color-red: #e0383e; --editor-content-color-white: #ffffff; --editor-content-color-heading: #292929; --editor-content-color-text: #37352f; --editor-content-color-text-secondary: #4f4f4f; --editor-content-color-code: #e3342f; --editor-content-color-code-block: #272727; --editor-content-color-mark: #151413; --editor-content-color-primary: #007bff; --editor-content-color-primary-dark: #007bff; --editor-content-color-primary-dark-light: #007bff; --editor-content-color-border: #c8c8c6; --editor-content-color-border-dark: #d0d0ce; /* Content Background Colors */ --editor-content-bg-default: rgba(55, 53, 47, 0.12); --editor-content-bg-gray: #d5d7d8; --editor-content-bg-yellow: #ffebb4; --editor-content-bg-yellow-light: #f3f1eb; --editor-content-bg-green: #cbecbf; --editor-content-bg-blue: #b4d6ff; --editor-content-bg-purple: rgba(105, 64, 165, 0.4); --editor-content-bg-pink: #fecbe2; --editor-content-bg-red: #ff9898; --editor-content-bg-black: #000000; --editor-content-bg-secondary: #f7f6f4; --editor-content-bg-mark: #fef08a; --editor-content-bg-primary: #007bff; --editor-content-bg-primary-dark: #0a77ec; --editor-content-bg-primary-light: #eff5fd; --editor-content-bg-primary-fg: #ffffff; /* Headings */ --size-font-h1: 2.15em; --size-font-h2: 1.65em; --size-font-h3: 1.4em; --size-font-h4: 1.35em; --size-font-h5: 1.3em; --size-font-h6: 1.125em; --size-line-height-heading: 1.25; /* Table */ --size-font-table: 1.1em; --size-padding-table: 8px 12px; --size-padding-table-cell: 0.5em 0.75em; --size-table-cell-min-width: 100px; --z-index-selected-cell: 2; /* Code */ --size-font-code: 0.875em; /* Fonts */ --font-family-base: 'Roboto', Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --size-font-base: 15px; --size-font-sm: 0.875em; --size-font-xs: 0.75em; /* Spacing / Padding / Margin */ --size-spacing-sm: 0.25rem; --size-spacing-md: 0.5rem; --size-spacing-lg: 0.75rem; --size-spacing-xl: 1rem; --size-spacing-2xl: 1.5rem; --size-spacing-3xl: 2rem; --size-padding-sm: 0.125rem; --size-padding-md: 0.375rem; --size-padding-lg: 0.75rem; --size-padding-xl: 1rem; --size-margin-sm: 0.2em; --size-margin-md: 0.58em; --size-margin-lg: 0.88em; --size-margin-xl: 1.18em; --size-margin-2xl: 1.45em; /* Borders */ --size-border-radius-sm: 0.25rem; --size-border-radius-md: 0.375rem; --size-border-radius-lg: 0.5rem; --size-border-radius-xl: 1px; --size-border-width-sm: 0.95px; --size-border-width-md: 2px; --size-border-width-lg: 3px; /* Line Heights */ --size-line-height-base: 1.6; --size-line-height-list: 1.6; --size-line-height-list-marker: 1.4; /* Misc Sizes */ --size-cursor-width: 20px; --size-resize-handle: 4px; --size-drag-handle-width: 1.03rem; --size-drag-handle-height: 1.4rem; }
If you encounter a bug, miss a feature, or want to suggest an enhancement: Open an issue on GitHub: issues Provide a clear description, steps to reproduce, and screenshots if possible. For feature requests, describe your use case and expected behavior.
MIT