Skip to content

@scryb-editor/angular

Terminal window
pnpm add @scryb-editor/angular @scryb-editor/core @scryb-editor/extensions @scryb-editor/themes

The root editor component. Bind content via [(content)] (using ngModel) and pass all configuration through [options].

<scryb-editor
[(content)]="htmlContent"
[options]="editorConfig"
(contentChange)="onContentChange($event)"
(editorCreated)="onEditorReady($event)"
/>
import type { ScrybEditorConfig } from "@scryb-editor/core";
readonly editorConfig: Partial<ScrybEditorConfig> = {
locale: "en",
theme: "light",
placeholder: "Start writing...",
toolbar: { show: true },
bubbleMenu: { show: true },
sideMenu: { enabled: true },
slashCommands: { enabled: true },
characterCount: { show: true },
};
Input Type Default Description
content string "" HTML content of the editor. Bindable via ngModel.
options Partial<ScrybEditorConfig> {} All editor configuration — see ScrybEditorConfig below.
Output Type Description
contentChange EventEmitter<string> Emitted on every content change with the latest HTML string
editorCreated EventEmitter<Editor> Emitted once when the Tiptap Editor instance is ready
editorUpdate EventEmitter<EditorUpdateEvent> Emitted on every editor transaction with update metadata
editorFocus EventEmitter<EditorFocusEvent> Emitted when the editor receives focus
editorBlur EventEmitter<EditorFocusEvent> Emitted when the editor loses focus
imageError EventEmitter<string> Emitted when an image cannot be inserted — too large, wrong type, unreadable, or a rejected upload. Without a listener the file simply does not appear; the editor has no opinion on how a rejection should look. React’s equivalent is the onImageError prop

All configuration is passed via [options] as Partial<ScrybEditorConfig>. The type is defined in @scryb-editor/core and shared with the React adapter.

Field Type Default Description
toolbar { items?: ToolbarItemKey[], maxVisibleItems?: number, show?: boolean } 26 controls, first 14 entries inline, show: true Toolbar items, overflow threshold, and visibility. The threshold counts entries including separators — DEFAULT_TOOLBAR_MAX_VISIBLE_ITEMS is 14, which is one row at ~550px
bubbleMenu { items?: BubbleMenuItemKey[], maxVisibleItems?: number, show?: boolean } 21 controls, first 7 entries inline, show: true Text selection bubble menu. Defaults carry only selection-scoped items — see below
slashCommands { commands?: SlashCommandItem[], enabled?: boolean } enabled: true Slash command menu
theme "light" | "dark" | "auto" "light" Color theme
locale LocaleCode "en" UI locale. Built-in: "en", "fr", "pt", "es", "zh". Any other BCP-47 code works once registered via registerLocale() or translations.
sideMenu { enabled?: boolean, buttons?: boolean } enabled: true Floating side menu
characterCount { show?: boolean } show: true Character/word count footer
image ImageUploadConfig see below Image upload constraints and drag-drop
height { minHeight?: number, height?: number, maxHeight?: number } minHeight: 200 Editor height constraints
placeholder string "" Placeholder text
editable boolean true Read-only when false
maxCharacters number | null null Character limit
imageBubbleMenu ImageBubbleMenuConfig & { show?: boolean } show: true Image bubble menu
officePaste { enabled?: boolean } enabled: true Office/Word paste cleanup
hideWhenInactive boolean false Drop all chrome when the editor is not focused — toolbar, card border, background and shadow. Clicking the content, the toolbar or a portaled menu keeps it active; dragging the scrollbar does not collapse it.
uniqueId { types?: string[] } omit to disable Stable id attributes on listed node types (default ["heading"] when provided). Required for toc.
typography { enabled?: boolean } { enabled: true } Smart quotes, em/en dashes, ellipsis, arrows, fractions. Set { enabled: false } to opt out. (default-on)
invisibleCharacters { enabled?: boolean } omit to disable Toolbar-toggleable pilcrow/space/tab glyphs.
youtube { enabled?: boolean } omit to disable YouTube video embeds via slash command.
wordCount { display?: "words" | "characters" | "both" | "none", position?: "footer" | "none" } display: "words", position: "footer" Live word/character count widget in the editor footer.
autosave { saveFn: (content) => Promise<void>, debounceMs?: number, format?: "json" | "html", onStatusChange?: (status) => void } debounceMs: 1000, format: "json" Debounced save handler + <scryb-save-status> footer widget. saveFn is required when the key is present.
details { enabled?: boolean, toggleAriaLabel?: string } omit to disable Collapsible toggle blocks (editor + viewer).
toc { enabled?: boolean, levels?: readonly number[], scrollContainer?: HTMLElement | string, callbacks?: TocCallbacks } levels: [1, 2, 3], scrollContainer: window Table of Contents extension. Editor-only. Requires uniqueId. Pass a createTocCallbackStore() for live updates.
emoji { enabled?: boolean, emojis?: readonly EmojiItem[], callbacks?: EmojiCallbacks } full emojibase-data dataset :shortcode typeahead + toolbar/bubble button with popular-first cold-start picker. Editor-only.
mention { enabled?: boolean, char?: string, items?: readonly MentionItem[], limit?: number, callbacks?: MentionCallbacks } char: "@", limit: 10 @user typeahead popup. MentionItem = { id: string; label: string }. Editor-only.

Any field not specified falls back to DEFAULT_EDITOR_CONFIG from @scryb-editor/core.

Toolbar, bubble menu and slash menu are split by what a control acts on, not by what it does. The defaults follow the split; the item keys do not enforce it, so any key can go anywhere you want it.

Surface Carries Constant
Toolbar Everything — selection, block, document, insertion DEFAULT_TOOLBAR_ORDER
Bubble menu Only what transforms the selection that summoned it DEFAULT_BUBBLE_MENU_ITEMS
Slash menu / side-menu + Insertion at the cursor defaultSlashCommands()

So undo, redo, image, table and horizontalRule are absent from the bubble menu defaults: the first two walk document history (and Mod-Z reaches them from anywhere), the last three insert at the cursor rather than transform a selection. All five stay valid BubbleMenuItemKeys — list them in config.bubbleMenu.items and they come back:

bubbleMenu: {
items: [...DEFAULT_BUBBLE_MENU_ITEMS, "separator", "undo", "redo"],
}

An editor running with toolbar: { show: false } still reaches every insertion through the slash menu, which the side menu’s + button also opens.

Field Default Description
maxSize 5 Maximum file size in MB
maxWidth / maxHeight 1920 / 1080 Target box for compression; the aspect ratio is preserved
allowedTypes ["image/jpeg", "image/png", "image/gif", "image/webp"] Exact MIME types accepted. Anything else is rejected before insert
enableDragDrop true Accept image files dropped onto the editor. With it off, a dropped file is refused and the browser is still stopped from navigating to it
compressImages true Re-encode through a canvas at quality. Set false to insert the original bytes untouched
quality 0.8 Compression quality, 0–1. Ignored when compressImages is false
upload omit to embed Sends the file to your storage and returns its URL. See below
showPreview true Reserved. The shipped panel always previews a chosen file; the option is read by <tiptap-image-upload>, which the editor does not currently render
multiple false Reserved. Same component; the shipped panel takes one file at a time

Every path into the editor honours these: the toolbar panel, the bubble menu, a file dropped on the editor, and an image pasted from the clipboard. Dragging blocks inside the document is unaffected — only drags carrying OS files are intercepted.

Without upload, an image is embedded in the document as a base64 data URL. That keeps the editor self-contained with nothing to configure, and it is fine for a demo or a short note. It is not a storage strategy: a 3 MB photo becomes roughly 4 MB of string inside the document, saved to your database, re-parsed on every load, re-sent on every autosave.

upload takes the file and gives back a URL. Scryb never touches the bytes afterwards and has no opinion on where they go.

config: ScrybEditorConfig = {
image: {
upload: async (file, { onProgress, signal }) => {
const body = new FormData();
body.append("file", file);
const response = await fetch("/api/images", { method: "POST", body, signal });
if (!response.ok) throw new Error("Upload failed");
onProgress(100);
const { url } = await response.json();
return url;
},
},
};

Return a string, or { src, width?, height? } when the server resized or converted the file and knows better than the browser did.

onProgress takes 0–100 and drives the placeholder’s progress bar. Leave it uncalled if your transport cannot measure progress — the bar stays indeterminate rather than claiming a number it does not have. signal aborts if the editor is destroyed mid-upload; forward it to fetch or wire it to XMLHttpRequest.abort().

While the handler runs, a placeholder holds the spot in the document. It is a decoration, not a node, so an in-flight upload never reaches getHTML(), an autosave, or your database as a half-finished image. Its position is tracked through every edit, so the image lands where it was promised even if the user typed several paragraphs above it in the meantime. If the handler rejects, the placeholder is removed, nothing is inserted, and the message reaches (imageError).

Several opt-in features ship a standalone component that pairs with its corresponding ScrybEditorConfig key. Each component imports from @scryb-editor/angular.

Live word/character count widget. Auto-rendered in the editor footer when config.wordCount.position === "footer" (default). Import ScrybWordCountComponent if you need to render it manually.

Autosave status widget (idle / dirty / saving / saved / error). Auto-rendered in the footer when config.autosave is configured. Import ScrybSaveStatusComponent if you need to render it manually.

Table-of-Contents sidebar. Import ScrybTocComponent and render alongside the editor:

<scryb-toc [editor]="editor" [callbacks]="tocCallbacks" />
import { ScrybTocComponent } from "@scryb-editor/angular";
import { createTocCallbackStore, type TocCallbacks } from "@scryb-editor/core";
readonly tocCallbacks: TocCallbacks = createTocCallbackStore();
readonly editorConfig: Partial<ScrybEditorConfig> = {
uniqueId: { types: ["heading"] },
toc: { enabled: true, callbacks: this.tocCallbacks },
};

Pass the SAME tocCallbacks reference to both <scryb-toc> and the config. The component patches callbacks.onUpdate on mount and restores a noop on cleanup.

The TOC, emoji, and mention features use a callback-store pattern to decouple the Tiptap extension lifecycle from the adapter popup. Create a store once, then pass it to both the config and the popup component.

import {
createTocCallbackStore,
createEmojiCallbackStore,
createMentionCallbackStore,
} from "@scryb-editor/core";
const tocCallbacks = createTocCallbackStore();
const emojiCallbacks = createEmojiCallbackStore();
const mentionCallbacks = createMentionCallbackStore();

Emoji and mention popups are mounted automatically by ScrybEditorComponent when the corresponding config keys are enabled — consumers typically only need to create the store and pass it in.

Root singleton managing UI locale. Built-in: "en", "fr", "pt". setLocale(), getSupportedLocales(), and addTranslations() all accept any LocaleCode (any BCP-47 string), not just the three built-ins — pair with registerLocale() from @scryb-editor/core, or pass config.translations on <scryb-editor> for a per-instance catalog. See the Internationalization guide for the full picture.

import { ScrybI18nService, SupportedLocale } from "@scryb-editor/angular";
private readonly i18n = inject(ScrybI18nService);
setLanguage(locale: SupportedLocale) {
this.i18n.setLocale(locale);
}

Stateless command facade. All methods take an Editor instance as the first argument.

import { EditorCommands } from "@scryb-editor/core";
const commands = new EditorCommands();
commands.toggleBold(editor);

Instance-scoped service (not root) that tracks which dropdown is open within an editor instance. Prevents multiple dropdowns from opening simultaneously. Provided automatically by ScrybEditorComponent.

Instance-scoped service that handles image upload, compression (canvas-based), and validation.

The package re-exports default configurations from @scryb-editor/core:

import {
DEFAULT_EDITOR_CONFIG,
DEFAULT_TOOLBAR_ORDER,
DEFAULT_BUBBLE_MENU_ITEMS,
DEFAULT_SLASH_COMMANDS,
DEFAULT_FONT_SIZES,
DEFAULT_FONT_FAMILIES,
DEFAULT_TEXT_COLORS,
} from "@scryb-editor/angular";
import type { ScrybEditorConfig } from "@scryb-editor/core";
import type { ScrybTheme, SupportedLocale, ToolbarItemKey, BubbleMenuItemKey } from "@scryb-editor/core";