Skip to content

Angular Rich Text Editor: Install and Configure

Scryb is a rich text editor built on Tiptap and ProseMirror. The Angular adapter — @scryb-editor/angular — ships a full-featured standalone component with toolbar, bubble menus, slash commands, side menu, and image upload built in.

  • Angular 20 or higher (@scryb-editor/angular declares >=20.0.0 <23.0.0 for @angular/core, @angular/common, and @angular/forms)
  • Node.js 24 or higher
  • A working Angular application (ng new or existing project)
  • @scryb-editor/angular is commercially licensed; a subscription provides the signed key (see Licensing)
Terminal window
npm install @scryb-editor/angular @scryb-editor/core @scryb-editor/extensions @scryb-editor/themes

The editor styles must be loaded globally — they cannot be scoped inside a single component. Choose one of the two methods below.

Section titled “Option A: angular.json styles array (recommended)”

Open angular.json and add the stylesheet to your application’s styles array:

angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"styles": [
"node_modules/@scryb-editor/themes/dist/all.css",
"src/styles.css"
]
}
}
}
}
}
}

Add the import to your root styles.css (or styles.scss):

src/styles.css
@import "@scryb-editor/themes/all";

Import ScrybEditorComponent into your standalone component and add the <scryb-editor> selector to your template.

src/app/my-editor.component.ts
import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { ScrybEditorComponent } from "@scryb-editor/angular";
import type { ScrybEditorConfig } from "@scryb-editor/core";
@Component({
selector: "app-my-editor",
standalone: true,
imports: [ScrybEditorComponent, FormsModule],
template: `
<scryb-editor
[(ngModel)]="content"
[options]="editorConfig"
(contentChange)="onContentChanged($event)"
/>
`,
})
export class MyEditorComponent {
content = signal("");
readonly editorConfig: Partial<ScrybEditorConfig> = {
placeholder: "Start writing...",
};
onContentChanged(html: string): void {
console.log("Editor content:", html);
}
}

All editor configuration is passed through the [options] input as a Partial<ScrybEditorConfig> object. Import the type from @scryb-editor/core.

Display toolbar labels and tooltips in a specific language. Supported values: "en" (default), "fr", "pt" (Brazilian Portuguese), "es", "zh" (Simplified Chinese).

readonly editorConfig: Partial<ScrybEditorConfig> = {
locale: "fr",
};

Need a language beyond the five built-ins, or want to override a few strings without forking a locale? See the Internationalization guide for registerLocale() and config.translations.

Control light/dark appearance. Supported values: "light", "dark", "auto" (follows prefers-color-scheme).

readonly editorConfig: Partial<ScrybEditorConfig> = {
theme: "dark",
};

Customize which toolbar items are shown and their order:

src/app/my-editor.component.ts
readonly editorConfig: Partial<ScrybEditorConfig> = {
toolbar: {
items: ["bold", "italic", "underline", "separator", "heading1"],
show: true,
},
};
<scryb-editor [options]="editorConfig" />

Control the minimum and maximum height of the editor area:

readonly editorConfig: Partial<ScrybEditorConfig> = {
height: {
minHeight: 200,
maxHeight: 600,
},
};

Set the placeholder text shown when the editor is empty:

readonly editorConfig: Partial<ScrybEditorConfig> = {
placeholder: "Write something amazing...",
};

Everything on this page so far is configuration: a closed set of features the editor already registers. For anything outside it — a custom node or mark, a ProseMirror plugin, an official Tiptap extension this adapter does not configure — use the extensions input. It takes an array of Tiptap extensions and appends them after the Scryb defaults.

src/app/my-editor.component.ts
import { Component } from "@angular/core";
import { ScrybEditorComponent } from "@scryb-editor/angular";
import type { AnyExtension } from "@tiptap/core";
import { MyCustomBlock } from "./my-custom-block";
@Component({
selector: "app-my-editor",
standalone: true,
imports: [ScrybEditorComponent],
template: `<scryb-editor [extensions]="extensions" />`,
})
export class MyEditorComponent {
readonly extensions: AnyExtension[] = [MyCustomBlock];
}

Because they are appended, an extension here extends the editor rather than displacing part of it. The resulting order is Scryb defaults, then yours, then the adapter’s own — the same order @scryb-editor/react produces from its extensions prop.

Scryb ships a catalog of opt-in features gated by ScrybEditorConfig keys. Omitting a key leaves the corresponding Tiptap extension unregistered — zero runtime cost. The subsections below show the minimal config for each feature. typography and taskList are default-on (no config needed).

Debounced save handler that fires on content change. <scryb-save-status> is auto-rendered in the editor footer when autosave is configured.

readonly editorConfig: Partial<ScrybEditorConfig> = {
autosave: {
saveFn: async (content) => {
await fetch("/api/save", { method: "POST", body: JSON.stringify(content) });
},
debounceMs: 1000,
format: "json",
},
};
// <scryb-save-status> is auto-rendered in the editor footer.

Sidebar component with scroll-tracking. Requires uniqueId for stable heading IDs. Create a TocCallbacks store once and pass the same reference to both the config and <scryb-toc>.

src/app/my-editor.component.ts
import { ScrybEditorComponent, ScrybTocComponent } from "@scryb-editor/angular";
import { createTocCallbackStore, type TocCallbacks } from "@scryb-editor/core";
@Component({
imports: [ScrybEditorComponent, ScrybTocComponent],
template: `
<div class="layout">
<scryb-toc [editor]="editor" [callbacks]="tocCallbacks" />
<scryb-editor [options]="editorConfig" />
</div>
`,
})
export class MyEditorComponent {
readonly tocCallbacks: TocCallbacks = createTocCallbackStore();
readonly editorConfig: Partial<ScrybEditorConfig> = {
uniqueId: { types: ["heading"] },
toc: { enabled: true, callbacks: this.tocCallbacks },
};
}

@user typeahead popup. The popup is auto-mounted by ScrybEditorComponent — consumers only need to create the callback store and supply the item list.

import { createMentionCallbackStore } from "@scryb-editor/core";
readonly mentionCallbacks = createMentionCallbackStore();
readonly editorConfig: Partial<ScrybEditorConfig> = {
mention: {
enabled: true,
char: "@",
items: [{ id: "u1", label: "Alice" }, { id: "u2", label: "Bob" }],
callbacks: this.mentionCallbacks,
},
};

:shortcode typeahead plus toolbar/bubble button with a popular-first cold-start picker. Popup is auto-mounted by ScrybEditorComponent.

import { createEmojiCallbackStore } from "@scryb-editor/core";
readonly emojiCallbacks = createEmojiCallbackStore();
readonly editorConfig: Partial<ScrybEditorConfig> = {
emoji: { enabled: true, callbacks: this.emojiCallbacks },
};
// Type ":" inside the editor to open the picker, or click the emoji toolbar button.

Collapsible toggle blocks (editor + viewer). Insert via the slash menu.

readonly editorConfig: Partial<ScrybEditorConfig> = {
details: { enabled: true },
};
// Insert via slash menu → "Toggle" (Advanced group).

YouTube video embeds via slash command.

readonly editorConfig: Partial<ScrybEditorConfig> = {
youtube: { enabled: true },
};
// Insert via slash menu → "YouTube video" (Media group).

Live word/character count widget. <scryb-word-count> is auto-rendered in the editor footer when position === "footer" (default).

readonly editorConfig: Partial<ScrybEditorConfig> = {
wordCount: { display: "both", position: "footer" },
};
// <scryb-word-count> is auto-rendered in the editor footer.

Toggle pilcrow/space/tab glyphs from the toolbar. Add the invisibleCharacters toolbar item to expose the toggle.

readonly editorConfig: Partial<ScrybEditorConfig> = {
invisibleCharacters: { enabled: true },
toolbar: { items: ["bold", "italic", "separator", "invisibleCharacters"] },
};

Stable id attributes on listed node types — required by toc for deep-link anchors.

readonly editorConfig: Partial<ScrybEditorConfig> = {
uniqueId: { types: ["heading"] }, // default; also required by `toc`.
};

typography (smart quotes, dashes, ellipsis, arrows, fractions) and taskList (interactive checkbox lists) are enabled by default. Pass typography: { enabled: false } to disable typography transformations for legal/code-heavy content. taskList has no config key.

@scryb-editor/themes uses CSS custom properties under the .scryb-editor container class (automatically applied by ScrybEditorComponent). Set the theme via the [options] config:

readonly editorConfig: Partial<ScrybEditorConfig> = {
theme: "auto", // follows system preference
};

You can override individual design tokens in your global stylesheet:

src/styles.css
.scryb-editor {
--scryb-toolbar-bg: #f8f9fa;
--scryb-button-active-bg: #e9ecef;
--scryb-border-color: #dee2e6;
}

If you prefer to load only the styles you need:

/* Only design tokens */
@import "@scryb-editor/themes/tokens";
/* Only theme color schemes (light/dark) */
@import "@scryb-editor/themes/themes";
/* Only ProseMirror content styles */
@import "@scryb-editor/themes/content";

A fully configured editor component with locale, theme, and reactive form binding:

src/app/editor-page.component.ts
import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { ScrybEditorComponent } from "@scryb-editor/angular";
import type { ScrybEditorConfig } from "@scryb-editor/core";
@Component({
selector: "app-editor-page",
standalone: true,
imports: [ScrybEditorComponent, FormsModule],
template: `
<scryb-editor
[(ngModel)]="content"
[options]="editorConfig"
(contentChange)="onContentChanged($event)"
(editorReady)="onEditorReady()"
/>
<p>Characters: {{ charCount() }}</p>
`,
})
export class EditorPageComponent {
content = signal("");
charCount = signal(0);
readonly editorConfig: Partial<ScrybEditorConfig> = {
locale: "en",
theme: "auto",
placeholder: "Start writing...",
height: { minHeight: 300, maxHeight: 800 },
characterCount: { show: true },
toolbar: { show: true },
bubbleMenu: { show: true },
sideMenu: { enabled: true },
slashCommands: { enabled: true },
};
onContentChanged(html: string): void {
this.content.set(html);
}
onEditorReady(): void {
console.log("Editor is ready");
}
}