npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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 .txt files 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 an onDownload hook 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:

  1. Basic Usage
  2. Custom Toolbar Groups
  3. Theming with Tailwind CSS
  4. Markdown Support
  5. Custom Toolbar Buttons (Slots)
  6. Form Validation Integration7. Internationalization (i18n)
  7. Read-Only Display
  8. Chat / Comment Input
  9. 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:

  1. Toolbar button — An Upload button appears in the toolbar. Clicking it opens a file picker.
  2. Drag and drop — Drag a supported file directly onto the editor. A drop overlay will appear to confirm the target.
  3. 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 via onDownload and 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