• Primary
    Surface
    PresetsAuraMaterialLaraNora
    Ripple
    RTL
    1. Philosophy
    2. Installation
    3. Configuration
    4. Playground
    1. Styled Mode
    2. Unstyled Mode
    3. Colors
    4. Tailwind CSS
    1. What's New
      1. Scheduler
      2. Text Editor
    2. Form
      1. AutoComplete
      2. CascadeSelect
      3. Checkbox
      4. ColorPicker
      5. DatePicker
      6. Editor
      7. FloatLabel
      8. IconField
      9. IftaLabel
      10. InputGroup
      11. InputMask
      12. InputNumber
      13. InputOtp
      14. InputText
      15. KeyFilter
      16. Knob
      17. Listbox
      18. MultiSelect
      19. Password
      20. RadioButton
      21. Rating
      22. Select
      23. SelectButton
      24. Slider
      25. Textarea
      26. ToggleButton
      27. ToggleSwitch
      28. TreeSelect
    3. Button
      1. Button
      2. SpeedDial
      3. SplitButton
    4. Data
      1. DataView
      2. OrderList
      3. OrgChart
      4. Paginator
      5. PickList
      6. Table
      7. Timeline
      8. Tree
      9. TreeTable
      10. VirtualScroller
    5. Panel
      1. Accordion
      2. Card
      3. Divider
      4. Fieldset
      5. Panel
      6. ScrollPanel
      7. Splitter
      8. Stepper
      9. Tabs
      10. Toolbar
    6. Overlay
      1. ConfirmDialog
      2. ConfirmPopup
      3. Dialog
      4. Drawer
      5. DynamicDialog
      6. Popover
      7. Tooltip
    7. File
      1. Upload
    8. Menu
      1. Breadcrumb
      2. ContextMenu
      3. Dock
      4. Menu
      5. Menubar
      6. MegaMenu
      7. PanelMenu
      8. TieredMenu
    9. Chart
      1. Chart.js
    10. Messages
      1. Message
      2. Toast
    11. Media
      1. Carousel
      2. Galleria
      3. Image
      4. ImageCompare
    12. Misc
      1. AnimateOnScroll
      2. AutoFocus
      3. Avatar
      4. Badge
      5. Bind
      6. BlockUI
      7. Chip
      8. ClassNames
      9. DragDrop
      10. Fluid
      11. FocusTrap
      12. Inplace
      13. MeterGroup
      14. ProgressBar
      15. ProgressSpinner
      16. Ripple
      17. ScrollTop
      18. Skeleton
      19. StyleClass
      20. Tag
      21. Terminal
    13. Utilities
      1. FilterService
      2. Overlay API
    1. Accessibility
    2. Animations
    3. PrimeFlex
    4. RTL
    1. bdSpark Icons
    2. Custom Icons
    1. Update SparkNG
  1. GitHub
  2. Discussions
  3. Primary
    Surface
    PresetsAuraMaterialLaraNora
    Ripple
    RTL

Text Editor

TextEditor is a ProseMirror based rich text editor built from headless parts with commands, format state, sanitization, uploads, tables, mentions, block mode and a plugin system.

Import #

TextEditor is available under the @bdspark/sparkng/texteditor entry point. Import TextEditorModule for every part, or import the standalone parts you mount. ProseMirror packages (prosemirror-model, prosemirror-state, prosemirror-view, …) are peer dependencies of the library and must be installed once per application.

// ng-modules
import { TextEditorModule } from '@bdspark/sparkng/texteditor';

// standalone, per part
import { TextEditorRoot } from '@bdspark/sparkng/texteditor';
import { TextEditorContent } from '@bdspark/sparkng/texteditor';
import { TextEditorToolbar } from '@bdspark/sparkng/texteditor';

// types
import type { TextEditorCommands, TextEditorFormatState } from '@bdspark/sparkng/types/texteditor';

Basic #

TextEditor is a ProseMirror based rich text editor built from headless parts. The root owns the document, commands and state; mount a p-texteditor-toolbar and a p-texteditor-content inside it and bind the value with [(value)]. The value is an HTML string in classic mode and an array of block HTML strings in block mode.

import { Component, signal } from '@angular/core';
import { TextEditorContent, TextEditorRoot, TextEditorToolbar } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'my-editor',
    standalone: true,
    imports: [TextEditorRoot, TextEditorToolbar, TextEditorContent, TextEditorToolbarDef],
    template: `
        <p-texteditor-root [(value)]="value" placeholder="Tell a story…">
            <p-texteditor-toolbar>
                <ng-template pTexteditorToolbarDef let-commands="commands" let-state="state">
                    <button (click)="commands.bold()" [attr.aria-pressed]="state.bold">B</button>
                    <button (click)="commands.italic()" [attr.aria-pressed]="state.italic">I</button>
                    <button (click)="commands.underline()" [attr.aria-pressed]="state.underline">U</button>
                </ng-template>
            </p-texteditor-toolbar>
            <p-texteditor-content />
        </p-texteditor-root>
    `
})
export class MyEditor {
    value = signal('<h2>Hello TextEditor</h2><p>Start writing…</p>');
}

Toolbar #

The static toolbar is a headless role="toolbar" surface: the pTexteditorToolbarDef template hands over commands and state already unwrapped. Toggle buttons bind [attr.aria-pressed] to the matching state flag, selects bind [value] and dispatch on change, and value inputs such as font family report null when the selection mixes values.

import { Component } from '@angular/core';
import { TextEditorContent, TextEditorRoot, TextEditorToolbar } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'my-editor',
    standalone: true,
    imports: [TextEditorRoot, TextEditorToolbar, TextEditorContent, TextEditorToolbarDef],
    template: `
        <p-texteditor-root [(value)]="value">
            <p-texteditor-toolbar>
                <ng-template pTexteditorToolbarDef let-commands="commands" let-state="state">
                    <select [value]="state.heading ?? ''" (change)="onChange($event, commands)">
                        <option value="">Paragraph</option>
                        <option value="1">Heading 1</option>
                        <option value="2">Heading 2</option>
                    </select>
                    <button (click)="commands.bold()" [attr.aria-pressed]="state.bold">B</button>
                    <button (click)="commands.italic()" [attr.aria-pressed]="state.italic">I</button>
                    <button (click)="commands.foregroundColor('#e11d48')"
                            [attr.aria-pressed]="state.foregroundColor === '#e11d48'">Red</button>
                    <button (click)="commands.bulletList()" [attr.aria-pressed]="state.bulletList">• List</button>
                    <button (click)="commands.textAlign('center')"
                            [attr.aria-pressed]="state.textAlign === 'center'">Center</button>
                    <button (click)="commands.insertHorizontalRule()">—</button>
                    <button (click)="commands.undo()" [disabled]="!state.canUndo">Undo</button>
                    <button (click)="commands.print()">Print</button>
                </ng-template>
            </p-texteditor-toolbar>
            <p-texteditor-content />
        </p-texteditor-root>
    `
})
export class MyEditor {
    value = signal('<p>hello</p>');

    onChange(event: Event, commands: TextEditorCommands) {
        const level = Number((event.target as HTMLSelectElement).value) || null;
        level === null ? commands.paragraph() : commands.heading(level);
    }
}

Context Toolbar #

The context toolbar floats over the selection. It opens on double-click or text selection, owns click-outside dismissal, and ships an overflow sub-part. Mounting p-texteditor-context-toolbar enables the feature — without it, selections never trigger the request.

import { Component, signal } from '@angular/core';
import { TextEditorContent, TextEditorContextToolbar, TextEditorContextToolbarMore, TextEditorRoot } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'my-editor',
    standalone: true,
    imports: [TextEditorRoot, TextEditorContextToolbar, TextEditorContextToolbarMore, TextEditorContent, TextEditorContextToolbarDef, TextEditorContextToolbarMoreDef],
    template: `
        <p-texteditor-root [(value)]="value">
            <p-texteditor-context-toolbar>
                <ng-template pTexteditorContextToolbarDef let-commands="commands" let-state="state" let-toggleMore="toggleMore" let-moreActive="moreActive">
                    <button (click)="commands.bold()" [attr.aria-pressed]="state.bold">B</button>
                    <button (click)="commands.italic()" [attr.aria-pressed]="state.italic">I</button>
                    <button (click)="toggleMore()" [attr.aria-pressed]="moreActive">⋯</button>
                </ng-template>
                <p-texteditor-context-toolbar-more>
                    <ng-template pTexteditorContextToolbarMoreDef let-commands="commands" let-state="state">
                        <button (click)="commands.strikethrough()" [attr.aria-pressed]="state.strikethrough">S</button>
                    </ng-template>
                </p-texteditor-context-toolbar-more>
            </p-texteditor-context-toolbar>
            <p-texteditor-content />
        </p-texteditor-root>
    `
})
export class MyEditor {
    value = signal('<p>Select some text…</p>');
}

Formats #

Inline Formats

Bold, italic, underline, strikethrough, superscript, subscript and inline code are marks toggled with commands.*() and mirrored by their boolean state flag. A collapsed selection sets the typing mark, so the next characters come out formatted. Subscript and superscript are mutually exclusive.

Font & Colors

commands.fontFamily(value), commands.fontSize(value), commands.foregroundColor(color) and commands.backgroundColor(color) apply value marks. When the selection spans more than one value the state reports null, so a select reads as cleared instead of guessing the first value.

// toggle marks
<button (click)="commands.bold()" [attr.aria-pressed]="state.bold">B</button>
<button (click)="commands.subscript()" [attr.aria-pressed]="state.subscript">x₂</button>

// value marks — null on mixed selections
<select [value]="state.fontFamily || ''" (change)="commands.fontFamily($any($event.target).value)">
    <option value="Arial">Arial</option>
    <option value="Georgia">Georgia</option>
</select>

// swatch buttons
<button (click)="commands.foregroundColor('#e11d48')"
        [attr.aria-pressed]="state.foregroundColor === '#e11d48'">A</button>

Structure #

Headings & Blocks

commands.heading(level) converts the block; calling it with the active level toggles back to a paragraph. commands.blockquote() and commands.codeBlock() toggle their block types — a code block holds plain text and accepts no inline marks. commands.insertHorizontalRule() has no state flag.

Lists

commands.bulletList(), commands.orderedList() and commands.checkList() toggle the matching list. Checklist items serialize as ul[data-p-checked-list] with li[data-p-checked] and their checkbox toggles with a click. checklistPlaceholder labels empty checklist items.

Text Align

commands.textAlign('left' | 'center' | 'right' | 'justify') sets the block alignment; the active value is reported by state.textAlign.

// headings — re-clicking the active level falls back to paragraph
onChange(event: Event, commands: TextEditorCommands) {
    const level = Number((event.target as HTMLSelectElement).value) || null;
    level === null ? commands.paragraph() : commands.heading(level);
}

// lists & blocks
commands.bulletList();
commands.orderedList();
commands.checkList();          // ul[data-p-checked-list]
commands.blockquote();
commands.codeBlock();
commands.insertHorizontalRule();

// text align
commands.textAlign('center');

Link #

Links are managed with three commands. insertLink(url, text?) wraps the selection — or inserts text when the selection is collapsed. updateLink(url) and removeLink() detect the link boundaries around the cursor, so the selection does not have to cover the whole link. state.link and state.linkUrl report the active link. Only http, https, mailto, tel, ftp and relative URLs pass sanitization; unsafe schemes drop the link mark but keep the text.

commands.insertLink('https://example.com');          // wraps the selection
commands.insertLink('https://example.com', 'label'); // label when collapsed
commands.updateLink('https://other.com');            // link around the cursor
commands.removeLink();                               // unwraps it

// state
<button (click)="commands.removeLink()" [disabled]="!state.link">Remove link</button>

Image #

Images enter the document two ways: commands.insertImage(src) inserts directly from a URL, while commands.uploadImages() — or dropping an image on the content — opens the upload surface. The handler contract is (file, {onProgress, signal}) => Promise<url>; forward the AbortSignal to fetch so dismissing cancels the in-flight request. Validation (maxFileCount, maxFileSize, allowedTypes) is a UX guard — the server must re-validate. Data URLs accept raster images but never SVG.

import { TEXTEDITOR_FILE_SIZE, TextEditorModule } from '@bdspark/sparkng/texteditor';

readonly oneMB = TEXTEDITOR_FILE_SIZE.ONE_MB;

readonly uploadHandler = async (file: File, { onProgress, signal }: any): Promise<string> => {
    return uploadToServer(file, { signal, onProgress }); // forward the AbortSignal
};

onFileReject({ files, reason }: { files: File[]; reason: string }) {
    // reason: 'max-file-count' | 'max-file-size' | 'file-type'
}

Document #

p-texteditor-document-upload mirrors the image part for non-image files: every uploaded file is inserted into the content as a link. Drop a non-image file on the content or call commands.uploadDocuments(). Defaults accept .pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.zip with a 10MB ceiling; both are configurable through the part inputs.

// mirrors the image part; inserts each uploaded file as a link
<p-texteditor-document-upload [handler]="uploadHandler"
                              [maxFileCount]="3"
                              [maxFileSize]="TEXTEDITOR_FILE_SIZE.TEN_MB">
    <p-texteditor-document-upload-dropzone />
    <p-texteditor-document-upload-progress />
</p-texteditor-document-upload>

Table #

Insert with commands.table(rows, cols). Four floating parts manage the active table: p-texteditor-table-controls renders the add row/column buttons and the column/row trigger dots, p-texteditor-table-column-menu, -row-menu and -cell-menu open the matching action popovers with a shared submenu controller keeping both halves in lock-step. Column widths honor minTableColumnWidth (40px) and defaultTableColumnWidth (120px).

commands.table(3, 3); // insert

// floating controls
<p-texteditor-table-controls>
    <ng-template pTexteditorTableControlsDef let-position="position" let-commands="commands">
        @if (position) {
            <button (click)="commands.addColumn()">+ column</button>
            <button (click)="commands.addRow()">+ row</button>
        }
    </ng-template>
</p-texteditor-table-controls>

// column menu with a shared submenu
<button role="menuitem" (click)="commands.insertBefore(); dismiss()">Insert left</button>
<button role="menuitem" (click)="submenu.open('align')">Align…</button>

Mention #

Mounting p-texteditor-mention-menu enables the @ type-ahead in the runtime. The handler resolves candidates for the current query (sync or async), filterField drives substring filtering, and template renders the inserted chip. Keyboard support follows the ARIA combobox pattern: arrows move, Enter accepts, Escape dismisses.

import { Component, signal } from '@angular/core';
import { TextEditorContent, TextEditorMentionMenu, TextEditorRoot } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'my-editor',
    standalone: true,
    imports: [TextEditorRoot, TextEditorMentionMenu, TextEditorContent, TextEditorMentionMenuDef],
    template: `
        <p-texteditor-root [(value)]="value" placeholder="Type @ to mention a user">
            <p-texteditor-mention-menu [handler]="mentionHandler" filterField="name" [template]="mentionTemplate">
                <ng-template pTexteditorMentionMenuDef let-items="items" let-commands="commands" let-filterText="filterText">
                    @if (items.length === 0) {
                        <div>No matches for "{{ filterText }}"</div>
                    }
                    @for (item of items; track item.id) {
                        <button role="option" (click)="commands.select(item)">{{ item.name }}</button>
                    }
                </ng-template>
            </p-texteditor-mention-menu>
            <p-texteditor-content />
        </p-texteditor-root>
    `
})
export class MyEditor {
    value = signal<string | undefined>(undefined);
    mentionHandler = (query: string) => USERS;
    mentionTemplate = (data: (typeof USERS)[number]) => `@${data.name}`;
}

Utilities #

History

The runtime owns the history stack; widgets read commands.undo()/redo() and the availability flags state.canUndo / state.canRedo.

Print

commands.print() prints the sanitized document content through a sandboxed iframe — always available, no state flag.

<button (click)="commands.undo()" [disabled]="!state.canUndo" aria-label="Undo">↶</button>
<button (click)="commands.redo()" [disabled]="!state.canRedo" aria-label="Redo">↷</button>
<button (click)="commands.print()" aria-label="Print">Print</button>

Navigator #

The navigator is a heading-outline minimap built from three parts. Mounting p-texteditor-navigator enables heading tracking (it renders nothing); the trigger is a focusable rail opening on hover, focus, Enter, Space or ArrowDown; the menu lists the outline with scrollTo(pos) navigation and highlights the active heading while the document scrolls.

0 headings
import { Component, signal } from '@angular/core';
import { TextEditorContent, TextEditorNavigator, TextEditorNavigatorMenu, TextEditorNavigatorTrigger, TextEditorRoot } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'my-editor',
    standalone: true,
    imports: [TextEditorRoot, TextEditorNavigator, TextEditorNavigatorTrigger, TextEditorNavigatorMenu, TextEditorContent, TextEditorNavigatorTriggerDef, TextEditorNavigatorMenuDef],
    template: `
        <p-texteditor-root [(value)]="value" [navigator]="true">
            <p-texteditor-navigator>
                <p-texteditor-navigator-trigger>
                    <ng-template pTexteditorNavigatorTriggerDef let-headings="headings">
                        <div role="button" aria-label="Document outline">{{ headings.length }} headings</div>
                    </ng-template>
                </p-texteditor-navigator-trigger>
                <p-texteditor-navigator-menu>
                    <ng-template pTexteditorNavigatorMenuDef let-headings="headings" let-activeIndex="activeIndex" let-scrollTo="scrollTo">
                        @for (h of headings; track h.pos; let i = $index) {
                            <button role="menuitem" [attr.aria-current]="i === activeIndex ? 'true' : null"
                                    (click)="scrollTo(h.pos)">{{ h.text }}</button>
                        }
                    </ng-template>
                </p-texteditor-navigator-menu>
            </p-texteditor-navigator>
            <p-texteditor-content height="300px" />
        </p-texteditor-root>
    `
})
export class MyEditor {
    value = signal('<h1>Doc</h1><p>…</p>');
}

Events #

Document edits flow through [(value)] / valueChange. selectionUpdate makes caret movement observable without polling, editorCreate fires once when the view is ready, and editorFocus / editorBlur track focus. valueChangeDebounce coalesces value emissions (150-300ms for large documents).

Event log
import { Component, ViewChild, signal } from '@angular/core';
import { TextEditorContent, TextEditorRoot, TextEditorModule } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'events-demo',
    standalone: true,
    imports: [TextEditorModule, TextEditorToolbarDef],
    template: `
        <p-texteditor-root #root [(value)]="value"
                           (editorCreate)="onCreate()"
                           (selectionUpdate)="onSelection()"
                           (editorFocus)="onFocus()"
                           (editorBlur)="onBlur()">
            <p-texteditor-content />
        </p-texteditor-root>
    `
})
export class EventsDemo {
    @ViewChild('root') root!: TextEditorRoot;
    value = signal('');

    onMarkdown() {
        console.log(this.root.getMarkdown());
    }

    onCreate() {}
    onSelection() {}
    onFocus() {}
    onBlur() {}
}

Block Mode #

mode="block" switches the value to string[] — one HTML string per block. The runtime owns drag reordering, the hover bar and slash detection. Mount p-texteditor-block-controls for the add/drag handles, p-texteditor-block-menu for per-block options (with a Turn into submenu), and p-texteditor-slash-menu to enable the / command palette — it is an ARIA combobox whose entries render as role="option" buttons.

import { Component, signal } from '@angular/core';
import { TextEditorBlockControls, TextEditorBlockMenu, TextEditorContent, TextEditorRoot, TextEditorSlashMenu } from '@bdspark/sparkng/texteditor';

@Component({
    selector: 'my-block-editor',
    standalone: true,
    imports: [TextEditorRoot, TextEditorBlockControls, TextEditorBlockMenu, TextEditorSlashMenu, TextEditorContent, TextEditorBlockControlsDef, TextEditorBlockMenuDef, TextEditorBlockSubmenuDef, TextEditorSlashMenuDef],
    template: `
        <p-texteditor-root [(value)]="blocks" mode="block" placeholder="Type '/' for commands…">
            <p-texteditor-block-controls>
                <ng-template pTexteditorBlockControlsDef
                             let-index="index" let-addBlock="addBlock"
                             let-onDragStart="onDragStart" let-onDragEnd="onDragEnd"
                             let-onDragHandleClick="onDragHandleClick">
                    <button (click)="addBlock(index)" aria-label="Add block">+</button>
                    <button draggable="true"
                            (dragstart)="onDragStart(index, $event)"
                            (dragend)="onDragEnd()"
                            (click)="onDragHandleClick(index, $event)"
                            aria-label="Block options">⋮⋮</button>
                </ng-template>
            </p-texteditor-block-controls>
            <p-texteditor-block-menu>
                <ng-template pTexteditorBlockMenuDef let-commands="commands" let-submenu="submenu" let-dismiss="dismiss">
                    <button role="menuitem" (click)="commands.duplicate(); dismiss()">Duplicate</button>
                    <button role="menuitem" (click)="submenu.open('turnInto')">Turn into…</button>
                    <button role="menuitem" (click)="commands.deleteBlock(); dismiss()">Delete</button>
                </ng-template>
            </p-texteditor-block-menu>
            <p-texteditor-slash-menu placeholder="Type to search">
                <ng-template pTexteditorSlashMenuDef let-commands="commands" let-filterText="filterText" let-dismiss="dismiss">
                    <button role="option" [hidden]="!'heading 1'.includes(filterText)"
                            (click)="commands.heading(1); dismiss()">Heading 1</button>
                </ng-template>
            </p-texteditor-slash-menu>
            <p-texteditor-content height="400px" />
        </p-texteditor-root>
    `
})
export class MyBlockEditor {
    blocks = signal(['<h2>Welcome</h2>', '<p>Each entry is a block.</p>']);
}

Plugins #

A plugin namespaces imperative commands onto pluginCommands[name] with defineTextEditorPlugin(name, install, options?). Pass definitions — or [plugin, options] tuples — to the [plugins] input; the set is frozen once the editor state is created. The install context exposes getSelectedText, replaceSelection, getView, registerProseMirrorPlugin and onUnmounted; the third argument can contribute schema nodes/marks and ProseMirror plugins before the first render.

import { defineTextEditorPlugin, TEXTEDITOR_CONTEXT, TextEditorModule } from '@bdspark/sparkng/texteditor';
import { inject, signal, effect, Component } from '@angular/core';

export const uppercasePlugin = defineTextEditorPlugin('uppercase', ({ getSelectedText, replaceSelection }) => ({
    commands: {
        toUpperCase: () => {
            const text = getSelectedText();
            if (text) replaceSelection(text.toUpperCase());
        }
    }
}));

// registration — options via [plugin, options] tuples
<p-texteditor-root [plugins]="[[characterCountPlugin, { limit: 280 }], uppercasePlugin]">
    <p-texteditor-toolbar>
        <ng-template pTexteditorToolbarDef let-plugins="plugins">
            <button (click)="plugins['uppercase']?.['toUpperCase']()">UPPERCASE</button>
        </ng-template>
    </p-texteditor-toolbar>
    <p-texteditor-content />
</p-texteditor-root>

// consuming plugin commands from a projected component
export class CounterDisplay {
    readonly ctx = inject(TEXTEDITOR_CONTEXT);
    readonly stats = signal({ characters: 0, words: 0, overLimit: false });

    constructor() {
        effect(() => {
            const cmds = this.ctx.pluginCommands();
            cmds['characterCount']?.onChange((s) => this.stats.set(s));
        });
    }
}

Accessibility #

Screen Reader

The editing region is a ProseMirror contenteditable exposed as role="textbox" with aria-multiline="true". An accessible name is required (WCAG 4.1.2) — provide ariaLabel or ariaLabelledby on the root; the default label is "Rich text editor". readonly and disabled both set aria-readonly="true".

Toolbars render role="toolbar" with aria-orientation and an aria-label. Icon-only buttons carry aria-label, toggles carry aria-pressed. While a slash or mention type-ahead is open the region becomes a combobox (aria-expanded, aria-activedescendant against role="listbox"/role="option" entries); block and table menus stay role="menu"/menuitem. Upload progress is wrapped in role="status" with aria-live="polite".

Keyboard Support

KeyFunction
TabMoves focus between toolbar buttons; indents list items and jumps to the next table cell.
Shift + TabOutdents list items and jumps to the previous table cell.
EnterNew line; splits list items; accepts the highlighted type-ahead option.
Shift + EnterInserts a hard break.
Arrow KeysMove the caret; navigate type-ahead and menu entries; move between table cells at boundaries.
Ctrl/Cmd + B / I / UBold, italic and underline.
Ctrl/Cmd + ZUndo; Shift + Ctrl/Cmd + Z (or Ctrl/Cmd + Y) redoes.
Shift + Ctrl + 1…6Applies heading levels 1 through 6.
EscapeCloses popovers (type-ahead, context toolbar, block and table menus) and returns focus to the editor.
/Opens the slash command palette when its part is mounted (block mode).
@Opens the mention menu when its part is mounted.

Reduced Motion

All overlay animations are disabled under prefers-reduced-motion: reduce.

SparkNG 22.0.5 by bdSparkPhilosophyFAQContributeGitHubDiscussionsMIT License