A modern TypeScript port of the popular Medium.com-style WYSIWYG editor
Features โข Installation โข Quick Start โข Live Demos โข API โข Examples โข Contributing
- ๐ Medium-like Editor - A modern TypeScript port of the popular Medium.com-style WYSIWYG editor
- ๐ง Extensible Architecture - Plugin system for custom functionality and toolbar buttons
- ๐ฑ Mobile Friendly - Touch and mobile device support with responsive design
- ๐จ Customizable Themes - 7 built-in themes plus extensive styling options
- โก Lightweight - Zero dependencies, small bundle size
- ๐ Type Safe - Full TypeScript support with comprehensive type definitions
- ๐ฏ Auto-Link Detection - Automatically converts URLs to clickable links
- ๐ Smart Paste - Cleans up pasted content from Word, Google Docs, etc.
- ๐ Event System - Comprehensive event handling for content changes
- ๐๏ธ Flexible Toolbars - Static, floating, or custom positioned toolbars
Choose your preferred package manager:
# npm
npm install ts-medium-editor
# yarn
yarn add ts-medium-editor
# pnpm
pnpm add ts-medium-editor
# bun
bun add ts-medium-editor
import { MediumEditor } from 'ts-medium-editor'
import 'ts-medium-editor/css/medium-editor.css'
import 'ts-medium-editor/css/themes/default.css'
// Initialize editor
const editor = new MediumEditor('.editable', {
toolbar: {
buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote']
},
placeholder: {
text: 'Tell your story...'
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Editor</title>
<link rel="stylesheet" href="node_modules/ts-medium-editor/css/medium-editor.css">
<link rel="stylesheet" href="node_modules/ts-medium-editor/css/themes/default.css">
</head>
<body>
<div class="editable">
<p>Start typing here...</p>
</div>
<script type="module">
import { MediumEditor } from './node_modules/ts-medium-editor/dist/index.js'
const editor = new MediumEditor('.editable', {
placeholder: { text: 'Tell your story...' }
})
</script>
</body>
</html>
Explore our comprehensive demo collection to see all features in action:
- Basic Editor - Simple setup with essential toolbar
- Auto-Link Detection - Automatic URL to link conversion
- Clean Paste - Smart content cleaning from Word/Google Docs
- Textarea Support - Enhance HTML textareas with rich editing
- Custom Toolbars - 5 different toolbar configurations
- Static Toolbar - Always-visible toolbars with alignment options
- Button Examples - Custom button creation with Rangy integration
- Extension Examples - 4 powerful extensions with Shiki syntax highlighting
- Multi-Editor - Multiple independent editor instances
- Single Instance - Dynamic element addition to existing editors
- Nested Editable - Complex nested contenteditable layouts
- Multi-Paragraph - Toolbar behavior with paragraph selection
- Relative Toolbar - Constrained toolbar positioning
- Absolute Container - Absolute positioned container examples
- Custom Extensions - Instance-aware extension development
- Table Extension - Custom table insertion functionality
For optimal TypeScript support, configure your tsconfig.json
:
{
"compilerOptions": {
"lib": ["esnext", "dom", "dom.iterable"],
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
interface MediumEditorOptions {
// Core Settings
activeButtonClass?: string // CSS class for active buttons
buttonLabels?: boolean | string | ButtonLabels // Button label configuration
delay?: number // Toolbar show delay (ms)
disableReturn?: boolean // Disable return key
disableDoubleReturn?: boolean // Disable double return
disableExtraSpaces?: boolean // Prevent extra spaces
disableEditing?: boolean // Make editor read-only
spellcheck?: boolean // Enable spellcheck
// Auto-features
autoLink?: boolean // Auto-convert URLs to links
targetBlank?: boolean // Open links in new tab
imageDragging?: boolean // Enable image drag-and-drop
fileDragging?: boolean // Enable file drag-and-drop
// DOM Configuration
elementsContainer?: HTMLElement // Container for editor elements
contentWindow?: Window // Window context
ownerDocument?: Document // Document context
// Extensions
extensions?: Record<string, Extension> // Custom extensions
// Feature Modules
toolbar?: ToolbarOptions | false // Toolbar configuration
anchorPreview?: AnchorPreviewOptions | false // Link preview
placeholder?: PlaceholderOptions | false // Placeholder text
anchor?: AnchorOptions | false // Link creation
paste?: PasteOptions | false // Paste handling
keyboardCommands?: KeyboardOptions | false // Keyboard shortcuts
}
class MediumEditor {
// Lifecycle
constructor(elements: Elements, options?: MediumEditorOptions)
setup(): MediumEditor
destroy(): void
// Content Management
getContent(index?: number): string
setContent(html: string, index?: number): void
serialize(): Record<string, string>
resetContent(element?: HTMLElement): void
// Element Management
addElements(elements: Elements): void
removeElements(elements: Elements): void
// Selection Management
exportSelection(): SelectionState | null
importSelection(state: SelectionState, favorLater?: boolean): void
saveSelection(): void
restoreSelection(): void
selectAllContents(): void
selectElement(element: HTMLElement): void
// Event Handling
subscribe(event: string, listener: EventListener): MediumEditor
unsubscribe(event: string, listener: EventListener): MediumEditor
trigger(event: string, data?: any, editable?: HTMLElement): MediumEditor
// Actions
execAction(action: string, opts?: any): boolean
queryCommandState(action: string): boolean
}
const editor = new MediumEditor('.editable', {
buttonLabels: 'fontawesome',
toolbar: {
buttons: [
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'anchor',
'image',
'quote',
'pre',
'orderedlist',
'unorderedlist',
'indent',
'outdent',
'justifyLeft',
'justifyCenter',
'justifyRight',
'justifyFull',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'
],
static: true,
sticky: true,
align: 'center'
}
})
const editor = new MediumEditor('.editable', {
autoLink: true,
targetBlank: true,
toolbar: {
buttons: ['bold', 'italic', 'anchor']
},
anchor: {
placeholderText: 'Enter a URL',
targetCheckbox: true,
targetCheckboxText: 'Open in new tab'
}
})
// Title editor (no line breaks)
const titleEditor = new MediumEditor('.title', {
disableReturn: true,
disableExtraSpaces: true,
toolbar: {
buttons: ['bold', 'italic']
},
placeholder: {
text: 'Enter title...'
}
})
// Content editor (full features)
const contentEditor = new MediumEditor('.content', {
autoLink: true,
toolbar: {
buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote', 'orderedlist', 'unorderedlist']
},
placeholder: {
text: 'Tell your story...'
}
})
const editor = new MediumEditor('.editable', {
paste: {
forcePlainText: false,
cleanPastedHTML: true,
cleanReplacements: [
[/\s*style\s*=\s*["'][^"']*["']/gi, ''], // Remove inline styles
[/<o:p\s*\/?>|<\/o:p>/gi, ''], // Remove Word tags
[/<xml>[\s\S]*?<\/xml>/gi, ''], // Remove XML
[/<!--[\s\S]*?-->/g, ''] // Remove comments
],
cleanAttrs: ['class', 'style', 'dir'],
cleanTags: ['meta', 'style', 'script', 'object', 'embed']
}
})
const editor = new MediumEditor('.editable')
// Content change events
editor.subscribe('editableInput', (event, editable) => {
console.log('Content changed:', editable.innerHTML)
// Auto-save logic here
})
// Selection change events
editor.subscribe('editableKeyup', (event, editable) => {
const selection = editor.exportSelection()
console.log('Cursor position:', selection)
})
// Focus events
editor.subscribe('focus', (event, editable) => {
console.log('Editor focused')
})
editor.subscribe('blur', (event, editable) => {
console.log('Editor blurred')
})
import { MediumEditorExtension } from 'ts-medium-editor'
class EmojiExtension implements MediumEditorExtension {
name = 'emoji'
private button!: HTMLButtonElement
private base: any
init(): void {
this.button = this.createButton()
}
getButton(): HTMLButtonElement {
return this.button
}
private createButton(): HTMLButtonElement {
const button = document.createElement('button')
button.className = 'medium-editor-action'
button.innerHTML = '๐'
button.title = 'Insert Emoji'
button.addEventListener('click', this.handleClick.bind(this))
return button
}
private handleClick(): void {
const emoji = '๐'
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
range.deleteContents()
range.insertNode(document.createTextNode(emoji))
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
}
}
destroy(): void {
if (this.button) {
this.button.removeEventListener('click', this.handleClick)
}
}
}
// Use the extension
const editor = new MediumEditor('.editable', {
toolbar: {
buttons: ['bold', 'italic', 'emoji']
},
extensions: {
emoji: new EmojiExtension()
}
})
const themeSelector = document.getElementById('theme-select') as HTMLSelectElement
const themeLink = document.getElementById('theme-css') as HTMLLinkElement
const themes = [
'default',
'beagle',
'bootstrap',
'flat',
'mani',
'roman',
'tim'
]
themeSelector.addEventListener('change', (event) => {
const theme = (event.target as HTMLSelectElement).value
themeLink.href = `./dist/css/themes/${theme}.css`
})
The library includes 7 beautiful themes:
- Default - Clean, modern design
- Beagle - Friendly, rounded interface
- Bootstrap - Bootstrap-compatible styling
- Flat - Minimalist flat design
- Mani - Elegant, sophisticated look
- Roman - Classic, serif-inspired
- Tim - Bold, high-contrast theme
<!-- Include your chosen theme -->
<link rel="stylesheet" href="dist/css/themes/default.css">
// Static toolbar (always visible)
const editor = new MediumEditor('.editable', {
toolbar: {
static: true,
sticky: true,
align: 'center'
}
})
// Relative container
const editor = new MediumEditor('.editable', {
toolbar: {
relativeContainer: document.getElementById('toolbar-container')
}
})
const editor = new MediumEditor('.editable', {
toolbar: {
buttons: [
'bold',
'italic',
{
name: 'highlight',
action: 'highlight',
aria: 'Highlight text',
contentDefault: 'H',
classList: ['custom-highlight-button'],
attrs: {
'data-action': 'highlight'
}
}
]
}
})
Run the test suite:
bun test
For help, discussion about best practices, or any other conversation:
- ๐ฌ GitHub Discussions
- ๐ฎ Discord Server
- ๐ Issue Tracker
โSoftware that is free, but hopes for a postcard.โ We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States ๐
We would like to extend our thanks to the following sponsors for funding Stacks development:
- JetBrains - Professional development tools
- The Solana Foundation - Blockchain infrastructure
Become a sponsor and support open source development.
- Medium - For the beautiful editor design inspiration
- medium-editor - The original JavaScript implementation that inspired this TypeScript port
- Chris Breuer - Primary maintainer and TypeScript port author
- All Contributors - Everyone who has contributed to making this project better
The MIT License (MIT). Please see LICENSE for more information.
Made with ๐ by the Stacks team
โญ Star us on GitHub โข ๐ฆ Follow on Bluesky โข ๐ฌ Join Discord