Angular Rich Text Editor
Scribe Editor is the lightweight, zero-dependency WYSIWYG editor for Angular. Because it exposes a plain Scribe.init(element) DOM API, it drops into any Angular component with a single @ViewChild — no official adapter package needed.
Why use Scribe in your Angular app?
Under 50KB gzipped
Zero runtime dependencies. No ProseMirror, no Quill core, no Angular wrapper package to ship — your bundle stays lean.
Plain DOM API
Scribe.init() takes any HTMLElement, so @ViewChild + ElementRef is all you need. No NgZone wrestling, no custom adapters.
Forms-ready
Wrap it as a ControlValueAccessor once and it drops into [(ngModel)] and reactive formControlName like any native input.
Built-in XSS sanitization
Safe paste handling from Word and Google Docs out of the box. No DomSanitizer gymnastics or extra libraries required.
Note: Scribe has no official Angular package. It exposes a single framework-agnostic, DOM-based API (Scribe.init(element, options) and editor.destroy()). The component and ControlValueAccessor below are standard Angular wrapper patterns around that DOM API — which works because Angular gives you the raw element through ElementRef.
Basic Angular Component
Grab the host element with @ViewChild + ElementRef, initialize Scribe in ngOnInit, and tear it down in ngOnDestroy. That is the entire lifecycle.
import {
Component,
ElementRef,
ViewChild,
OnInit,
OnDestroy,
} from '@angular/core';
import { Scribe } from 'scribejs-editor';
import 'scribejs-editor/dist/scribe.css';
@Component({
selector: 'app-rich-text-editor',
standalone: true,
template: '<div #host></div>',
})
export class RichTextEditorComponent implements OnInit, OnDestroy {
// ViewChild gives us the raw DOM element Scribe needs.
@ViewChild('host', { static: true }) host!: ElementRef<HTMLElement>;
private editor?: ReturnType<typeof Scribe.init>;
ngOnInit(): void {
// Scribe is framework-agnostic — it just needs a DOM element.
this.editor = Scribe.init(this.host.nativeElement, {
toolbar: 'floating', // or 'fixed' | 'none'
placeholder: 'Start writing...',
onChange: (html: string) => console.log(html),
});
}
ngOnDestroy(): void {
// Always tear down on destroy to avoid leaking listeners.
this.editor?.destroy();
}
}ControlValueAccessor for ngModel & Reactive Forms
Implement ControlValueAccessor so Scribe behaves like a native form control. Push edits through Scribe's onChange and apply external values with setHTML() inside writeValue().
import {
Component,
ElementRef,
ViewChild,
OnInit,
OnDestroy,
forwardRef,
} from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { Scribe } from 'scribejs-editor';
@Component({
selector: 'app-scribe-control',
standalone: true,
template: '<div #host></div>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScribeControlComponent),
multi: true,
},
],
})
export class ScribeControlComponent
implements ControlValueAccessor, OnInit, OnDestroy
{
@ViewChild('host', { static: true }) host!: ElementRef<HTMLElement>;
private editor?: ReturnType<typeof Scribe.init>;
private onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {};
ngOnInit(): void {
this.editor = Scribe.init(this.host.nativeElement, {
toolbar: 'fixed',
onChange: (html: string) => {
// Push every edit back into the form model.
this.onChange(html);
this.onTouched();
},
});
}
ngOnDestroy(): void {
this.editor?.destroy();
}
// --- ControlValueAccessor ---
writeValue(value: string | null): void {
// Angular sets the initial / patched value here.
this.editor?.setHTML(value ?? '');
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// TODO(verify): Scribe has no documented Angular-specific disable API.
// Use a standard contenteditable toggle on the host element instead.
this.host.nativeElement
.querySelector('[contenteditable]')
?.setAttribute('contenteditable', String(!isDisabled));
}
}Using it in a form
Once the wrapper implements ControlValueAccessor, it works with both template-driven [(ngModel)] and reactive formControlName bindings.
<!-- template-driven forms -->
<app-scribe-control [(ngModel)]="body"></app-scribe-control>
<!-- reactive forms -->
<form [formGroup]="form">
<app-scribe-control formControlName="body"></app-scribe-control>
</form>Everything you need, nothing you don't
Add a rich text editor to Angular in minutes.
No license keys. No CDN links. No account. Just npm install scribejs-editor and wire up one component.
Angular + Scribe — common questions
How do I add a rich text editor to an Angular app?
Install scribejs-editor, expose the host element with @ViewChild and ElementRef, then call Scribe.init(this.host.nativeElement) in ngOnInit and editor.destroy() in ngOnDestroy. Scribe is framework-agnostic and only needs a DOM element, so no Angular-specific adapter is required.
Is there an official Angular package for Scribe?
No. Scribe ships a single framework-agnostic, DOM-based API (Scribe.init / editor.destroy). The Angular component and ControlValueAccessor shown here are standard wrapper patterns you write yourself around that API — there is no official @scribejs/angular adapter.
How do I bind Scribe to ngModel or reactive forms?
Implement ControlValueAccessor on your wrapper component and register it with NG_VALUE_ACCESSOR. Push editor changes through onChange in Scribe’s onChange callback, and apply incoming values with editor.setHTML() inside writeValue(). The component then works with [(ngModel)] and formControlName.
Why call destroy() in ngOnDestroy?
Scribe attaches DOM listeners and toolbar elements when it initializes. Calling editor.destroy() in ngOnDestroy removes them so the editor is cleaned up when Angular tears down the component, preventing leaked listeners and duplicate toolbars on route changes.
Also compare Scribe with:
Use Scribe in your framework: