✍️ Rich Formatting
Complete text formatting options including bold, italic, underline, links, lists, and headings
A modern rich text editor component built on TipTap, providing complete text formatting functionality and an extensible plugin system. Perfectly integrated with the Vue 3 ecosystem, supporting two-way data binding and custom configuration.
View Live Demo
✍️ Rich Formatting
Complete text formatting options including bold, italic, underline, links, lists, and headings
🧩 Extensible Plugin System
Modular architecture based on TipTap, supports custom plugins and command extensions
🎨 Theme & Style Customization
Complete theme system with dark mode support and custom style configuration
🛡️ TypeScript Support
Complete type definitions ensuring type safety and excellent development experience
📊 Character Count & Limits
Built-in character statistics with character limit support and real-time notifications
♿ Accessibility Support
WCAG compliant with complete keyboard navigation and screen reader support
The simplest rich text editor integration:
<template><div class="space-y-4"> <!-- Basic Rich Text Editor --> <div> <label class="block text-sm font-medium mb-2"> Article Content </label> <RichTextEditor v-model="articleContent" placeholder="Start writing your article..." :min-height="'200px'" :character-limit="5000" :show-character-count="true" @change="handleContentChange" /> </div>
<!-- Preview Area --> <div v-if="articleContent" class="mt-6"> <h3 class="text-lg font-semibold mb-2">Preview</h3> <div class="prose prose-gray dark:prose-invert max-w-none p-4 border border-gray-200 dark:border-gray-700 rounded-lg" v-html="articleContent" /> </div></div></template>
<script setup lang="ts">import { ref } from 'vue'import { RichTextEditor } from '@/components/RichTextEditor'
// Article contentconst articleContent = ref(`<h2>Welcome to Rich Text Editor</h2><p>This is a powerful rich text editor supporting multiple formatting options:</p><ul><li><strong>Bold text</strong></li><li><em>Italic text</em></li><li><u>Underlined text</u></li><li><a href="https://example.com">Hyperlinks</a></li></ul><p>You can easily create professional content editing experiences.</p>`)
// Content change handlerconst handleContentChange = (content: string) => {console.log('Content updated:', content)// Auto-save functionality can be implemented here// autoSave(content)}</script>
Using complete configuration options and custom features:
<template><div class="space-y-6"> <!-- Advanced Configuration Editor --> <div class="border border-gray-200 dark:border-gray-700 rounded-lg"> <div class="p-4 bg-gray-50 dark:bg-gray-800 border-b"> <h3 class="font-semibold">Email Editor</h3> <p class="text-sm text-gray-600 dark:text-gray-400"> Rich text email editing functionality </p> </div>
<div class="p-4"> <RichTextEditor v-model="emailContent" :editable="!isSending" :show-toolbar="true" :toolbar-position="'top'" :auto-focus="true" :character-limit="10000" :show-character-count="true" :min-height="'300px'" :max-height="'500px'" placeholder="Write your email content..." class="email-editor" @focus="handleEditorFocus" @blur="handleEditorBlur" @character-count="handleCharacterCount" />
<div class="flex items-center justify-between mt-4"> <div class="text-sm text-gray-500"> <span v-if="characterCount > 0"> {{ characterCount }} characters <span v-if="characterCount > 8000" class="text-orange-500"> (recommended to keep under 8000 characters) </span> </span> </div>
<div class="flex gap-2"> <Button @click="saveDraft" variant="outline" size="sm" :disabled="isSending" > Save Draft </Button> <Button @click="sendEmail" :loading="isSending" size="sm" > {{ isSending ? 'Sending...' : 'Send Email' }} </Button> </div> </div> </div> </div>
<!-- Editor Status Display --> <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg"> <div class="text-sm font-medium text-blue-900 dark:text-blue-100"> Edit Status </div> <div class="text-blue-700 dark:text-blue-300"> {{ editorFocused ? 'Editing' : 'Inactive' }} </div> </div>
<div class="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg"> <div class="text-sm font-medium text-green-900 dark:text-green-100"> Character Count </div> <div class="text-green-700 dark:text-green-300"> {{ characterCount }} / 10,000 </div> </div>
<div class="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-lg"> <div class="text-sm font-medium text-purple-900 dark:text-purple-100"> Auto Save </div> <div class="text-purple-700 dark:text-purple-300"> {{ lastSaved ? `${lastSaved} saved` : 'Not saved' }} </div> </div> </div></div></template>
<script setup lang="ts">import { ref } from 'vue'import { Button } from '@/components/ui/button'import { RichTextEditor } from '@/components/RichTextEditor'import { toast } from 'vue-sonner'
// Email contentconst emailContent = ref('')const characterCount = ref(0)const editorFocused = ref(false)const isSending = ref(false)const lastSaved = ref('')
// Editor event handlersconst handleEditorFocus = () => {editorFocused.value = true}
const handleEditorBlur = () => {editorFocused.value = false// Auto-save draft on blursaveDraft()}
const handleCharacterCount = (count: number) => {characterCount.value = count}
// Save draftconst saveDraft = async () => {if (!emailContent.value.trim()) return
try { // Simulate API call await new Promise(resolve => setTimeout(resolve, 500))
lastSaved.value = new Date().toLocaleTimeString() toast.success('Draft saved')} catch (error) { toast.error('Save failed, please try again')}}
// Send emailconst sendEmail = async () => {if (!emailContent.value.trim()) { toast.error('Please enter email content') return}
isSending.value = truetry { // Simulate sending email await new Promise(resolve => setTimeout(resolve, 2000))
toast.success('Email sent successfully!') emailContent.value = '' characterCount.value = 0 lastSaved.value = ''} catch (error) { toast.error('Send failed, please try again')} finally { isSending.value = false}}</script>
<style>.email-editor {--editor-max-width: none;--editor-font-size: 14px;}</style>
Property | Type | Default | Description |
---|---|---|---|
modelValue | string | ” | Editor content (HTML format) |
placeholder | string | ’Start typing…’ | Placeholder text |
editable | boolean | true | Whether the editor is editable |
showToolbar | boolean | true | Whether to show toolbar |
toolbarPosition | 'top' | 'bottom' | 'floating' | ’top’ | Toolbar position |
minHeight | string | - | Minimum height (e.g., ‘200px’) |
maxHeight | string | - | Maximum height (e.g., ‘500px’) |
characterLimit | number | - | Character limit |
showCharacterCount | boolean | false | Whether to show character count |
autoFocus | boolean | false | Whether to auto-focus |
class | string | - | Custom CSS class name |
Event | Parameters | Description |
---|---|---|
update:modelValue | value: string | Emitted when content updates |
change | value: string | Emitted when content changes |
focus | - | Emitted when editor gains focus |
blur | - | Emitted when editor loses focus |
character-count | count: number | Emitted when character count changes |
Access component methods through template ref:
<template><div> <RichTextEditor ref="editorRef" v-model="content" placeholder="Enter content..." />
<div class="mt-4 flex gap-2"> <Button @click="focusEditor">Focus Editor</Button> <Button @click="clearContent">Clear Content</Button> <Button @click="insertTemplate">Insert Template</Button> <Button @click="getStats">Get Statistics</Button> </div></div></template>
<script setup lang="ts">import { ref } from 'vue'import type { RichTextEditorInstance } from '@/components/RichTextEditor/types'
const editorRef = ref<RichTextEditorInstance>()const content = ref('')
// Focus editorconst focusEditor = () => {editorRef.value?.focus()}
// Clear contentconst clearContent = () => {editorRef.value?.clear()}
// Insert template contentconst insertTemplate = () => {const template = ` <h2>Meeting Notes</h2> <p><strong>Date:</strong> {{ date }}</p> <p><strong>Attendees:</strong> {{ attendee list }}</p> <h3>Meeting Content</h3> <ul> <li>Topic 1:</li> <li>Topic 2:</li> </ul>`editorRef.value?.setContent(template)}
// Get editor statisticsconst getStats = () => {if (editorRef.value) { const htmlContent = editorRef.value.getHTML() const textContent = editorRef.value.getText() const charCount = editorRef.value.getCharacterCount()
console.log({ html: htmlContent, text: textContent, characters: charCount, words: textContent.split(/s+/).length })}}</script>
Perfect for CMS platforms requiring rich content editing capabilities with complete formatting control.
Ideal for email clients, messaging systems, and communication tools requiring formatted text input.
Excellent for documentation platforms, note-taking applications, and knowledge management systems.
/* Custom editor theme */.custom-editor {--editor-bg: theme('colors.gray.50');--editor-text: theme('colors.gray.900');--editor-border: theme('colors.gray.200');--editor-focus-ring: theme('colors.blue.500');}
.dark .custom-editor {--editor-bg: theme('colors.gray.800');--editor-text: theme('colors.gray.100');--editor-border: theme('colors.gray.600');--editor-focus-ring: theme('colors.blue.400');}
/* Toolbar customization */.custom-editor .rich-text-editor__toolbar {background: var(--editor-bg);border: 1px solid var(--editor-border);border-radius: 8px;padding: 8px;margin-bottom: 8px;}
/* Content area customization */.custom-editor .rich-text-editor__content {background: var(--editor-bg);color: var(--editor-text);border: 1px solid var(--editor-border);border-radius: 8px;padding: 16px;font-family: 'Inter', sans-serif;line-height: 1.6;}
.custom-editor .rich-text-editor__content:focus-within {outline: none;box-shadow: 0 0 0 2px var(--editor-focus-ring);}
/* Character count styling */.custom-editor .rich-text-editor__character-count {text-align: right;font-size: 12px;color: theme('colors.gray.500');margin-top: 4px;}
.custom-editor .rich-text-editor__character-count--warning {color: theme('colors.orange.500');}
.custom-editor .rich-text-editor__character-count--error {color: theme('colors.red.500');}
/* Content styling */.custom-editor .ProseMirror h1 {font-size: 1.5em;font-weight: 700;margin: 1em 0 0.5em;}
.custom-editor .ProseMirror h2 {font-size: 1.25em;font-weight: 600;margin: 0.8em 0 0.4em;}
.custom-editor .ProseMirror p {margin: 0.5em 0;}
.custom-editor .ProseMirror ul,.custom-editor .ProseMirror ol {margin: 0.5em 0;padding-left: 1.2em;}
.custom-editor .ProseMirror blockquote {border-left: 3px solid theme('colors.gray.300');padding-left: 1em;margin: 1em 0;color: theme('colors.gray.600');}
.custom-editor .ProseMirror code {background: theme('colors.gray.100');padding: 2px 4px;border-radius: 3px;font-size: 0.9em;}
.dark .custom-editor .ProseMirror code {background: theme('colors.gray.700');}
/* Link styling */.custom-editor .ProseMirror a {color: theme('colors.blue.600');text-decoration: underline;text-decoration-thickness: 1px;text-underline-offset: 2px;}
.custom-editor .ProseMirror a:hover {color: theme('colors.blue.700');}
/* Placeholder styling */.custom-editor .ProseMirror p.is-editor-empty:first-child::before {content: attr(data-placeholder);float: left;color: theme('colors.gray.400');pointer-events: none;height: 0;}
// ✅ Recommended: Content validation and cleaningconst validateAndCleanContent = (content: string) => {// 1. Basic validationif (!content || typeof content !== 'string') { return ''}
// 2. HTML security handling (XSS prevention)const cleanContent = DOMPurify.sanitize(content, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'], ALLOWED_ATTR: ['href', 'target', 'rel']})
// 3. Remove empty tagsreturn cleanContent.replace(/<p></p>/g, '').trim()}
// ✅ Recommended: Content length calculationconst getContentLength = (htmlContent: string) => {// Create temporary DOM element to get plain textconst temp = document.createElement('div')temp.innerHTML = htmlContentreturn temp.textContent?.length || 0}
// ❌ Avoid: Using unvalidated content directlyconst badExample = (userContent: any) => {// Direct use may cause security issuesreturn userContent}
// ✅ Recommended: Debounced saving and performance optimizationimport { debounce } from 'lodash-es'
const debouncedSave = debounce((content: string) => {// Only save when content actually changesif (content !== lastSavedContent.value) { saveContent(content) lastSavedContent.value = content}}, 1000)
// ✅ Recommended: Large content lazy loadingconst loadEditor = async () => {if (contentLength > 50000) { // Load large content in chunks const chunks = splitContentIntoChunks(content.value) for (const chunk of chunks) { await nextTick() appendChunk(chunk) }} else { // Load small content directly editor.value?.setContent(content.value)}}
// ❌ Avoid: Frequent content updatesconst badExample = () => {// Saving on every input affects performancewatch(content, (newContent) => { saveContent(newContent) // Not recommended})}