rte-vue
v2.2.4
Published
A fully configurable, highly intuitive rich text editor component for Vue 3. Built with TipTap and TypeScript, supporting Markdown and JSON output.
Maintainers
Readme
rte-vue
A fully configurable, ready-to-use Rich Text Editor for Vue 3. Built on TiPTap, this editor provides a seamless WYSIWYG experience with Markdown support, customization slots, and zero-config usage.
Perfect for blogs, CMS, comment sections, or any Vue 3 app needing text editing.
✨ Features
- 🚀 Zero-Config: Just drop it in and it works with
v-model. - 📝 Markdown & JSON: Native support for Markdown input/output (or use JSON).
- 🎨 Component-Scoped Styles: Looks good out of the box, but easily overridable.
- 🔧 Highly Customizable:
- Configure toolbar items and groups.
- Override specific buttons or the entire toolbar via Slots.
- Custom styling for every element.
- 🧱 Block Styling: Headings, lists, links, bold, italic, strike-through out of the box.
- 📎 File Uploader: Import content from
.docx,.odt,.odf, and.txtfiles with a single prop. - 💾 File Downloader: Save as
.md,.html,.txt,.docx,.odt, or.odf— all built-in. Plus an optional dropdown format picker and anonDownloadhook for any custom format. - 📋 Copy to Clipboard: One-click copy of the editor's content to the system clipboard.
- 🦾 TypeScript: Full TypeScript support with exported types.
� Examples
Check out the examples folder for detailed usage scenarios, including:
- Basic Usage
- Custom Toolbar Groups
- Theming with Tailwind CSS
- Markdown Support
- Custom Toolbar Buttons (Slots)
- Form Validation Integration7. Internationalization (i18n)
- Read-Only Display
- Chat / Comment Input
- Programmatic Control
�📦 Installation
npm install rte-vue⚡ Quick Start
The "It Just Works" Way
<script setup lang="ts">
import { ref } from 'vue';
import { RTextEditor } from 'rte-vue';
import 'rte-vue/style.css';
const content = ref('# Hello World');
</script>
<template>
<RTextEditor v-model="content" />
</template>Full Configuration Example
<script setup lang="ts">
import { ref } from 'vue';
import { RTextEditor } from 'rte-vue';
import 'rte-vue/style.css';
const myContent = ref('');
const handleSave = (content: string) => {
console.log('Saved content:', content);
};
</script>
<template>
<RTextEditor
v-model="myContent"
placeholder="Write something amazing..."
content-format="markdown"
:heading-offset="1"
:toolbar-items="['bold', 'italic', 'heading2', 'bulletList', 'link']"
@save="handleSave"
/>
</template>📖 Component API
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue / text | string | "" | The content of the editor. Supports v-model. |
| contentFormat | 'html' \| 'markdown' | 'html' | Format for input and output strings. |
| showToolbar | boolean | true | Visibility of the toolbar. |
| disabled | boolean | false | Disables editing. |
| placeholder | string | - | Placeholder text when empty. |
| headingOffset | number | 0 | Offsets heading levels (e.g., +1 makes H1 render as H2). |
| toolbarItems | ToolbarItem[] | ['bold', ...] | Array of button keys to show. |
| toolbarGroups | ToolbarItem[][] | - | Advanced: Group buttons into visual clusters. |
| toolbarLabels | Record<string, string> | - | Custom labels/tooltips for buttons. |
| classNames | object | - | Object of class strings for root, editor, button, etc. |
| minHeight | string | - | CSS min-height (e.g., '200px'). |
| maxHeight | string | - | CSS max-height. |
| width | string | - | CSS width. |
| uploader | boolean | false | Enables file uploading: adds an upload toolbar button and allows drag-and-drop / paste of .docx, .odt, .odf, and .txt files onto the editor. |
| downloader | boolean | false | Enables file downloading: adds a download toolbar button that saves the editor's current content to a file. |
| downloadFilename | string | "document" | Default filename (without extension) suggested to the browser when downloading. |
| downloadFormats | DownloadFormat[] | — | When 2+ entries are given, the download button becomes a dropdown of format choices. Built-in IDs (markdown, html, txt) are saved automatically; other IDs require an onDownload handler. With 0 or 1 entry, the button is a single-click action. |
| copyable | boolean | false | Adds a copy toolbar button that copies the editor's current content to the clipboard. |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | string | Emitted on every change (v-model). |
| change | string | Emitted on every change. |
| save | string | Emitted when user presses Cmd+S or Ctrl+S. |
| focus | - | Emitted when editor gains focus. |
| blur | - | Emitted when editor loses focus. |
| file-upload | (file: File, content: string) | Emitted after a file is successfully parsed. |
| file-error | (error: Error) | Emitted when file parsing fails. |
| download | ({ content, filename, format }) | Emitted when the user triggers a download. Return false to suppress the default browser save. |
| copy | ({ content, format }) | Emitted when the user triggers a copy. Return false to suppress the default clipboard write. |
Slots
#toolbar
Completely replace the toolbar area.
Scope: { items, groups, isActive(item), isDisabled(item), runCommand(item) }
#toolbar-button
Customize individual buttons while keeping the default layout.
Scope: { item, label, active, disabled, runCommand }
🖌️ Styling
The editor comes with minimalist base styles. You can override specific parts using the classNames prop or standard CSS.
Using Utility Classes (Tailwind, UnoCSS, etc)
Because the default styles use the :where() selector (zero specificity), you can pass utility classes directly to the component via the classNames prop and they will work immediately without !important.
<RTextEditor
:class-names="{
editorContent: 'bg-zinc-50 border-2 border-red-500 rounded-xl',
toolbar: 'bg-transparent border-b-0',
buttonActive: 'bg-blue-100 text-blue-700'
}"
/>CSS Classes & Selectors
If you prefer writing custom CSS, you can target the following stable class names.
| Class Name | Description |
|------------|-------------|
| .rte-root | The top-level container element. |
| .rte-header | The header row containing the icon and title. |
| .rte-title | The header title text. |
| .rte-icon | The header icon svg. |
| .rte-toolbar | The Flex container for the toolbar area. |
| .rte-toolbar-group | The container for a group of related buttons. |
| .rte-button | The toolbar button element. |
| .rte-button-active | The active state of a toolbar button. |
| .rte-editor | The wrapper around the editor content area. |
| .rte-editor-content | The actual editable area (TipTap/ProseMirror container). |
| .rte-placeholder | The absolute positioned placeholder text. |
| .rte-drop-overlay | The overlay shown when dragging a file over the editor. |
| .rte-download-menu | The dropdown panel shown when the Download button has multiple formats. |
| .rte-download-menu-item | An entry within the download dropdown. |
Since all defaults use :where(.classname), your custom CSS will always win as long as it has a specificity greater than 0 (which is basically any class selector).
/* No !important needed! */
.rte-editor-content {
background-color: #fdf2f8;
border-radius: 12px;
}📎 File Uploader
Enable the built-in file uploader to let users import content from .docx, .odt, .odf, and .txt files directly into the editor.
Basic Usage
<script setup lang="ts">
import { ref } from 'vue';
import { RTextEditor } from 'rte-vue';
import 'rte-vue/style.css';
const content = ref('');
const onFileUpload = (file: File, html: string) => {
console.log(`Imported "${file.name}" successfully`);
};
const onFileError = (error: Error) => {
console.error('File import failed:', error.message);
};
</script>
<template>
<RTextEditor
v-model="content"
:uploader="true"
placeholder="Upload a file or start typing..."
@file-upload="onFileUpload"
@file-error="onFileError"
/>
</template>When uploader is set to true, three ways to import a file are enabled:
- Toolbar button — An
Uploadbutton appears in the toolbar. Clicking it opens a file picker. - Drag and drop — Drag a supported file directly onto the editor. A drop overlay will appear to confirm the target.
- Paste — Copy a file to the clipboard and paste it into the editor (Cmd/Ctrl+V).
Once a file is imported, its content populates the editor and can be further edited.
Supported File Types
| Extension | Description | Dependency |
|-----------|-------------|------------|
| .docx | Microsoft Word documents | mammoth (bundled) |
| .odt | OpenDocument Text files | jszip (bundled) |
| .odf | OpenDocument Format files | jszip (bundled) |
| .txt | Plain text files | None |
Using parseDocumentFile Standalone
The file parser is also exported for use outside of the component:
import { parseDocumentFile } from 'rte-vue';
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
const html = await parseDocumentFile(file);
console.log(html);
});💾 File Downloader
Enable the built-in downloader to let users save the editor's current content to a file. The output format mirrors the editor's contentFormat prop: html → .html (MIME text/html), markdown → .md (MIME text/markdown).
Basic Usage
<script setup lang="ts">
import { ref } from 'vue';
import { RTextEditor } from 'rte-vue';
import 'rte-vue/style.css';
const content = ref('# Hello World');
</script>
<template>
<RTextEditor
v-model="content"
content-format="markdown"
:downloader="true"
download-filename="my-document"
/>
</template>When downloader is set to true, a Download button appears in the toolbar. Clicking it triggers the standard browser save dialog with the editor's content. The button is disabled while the editor is empty.
Multiple Formats (Dropdown)
Pass downloadFormats with 2+ entries to turn the Download button into a dropdown menu of format choices. The following IDs are built-in and saved automatically:
| id | Output | MIME | Notes |
|------|--------|------|-------|
| markdown | editor content as markdown | text/markdown | via Turndown |
| html | editor content as HTML | text/html | as-is from editor.getHTML() |
| txt | plain text | text/plain | HTML stripped, paragraph breaks preserved |
| docx | Microsoft Word | application/vnd.openxmlformats-officedocument.wordprocessingml.document | preserves headings (1–6), bold/italic/strike/underline, hyperlinks, bullet + numbered lists (nested sublists keep their indent + per-level numbering / bullet glyph). jszip is lazy-loaded. |
| odt | OpenDocument Text | application/vnd.oasis.opendocument.text | preserves headings (1–6), bold/italic/strike/underline, hyperlinks, bullet + numbered lists (nested sublists keep their indent + per-level numbering / bullet glyph). jszip is lazy-loaded. |
| odf | OpenDocument Format | application/vnd.oasis.opendocument.text | same builder as odt, different extension. |
Any other ID is treated as host-handled — you must implement the actual save inside an onDownload callback.
About
.docx/.odt/.odf: the built-in builders preserve the most common semantic formatting — headings (1–6), bold, italic, strike-through, underline, hyperlinks, and bullet/numbered lists with arbitrary nesting — and produce documents that open cleanly in Microsoft Word, LibreOffice Writer, and Google Docs. Anything outside that set (blockquotes, code, images, tables, fonts, colors, custom indentation) is flattened to plain text. If you need richer fidelity, intercept viaonDownloadand run your own converter.
<script setup lang="ts">
import { ref } from 'vue';
import { RTextEditor } from 'rte-vue';
import type { DownloadFormat } from 'rte-vue';
import 'rte-vue/style.css';
const content = ref('# Hello World');
const formats: DownloadFormat[] = [
{ id: 'markdown', label: 'Markdown (.md)', extension: 'md' },
{ id: 'html', label: 'HTML (.html)', extension: 'html' },
{ id: 'txt', label: 'Plain text (.txt)', extension: 'txt' },
{ id: 'docx', label: 'Word (.docx)', extension: 'docx' },
{ id: 'odt', label: 'OpenDocument (.odt)', extension: 'odt' },
];
</script>
<template>
<RTextEditor
v-model="content"
:downloader="true"
:download-formats="formats"
/>
</template>With 0 or 1 entry (or omitted), the button is a single-click action that saves in the editor's contentFormat.
DownloadFormat shape
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique identifier. "markdown", "html", "txt" are handled by rte-vue. Any other value is a custom format — your onDownload must take over. |
| label | string | Shown in the dropdown entry. |
| extension | string | File extension (no dot). Used to build the filename. |
| mimeType | string (optional) | Blob MIME. Defaults to text/markdown / text/html / text/plain for built-ins, application/octet-stream for custom. |
Intercepting the Download
Provide an onDownload callback (or listen to the @download event) to inspect the payload before the file is built. Return false to suppress the built-in save — useful when the host wants to handle the export itself (e.g., a custom format the package doesn't know about, or replacing one of the built-in builders with your own implementation).
<script setup lang="ts">
import { ref } from 'vue';
import { RTextEditor } from 'rte-vue';
import type { DownloadFormat } from 'rte-vue';
import 'rte-vue/style.css';
const content = ref('# Hello World');
const formats: DownloadFormat[] = [
{ id: 'markdown', label: 'Markdown (.md)', extension: 'md' },
// Custom — rte-vue does not know how to build .rtf, so onDownload must handle it.
{ id: 'rtf', label: 'Rich Text (.rtf)', extension: 'rtf' },
];
const onDownload = (payload: {
content: string;
filename: string;
format: DownloadFormat;
}) => {
if (payload.format.id === 'rtf') {
convertAndSaveRtf(payload.content, payload.filename); // your code
return false; // suppress rte-vue's default (which would warn for unknown IDs)
}
// Returning undefined / true lets rte-vue trigger the default save for
// built-in formats (markdown, html, txt, docx, odt, odf).
};
</script>
<template>
<RTextEditor
v-model="content"
:downloader="true"
:download-formats="formats"
@download="onDownload"
/>
</template>If onDownload returns true/undefined for an unknown format ID, a console.warn is logged and nothing happens. Returning false always suppresses the built-in save — handy if you want to override one of the built-in builders (e.g. to produce a styled .docx via your own converter):
const onDownload = (payload) => {
if (payload.format.id === 'docx') {
myStyledDocxBuilder(editor.getHTML(), payload.filename);
return false;
}
};Using buildDocx / buildOdt standalone
The same builders that rte-vue uses internally are exported for use outside the component. They accept editor HTML and preserve headings / bold / italic / strike / lists in the output:
import { buildDocx, buildOdt, triggerDownload } from 'rte-vue';
const html = `
<h1>My Report</h1>
<p>This is <strong>bold</strong> and this is <em>italic</em>.</p>
<ul>
<li><p>First bullet</p></li>
<li><p>Second bullet</p></li>
</ul>
`;
const blob = await buildDocx(html);
triggerDownload(blob, 'my-report.docx');Both lazy-load jszip, so the dependency only ships on first call.
If you want to introspect what the builder will produce — for example to write your own emitter for a different format — parseEditorHtml returns the same intermediate Block[] AST that both builders consume:
import { parseEditorHtml } from 'rte-vue';
import type { Block, InlineRun } from 'rte-vue';
const blocks = parseEditorHtml('<h1>Hi</h1><p><strong>hello</strong></p>');
// → [
// { kind: 'heading', level: 1, runs: [{ text: 'Hi' }] },
// { kind: 'paragraph', runs: [{ text: 'hello', bold: true }] },
// ]📋 Copy to Clipboard
Set :copyable="true" to add a Copy button to the toolbar. Clicking it copies the editor's current content (in the configured contentFormat) to the system clipboard using navigator.clipboard.writeText. The button is disabled while the editor is empty.
<template>
<RTextEditor
v-model="content"
content-format="markdown"
:copyable="true"
@copy="(payload) => console.log('copied', payload.content)"
/>
</template>As with downloads, return false from the onCopy callback (or @copy listener) to suppress the default clipboard write.
⌨️ Shortcuts
- Bold:
Cmd/Ctrl + B - Italic:
Cmd/Ctrl + I - Save:
Cmd/Ctrl + S
License
MIT
