Lightweight rich-text editor core with a modern demo UI, fixed toolbar workflow, and a minimal API.
Website: https://scribejs.top
Toolbar state (bold / italic / underline / strike / etc.) is now identical across Chrome, Firefox, and Safari.
- Normalized selection layer abstracts
window.getSelection()andRangedifferences. - Handles collapsed, backward, multi-node, and text-vs-element selections uniformly.
- Full iframe editing context support via
ownerDocument.defaultView.
- No longer relies on deprecated
document.queryCommandState/queryCommandValue. - Walks the DOM tree from the selection range, inspecting parent nodes for active marks (
<b>,<strong>,<i>,<em>,<u>,<s>,<strike>,<code>,<a>,<blockquote>,<ol>,<ul>, headings). - Alignment detected via
getComputedStylewith cross-window safety.
- Selection saved before every toolbar click and restored after action.
- Prevents selection reset that Safari applies on button focus.
selectionchangetiming gaps handled via multi-event pipeline (formatChange,change,focus,blur).- Mutation-safe selection refresh on every DOM change.
- 100 ms interval while editor is focused force-refreshes
FormatStatefromSelectionManager. - JSON-diff guard ensures React only re-renders when state actually changes.
- Interval stops on blur and cleans up on unmount.
- Supports
selectionchange,beforeinput,input,keyup,mouseup. formatChangeevent emitted synchronously after every command execution and DOM normalization.
- All toolbar items driven by
CommandMeta— icon, label, shortcut, group,active()function. - Fixed and floating toolbars share the same metadata; no duplicated logic.
- Structural cleanup (empty nodes, whitespace)
- Inline mark merging (adjacent
<b><b>→ single<b>) - Block-level normalization
- List structure repair
- Final whitespace pass
- Inline-first editing with a fixed toolbar experience.
- Simple, typed API with direct method calls.
- Built-in HTML sanitization and safe paste handling.
- Extensible plugin architecture.
- Framework-agnostic core for integration anywhere.
| API | Inline Toolbar | Fixed Editor |
|---|---|---|
![]() |
![]() |
![]() |
- NPM:
npm i scribejs-editor - CDN:
https://unpkg.com/scribejs-editor - Git:
git clone https://github.com/GoodPHP/scribejs
import { createEditor } from 'scribejs-editor';
const editor = createEditor({
target: '#editor',
placeholder: 'Start typing...'
});
editor.bold();
editor.link('https://example.com');
const html = editor.getHTML();import { ScribeEditor, type ScribeEditorRef } from './components/scribe';
import { useRef } from 'react';
function App() {
const editorRef = useRef<ScribeEditorRef>(null);
return (
<ScribeEditor
ref={editorRef}
toolbar="fixed"
placeholder="Write something..."
onChange={(html) => console.log(html)}
/>
);
}npm install
npm run devWhen the dev server starts, it prints the local URL to open the demo.
- Demo UI: index.html + public/demo.css + public/demo.js
- Build output: dist/index.js (browser ESM)
- Types: types/index.d.ts
- Source: src/
Common editor methods used in the demo:
editor.bold()/editor.italic()/editor.underline()editor.heading(1 | 2 | 3)/editor.paragraph()/editor.blockquote()editor.orderedList()/editor.unorderedList()editor.link(url)/editor.unlink()editor.setFontSize(size)/editor.setFontFamily(family)editor.setColor(color)/editor.setBackgroundColor(color)editor.getHTML()/editor.getText()/editor.isEmpty()
Scribe is built with a plugin-first architecture. Add only what you need and keep bundles lean.
- Built-in plugins live in src/plugins/
- External plugins can wrap common behaviors (toolbars, history, selection helpers)
If you use Scribe in production, consider sharing feedback or contributing improvements.
BSD-3-Clause



