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