
open-edit is a JavaScript WYSIWYG editor that supports rich text editing, clean HTML output, Markdown import and export, and optional plugins.
It uses the browser’s contenteditable API, works in plain HTML plus React, Vue, or Svelte apps, and is released under the MIT license.
Features:
- Supports bold, italic, underline, strikethrough, and inline code.
- Inserts paragraphs, headings, lists, blockquotes, code blocks, and horizontal rules.
- Handles images with drag resize and custom upload hooks.
- Exports and imports both HTML and Markdown.
- Shows a configurable toolbar, bubble toolbar, source view, and status bar.
- Applies light, dark, or auto theme modes through CSS variables.
- Extends the editor with highlight, emoji, template tag, callout, and slash command plugins.
- Supports TypeScript and framework wrappers with direct instance access.
How to use it:
1. Install the open-edit package from npm and import it into your JS project.
# NPM $ npm install open-edit
import { OpenEdit } from 'open-edit';2. Or load it directly from a CDN.
<script src="https://unpkg.com/open-edit/dist/open-edit.umd.js"></script>
3. Create a mount point for the editor.
<div id="editor"></div>
4. Initialize open-edit to create a basic editor.
const editor = OpenEdit.create('#editor', {
// options here
});5. Limit the toolbar to the tools you need:
undo(string): Shows the undo button.redo(string): Shows the redo button.blockType(string): Shows the block type dropdown.bold(string): Shows the bold button.italic(string): Shows the italic button.underline(string): Shows the underline button.code(string): Shows the inline code button.alignLeft(string): Shows the left align button.alignCenter(string): Shows the center align button.alignRight(string): Shows the right align button.alignJustify(string): Shows the justify align button.bulletList(string): Shows the bullet list button.orderedList(string): Shows the ordered list button.link(string): Shows the link button.image(string): Shows the image button.blockquote(string): Shows the blockquote button.hr(string): Shows the horizontal rule button.callout(string): Shows the callout insertion button.htmlToggle(string): Shows the HTML source toggle button.
const editor = OpenEdit.create('#editor', {
toolbarItems: ['bold', 'italic', 'underline', 'link']
});6. Configure the status bar:
const editor = OpenEdit.create('#editor', {
statusBar: {
wordCount: true,
charCount: true,
elementPath: false,
htmlToggle: false
}
});7. Handle image uploads:
const editor = OpenEdit.create('#editor', {
onImageUpload: async (file) => {
// Build a form payload for the upload endpoint
const payload = new FormData();
payload.append('file', file);
// Send the file to your API
const response = await fetch('/api/media/upload', {
method: 'POST',
body: payload
});
// Read the returned public URL
const data = await response.json();
// Return the final image URL to the editor
return data.url;
}
});8. Export and import Markdown:
const editor = OpenEdit.create('#editor', {
// options here
});
// Read Markdown from the current document
const markdown = editor.getMarkdown();
console.log(markdown);
// Replace the editor content with Markdown input
editor.setMarkdown('# Project Notes\n\nThis paragraph uses **bold** text.');9. All configuration options:
content(string): Sets the initial HTML content.placeholder(string): Shows placeholder text when the editor is empty.theme('light' | 'dark' | 'auto'): Selects the UI color mode.readOnly(boolean): Locks the editor into read-only mode.toolbar(ToolbarItemConfig[]): Replaces the default toolbar with a full custom toolbar config.toolbarItems(string[]): Filters the default toolbar by item ID.statusBar(boolean | StatusBarOptions): Shows, hides, or customizes the status bar.onChange((html: string) => void): Runs on every content update.onImageUpload((file: File) => Promise<string>): Uploads an image file and returns the final public URL.locale(locale object): Overrides automatic language detection with a specific locale object.
10. API methods.
// Return the current document as clean HTML
editor.getHTML();
// Replace the current content with new HTML
editor.setHTML('<p>Release notes go here.</p>');
// Return the current document as Markdown
editor.getMarkdown();
// Replace the current content with Markdown
editor.setMarkdown('## Weekly Update\n\nItem one');
// Return the internal document model
editor.getDocument();
// Check whether the editor is empty
editor.isEmpty();
// Check whether the editor currently has focus
editor.isFocused();
// Check whether an inline mark is active
editor.isMarkActive('bold');
// Read the active block type
editor.getActiveBlockType();
// Read the current selection model
editor.getSelection();
// Focus the editor
editor.focus();
// Remove focus from the editor
editor.blur();
// Register a custom editor event listener
editor.on('change', (doc) => {
console.log('Document changed:', doc);
});
// Remove a custom editor event listener
editor.off('change', listener);
// Start a chainable command sequence
editor.chain()
.toggleMark('bold')
.setBlock('heading', { level: 2 })
.setBlock('callout', { variant: 'warning' })
.setAlign('center')
.insertImage('https://example.com/cover.png', 'Article cover')
.insertHr()
.toggleList('bullet_list')
.undo()
.redo()
.run();
// Register a plugin
editor.use(myPlugin);
// Destroy the editor instance
editor.destroy();11. Events.
// Fires when the document content changes
editor.on('change', (doc) => {
console.log('Change event:', doc);
});
// Fires when the current selection changes
editor.on('selectionchange', (selection) => {
console.log('Selection event:', selection);
});
// Fires when the editor receives focus
editor.on('focus', () => {
console.log('Editor focused');
});
// Fires when the editor loses focus
editor.on('blur', () => {
console.log('Editor blurred');
});Add plugins:
Syntax highlighting
import { OpenEdit } from 'open-edit';
// Create the editor instance
const editor = OpenEdit.create('#editor');
// Register the code highlighting plugin
editor.use(OpenEdit.plugins.highlight());
Emoji picker
import { OpenEdit } from 'open-edit';
const editor = OpenEdit.create('#editor');
// Add emoji support to the toolbar
editor.use(OpenEdit.plugins.emoji());
Template variables
import { OpenEdit } from 'open-edit';
const editor = OpenEdit.create('#editor');
// Mark supported template placeholders inside content
editor.use(OpenEdit.plugins.templateTags({
variables: ['firstName', 'orderId', 'companyName']
}));
Callout blocks
import { OpenEdit } from 'open-edit';
const editor = OpenEdit.create('#editor');
// Register styled callout blocks
editor.use(OpenEdit.plugins.callout());
// Insert a warning callout through the chain API
editor.chain()
.setBlock('callout', { variant: 'warning' })
.run();
Slash commands
import { OpenEdit } from 'open-edit';
const editor = OpenEdit.create('#editor');
// Enable slash command insertion
editor.use(OpenEdit.plugins.slashCommands());
Framework Integrations:
React integration
import { useEffect, useRef } from 'react';
import { OpenEdit } from 'open-edit';
import type { EditorInterface } from 'open-edit';
export function RichTextField({ onChange }: { onChange: (html: string) => void }) {
// Keep a ref for the target node
const containerRef = useRef<HTMLDivElement>(null);
// Keep the editor instance between renders
const editorRef = useRef<EditorInterface | null>(null);
useEffect(() => {
// Stop duplicate setup in development strict mode
if (!containerRef.current || editorRef.current) return;
// Create the editor once
editorRef.current = OpenEdit.create(containerRef.current, {
onChange
});
// Destroy the editor on unmount
return () => {
editorRef.current?.destroy();
editorRef.current = null;
};
}, []);
return <div ref={containerRef} />;
}
Vue 3 integration
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { OpenEdit } from 'open-edit';
import type { EditorInterface } from 'open-edit';
// Point to the DOM mount node
const container = ref<HTMLDivElement>();
// Hold the editor instance
let editor: EditorInterface;
onMounted(() => {
// Create the editor after the component mounts
editor = OpenEdit.create(container.value!, {
onChange: (html) => {
console.log('Vue editor value:', html);
}
});
});
onUnmounted(() => {
// Clean up the instance when the component leaves the page
editor?.destroy();
});
</script>
<template>
<div ref="container" />
</template>







