
Company News
Socket Named Top Sales Organization by RepVue
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.
plain-rich-editor
Advanced tools
A rich text editor built with plain TypeScript, no external dependencies for core functionality
A modern, lightweight rich text editor built with plain TypeScript. Zero external dependencies for core functionality, fully customizable, and designed for both developers and contributors.
npm install plain-rich-editor
plain-rich-editor is a vanilla TypeScript package that works with any JavaScript framework or vanilla JavaScript. It can be used in:
import { RichEditor } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
const container = document.getElementById('editor');
const editor = new RichEditor(container, {
initialContent: '<p>Hello World</p>',
onContentChange: (content) => console.log(content),
});
editor.render();
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="node_modules/plain-rich-editor/dist/styles/editor.css">
</head>
<body>
<div id="editor-root"></div>
<script type="module">
import { RichEditor } from 'node_modules/plain-rich-editor/dist/index.js';
const editor = new RichEditor(document.getElementById('editor-root'));
editor.render();
</script>
</body>
</html>
The RichEditor constructor accepts an optional RichEditorOptions object to configure the editor behavior and appearance.
interface RichEditorOptions {
// Content options
initialContent?: string; // Initial HTML content
readonly?: boolean; // Enable readonly mode
// Callbacks
onContentChange?: (content: string) => void; // Called when content changes
onFormatChange?: (format: TextFormat) => void; // Called when format changes
onSelectionChange?: (range: EditorRange | null) => void; // Called when selection changes
// View options
showHtmlPreview?: boolean; // Show HTML preview toggle
initialViewMode?: 'editor' | 'preview' | 'both'; // Initial view mode
// Extensions
extensions?: Extension[]; // Array of extensions/plugins
// Toolbar configuration
toolbar?: ToolbarOptions; // Toolbar customization
}
| Option | Type | Default | Description |
|---|---|---|---|
initialContent | string | '' | Initial HTML content to load into the editor |
readonly | boolean | false | If true, the editor becomes read-only |
| Option | Type | Default | Description |
|---|---|---|---|
onContentChange | (content: string) => void | undefined | Called whenever the editor content changes. Receives the current HTML content. |
onFormatChange | (format: TextFormat) => void | undefined | Called when text formatting changes (e.g., bold, italic, font). Receives the current format state. |
onSelectionChange | (range: EditorRange | null) => void | undefined | Called when the text selection changes. Receives the selection range or null if no selection. |
TextFormat Interface:
interface TextFormat {
bold?: boolean;
italic?: boolean;
underline?: boolean;
fontFamily?: string;
}
Note: The
TextFormatinterface is returned by theonFormatChangecallback. Only properties with UI controls are listed above. Additional properties may exist in the type system for internal use but are not user-configurable through the toolbar.
EditorRange Interface:
interface EditorRange {
start: number; // Start position
end: number; // End position
}
| Option | Type | Default | Description |
|---|---|---|---|
showHtmlPreview | boolean | true | Whether to show the view mode buttons (Editor, Preview, Both). If false, all three buttons are hidden |
initialViewMode | 'editor' | 'preview' | 'both' | 'editor' | Initial view mode when editor loads. Sets which button is active initially |
View Mode Buttons:
All three buttons are controlled by showHtmlPreview. If set to false, none of the view mode buttons will be shown.
| Option | Type | Default | Description |
|---|---|---|---|
extensions | Extension[] | [] | Array of extension/plugin objects to extend editor functionality |
interface ToolbarOptions {
// Show/hide toolbar groups
showFormatButtons?: boolean; // Show Bold, Italic, Underline buttons
showFontFamily?: boolean; // Show font family dropdown
showStyleDropdown?: boolean; // Show style dropdown (headings)
// Format buttons configuration
formatButtons?: {
bold?: boolean; // Show/hide Bold button
italic?: boolean; // Show/hide Italic button
underline?: boolean; // Show/hide Underline button
};
}
| Option | Type | Default | Description |
|---|---|---|---|
showFormatButtons | boolean | true | Show/hide the entire format buttons group (Bold, Italic, Underline). If false, all format buttons are hidden |
formatButtons | object | undefined | Fine-grained control over individual format buttons. Only applies when showFormatButtons is true |
formatButtons.bold | boolean | true | Show/hide Bold button individually |
formatButtons.italic | boolean | true | Show/hide Italic button individually |
formatButtons.underline | boolean | true | Show/hide Underline button individually |
showFontFamily | boolean | true | Show/hide the font family dropdown |
showStyleDropdown | boolean | true | Show/hide the style dropdown (Normal, H1, H2, etc.) |
Format Buttons Control:
showFormatButtons: false to hide all format buttons (Bold, Italic, Underline)showFormatButtons: true and use formatButtons object to show/hide individual buttons:
toolbar: {
showFormatButtons: true, // Show format buttons group
formatButtons: {
bold: true, // Show Bold
italic: false, // Hide Italic
underline: true // Show Underline
}
}
const editor = new RichEditor(container, {
initialContent: '<p>Hello World</p>',
onContentChange: (content) => {
console.log('Content changed:', content);
},
});
editor.render();
const editor = new RichEditor(container, {
initialContent: '<p>This is read-only content</p>',
readonly: true,
});
editor.render();
const editor = new RichEditor(container, {
toolbar: {
showFormatButtons: true,
formatButtons: {
bold: true,
italic: true,
underline: false, // Hide underline button individually
},
showFontFamily: true,
showStyleDropdown: false, // Hide style dropdown
},
showHtmlPreview: true, // Show view mode buttons (Editor, Preview, Both)
initialViewMode: 'both', // Start with both views visible
});
editor.render();
const editor = new RichEditor(container, {
toolbar: {
showFormatButtons: true,
showFontFamily: false,
showStyleDropdown: false,
},
});
editor.render();
const editor = new RichEditor(container, {
showHtmlPreview: true, // Show view mode buttons (Editor, Preview, Both)
initialViewMode: 'both', // Start with both editor and preview visible
});
editor.render();
Hide View Mode Buttons:
const editor = new RichEditor(container, {
showHtmlPreview: false, // Hide Editor, Preview, Both buttons completely
});
editor.render();
const editor = new RichEditor(container, {
onContentChange: (content) => {
console.log('Content:', content);
// Save to server, update state, etc.
},
onFormatChange: (format) => {
console.log('Format:', format);
// Update UI, show format info, etc.
},
onSelectionChange: (range) => {
console.log('Selection:', range);
// Show selection info, enable/disable buttons, etc.
},
});
editor.render();
import React, { useEffect, useRef } from 'react';
import { RichEditor } from 'plain-rich-editor';
import type { RichEditorOptions } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
interface RichEditorComponentProps {
options?: RichEditorOptions;
onContentChange?: (content: string) => void;
}
const RichEditorComponent: React.FC<RichEditorComponentProps> = ({
options = {},
onContentChange
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<RichEditor | null>(null);
useEffect(() => {
if (containerRef.current && !editorRef.current) {
const editorOptions: RichEditorOptions = {
...options,
onContentChange: onContentChange || options.onContentChange,
};
editorRef.current = new RichEditor(containerRef.current, editorOptions);
editorRef.current.render();
}
// Cleanup on unmount
return () => {
if (editorRef.current) {
editorRef.current.destroy();
editorRef.current = null;
}
};
}, []);
// Update content when options.initialContent changes
useEffect(() => {
if (editorRef.current && options.initialContent !== undefined) {
editorRef.current.setContent(options.initialContent);
}
}, [options.initialContent]);
return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
};
export default RichEditorComponent;
Usage:
import RichEditorComponent from './RichEditorComponent';
function App() {
const handleContentChange = (content: string) => {
console.log('Content changed:', content);
};
return (
<div style={{ padding: '20px' }}>
<RichEditorComponent
options={{
toolbar: {
showFormatButtons: true,
showFontFamily: true,
},
}}
onContentChange={handleContentChange}
/>
</div>
);
}
<template>
<div ref="editorContainer" style="width: 100%; height: 100%;"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { RichEditor } from 'plain-rich-editor';
import type { RichEditorOptions } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
interface Props {
options?: RichEditorOptions;
}
const props = withDefaults(defineProps<Props>(), {
options: () => ({}),
});
const emit = defineEmits<{
(e: 'content-change', content: string): void;
(e: 'format-change', format: any): void;
}>();
const editorContainer = ref<HTMLDivElement | null>(null);
let editorInstance: RichEditor | null = null;
onMounted(() => {
if (editorContainer.value) {
const editorOptions: RichEditorOptions = {
...props.options,
onContentChange: (content) => emit('content-change', content),
onFormatChange: (format) => emit('format-change', format),
};
editorInstance = new RichEditor(editorContainer.value, editorOptions);
editorInstance.render();
}
});
onBeforeUnmount(() => {
if (editorInstance) {
editorInstance.destroy();
editorInstance = null;
}
});
watch(() => props.options?.initialContent, (newContent) => {
if (editorInstance && newContent !== undefined) {
editorInstance.setContent(newContent);
}
});
</script>
Usage:
<template>
<RichEditorComponent
:options="{
toolbar: {
showFormatButtons: true,
},
}"
@content-change="handleContentChange"
/>
</template>
<script setup lang="ts">
import RichEditorComponent from './RichEditorComponent.vue';
const handleContentChange = (content: string) => {
console.log('Content changed:', content);
};
</script>
<template>
<div ref="editorContainer" style="width: 100%; height: 100%;"></div>
</template>
<script>
import { RichEditor } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
export default {
name: 'RichEditorComponent',
props: {
options: {
type: Object,
default: () => ({}),
},
},
mounted() {
if (this.$refs.editorContainer) {
this.editor = new RichEditor(this.$refs.editorContainer, {
...this.options,
onContentChange: (content) => this.$emit('content-change', content),
});
this.editor.render();
}
},
beforeDestroy() {
if (this.editor) {
this.editor.destroy();
}
},
watch: {
'options.initialContent': {
handler(newContent) {
if (this.editor && newContent !== undefined) {
this.editor.setContent(newContent);
}
},
},
},
data() {
return {
editor: null,
};
},
};
</script>
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { RichEditor } from 'plain-rich-editor';
import type { RichEditorOptions } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
@Component({
selector: 'app-rich-editor',
template: '<div #editorContainer style="width: 100%; height: 100%;"></div>',
styles: [':host { display: block; width: 100%; height: 100%; }']
})
export class RichEditorComponent implements OnInit, OnDestroy {
@Input() options: RichEditorOptions = {};
@ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef<HTMLDivElement>;
private editorInstance: RichEditor | null = null;
ngOnInit(): void {
if (this.editorContainer?.nativeElement) {
this.editorInstance = new RichEditor(
this.editorContainer.nativeElement,
this.options
);
this.editorInstance.render();
}
}
ngOnDestroy(): void {
if (this.editorInstance) {
this.editorInstance.destroy();
this.editorInstance = null;
}
}
getContent(): string {
return this.editorInstance?.getContent() || '';
}
setContent(content: string): void {
if (this.editorInstance) {
this.editorInstance.setContent(content);
}
}
}
Usage:
<app-rich-editor
[options]="{
toolbar: {
showFormatButtons: true,
}
}"
></app-rich-editor>
'use client'; // Required for Next.js 13+ App Router
import { useEffect, useRef } from 'react';
import { RichEditor } from 'plain-rich-editor';
import type { RichEditorOptions } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
interface RichEditorProps {
options?: RichEditorOptions;
}
export default function RichEditorClient({ options = {} }: RichEditorProps) {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<RichEditor | null>(null);
useEffect(() => {
// Only run on client-side
if (typeof window !== 'undefined' && containerRef.current && !editorRef.current) {
editorRef.current = new RichEditor(containerRef.current, options);
editorRef.current.render();
}
return () => {
if (editorRef.current) {
editorRef.current.destroy();
editorRef.current = null;
}
};
}, []);
useEffect(() => {
if (editorRef.current && options.initialContent !== undefined) {
editorRef.current.setContent(options.initialContent);
}
}, [options.initialContent]);
return <div ref={containerRef} style={{ width: '100%', minHeight: '400px' }} />;
}
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { RichEditor } from 'plain-rich-editor';
import type { RichEditorOptions } from 'plain-rich-editor';
import 'plain-rich-editor/dist/styles/editor.css';
export let options: RichEditorOptions = {};
let editorContainer: HTMLDivElement;
let editorInstance: RichEditor | null = null;
onMount(() => {
if (editorContainer) {
editorInstance = new RichEditor(editorContainer, options);
editorInstance.render();
}
});
onDestroy(() => {
if (editorInstance) {
editorInstance.destroy();
editorInstance = null;
}
});
export function getContent(): string {
return editorInstance?.getContent() || '';
}
export function setContent(content: string): void {
if (editorInstance) {
editorInstance.setContent(content);
}
}
</script>
<div bind:this={editorContainer} style="width: 100%; height: 100%;"></div>
Usage:
<script>
import RichEditor from './RichEditor.svelte';
let editor;
function handleContentChange() {
console.log('Content:', editor.getContent());
}
</script>
<RichEditor
bind:this={editor}
options={{
toolbar: {
showFormatButtons: true,
},
}}
on:content-change={handleContentChange}
/>
Main editor class that orchestrates all components.
constructor(container: HTMLElement, options?: RichEditorOptions)
Creates a new editor instance.
Parameters:
container (required): The HTML element that will contain the editoroptions (optional): Configuration options (see Configuration Options)Example:
const container = document.getElementById('editor');
const editor = new RichEditor(container, {
initialContent: '<p>Hello World</p>',
onContentChange: (content) => console.log(content),
});
render(): voidInitializes and renders the editor. This must be called after creating a new RichEditor instance.
Example:
const editor = new RichEditor(container, options);
editor.render(); // Must call render() to display the editor
focus(): voidFocuses the editor content area, placing the cursor in the editor.
Example:
editor.focus();
getContent(): stringReturns the current HTML content of the editor.
Returns: The HTML content as a string
Example:
const content = editor.getContent();
console.log(content); // '<p>Hello World</p>'
setContent(html: string): voidSets the editor content from HTML.
Parameters:
html (required): HTML string to set as editor contentExample:
editor.setContent('<p>New content</p>');
destroy(): voidDestroys the editor instance, removing all event listeners and cleaning up resources. Should be called when removing the editor from the DOM (especially in React, Vue, etc.).
Example:
// In React useEffect cleanup
useEffect(() => {
const editor = new RichEditor(containerRef.current, options);
editor.render();
return () => {
editor.destroy(); // Cleanup
};
}, []);
setViewMode(mode: 'editor' | 'preview' | 'both'): voidChanges the current view mode of the editor.
Parameters:
mode (required): The view mode to set
'editor': Show only the editor'preview': Show only the HTML preview'both': Show both editor and preview side-by-sideExample:
editor.setViewMode('both'); // Show both views
editor.setViewMode('preview'); // Switch to preview only
registerExtension(extension: Extension): voidRegisters an extension/plugin with the editor. (Advanced usage)
Parameters:
extension (required): Extension object implementing the Extension interfaceExample:
const myExtension = {
name: 'my-extension',
// ... extension implementation
};
editor.registerExtension(myExtension);
unregisterExtension(extensionName: string): voidUnregisters an extension from the editor. (Advanced usage)
Parameters:
extensionName (required): Name of the extension to removeExample:
editor.unregisterExtension('my-extension');
src/
├── components/ # UI Components
│ ├── RichEditor.ts # Main editor component
│ ├── EditorContent.ts # Content area component
│ └── toolbar/ # Toolbar components
│ ├── Toolbar.ts
│ ├── FormatButtons.ts
│ ├── FontControls.ts
│ └── ...
├── core/ # Core logic
│ ├── EditorEngine.ts # Text manipulation & formatting
│ └── SelectionManager.ts # Selection & cursor management
├── types/ # TypeScript types
│ └── editor.types.ts
├── utils/ # Utility functions
│ ├── HtmlConverter.ts # HTML conversion utilities
│ ├── dom.ts # DOM helpers
│ └── constants.ts # Constants
├── styles/ # SCSS stylesheets
│ ├── editor.scss
│ ├── toolbar.scss
│ ├── variables.scss
│ └── mixins.scss
└── index.ts # Main entry point
Note: This section is for developers contributing to the package. If you're just using the package, you can skip to License and Support.
# Clone the repository
git clone <repository-url>
cd editor
# Install dependencies
npm install
# Build the project
npm run build
# Watch for changes and auto-compile
npm run watch
The project includes options to minify and obfuscate code for production:
# Build with minification (recommended for production)
npm run build:prod
# Build with obfuscation (maximum protection, may impact performance)
npm run build:secure
# Or run minification/obfuscation separately
npm run minify
npm run obfuscate
Note:
build:prod - Minifies code using terser (reduces file size, harder to read)build:secure - Obfuscates code using javascript-obfuscator (very hard to read, but may slow down execution)Components (src/components/): All UI components
Core (src/core/): Core business logic
Types (src/types/): TypeScript type definitions
Utils (src/utils/): Utility functions
Styles (src/styles/): SCSS stylesheets
Customize the editor appearance by overriding SCSS variables:
// Your custom styles
@use 'plain-rich-editor/dist/styles/variables' as *;
:root {
--color-primary: #your-color;
--toolbar-height: 40px;
}
The editor uses CSS classes that you can override:
.rich-editor - Main container.editor-toolbar-container - Toolbar.editor-content - Content areaWe welcome contributions! Please see CONTRIBUTING.md for guidelines.
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)# Run tests (when available)
npm test
MIT License - see LICENSE file for details
Built with modern web technologies, designed for developers who want full control over their editor implementation.
FAQs
A rich text editor built with plain TypeScript, no external dependencies for core functionality
The npm package plain-rich-editor receives a total of 2 weekly downloads. As such, plain-rich-editor popularity was classified as not popular.
We found that plain-rich-editor demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.

Security News
NIST will stop enriching most CVEs under a new risk-based model, narrowing the NVD's scope as vulnerability submissions continue to surge.

Company News
/Security News
Socket is an initial recipient of OpenAI's Cybersecurity Grant Program, which commits $10M in API credits to defenders securing open source software.