

Headless Editor is part of @ngstarter-ui/components. After installing NgStarter UI, import the editor from its secondary entry point.
provideNgsHeadlessEditor() creates an editor instance for the component that declares it and for its children. The editor is deliberately not a root singleton: every editor on a page has its own provider scope. Plugins are passed as features; basicTextEditorPlugin() registers paragraphs and the bold, italic, strike, and inline code marks.
import { Component, inject } from '@angular/core';
import {
basicTextEditorPlugin,
NgsHeadlessEditor,
NgsHeadlessEditorSurface,
provideNgsHeadlessEditor,
withHeadlessEditorPlugin
} from '@ngstarter-ui/components/headless-editor';
@Component({
selector: 'app-note-editor',
imports: [NgsHeadlessEditorSurface],
providers: [
provideNgsHeadlessEditor(
withHeadlessEditorPlugin(basicTextEditorPlugin())
)
],
templateUrl: './note-editor.html'
})
export class NoteEditor {
readonly editor = inject(NgsHeadlessEditor);
} Put ngsHeadlessEditorSurface on any element inside the same injector scope. The directive makes it editable, renders the document into it, and keeps native selection and the model in sync.
<div
class="editor"
ngsHeadlessEditorSurface
ariaLabel="Note"
placeholder="Write a note…">
</div> The value of an editor is its JSON document. Load one with setDocument() and read the current one from the document signal. Use origin to tell user edits apart from documents you loaded yourself.
import { effect } from '@angular/core';
import { createNgsHeadlessEditorDocument } from '@ngstarter-ui/components/headless-editor';
export class NoteEditor {
readonly editor = inject(NgsHeadlessEditor);
constructor() {
// Load initial content. History is reset by default.
this.editor.setDocument(createNgsHeadlessEditorDocument('Hello!'));
effect(() => {
const document = this.editor.document();
// 'external' means the document was loaded with setDocument().
if (this.editor.origin() !== 'external') {
this.save(document);
}
});
}
private save(document: NgsHeadlessEditorDocument): void {
// send JSON to your API
}
} The surface has no styles. Block and mark elements are created by the directive, outside your component template, so they do not receive Angular emulated-encapsulation attributes. Style them from a class on the surface with ::ng-deep, from global styles, or with ViewEncapsulation.None. The placeholder is exposed as an attribute on the first empty block.
.editor {
display: block;
min-height: 8rem;
padding: 0.75rem 1rem;
border: 1px solid var(--ngs-color-border);
border-radius: var(--ngs-radius-md);
outline: none;
&:focus {
border-color: var(--ngs-color-primary);
}
::ng-deep {
p {
margin: 0 0 0.5rem;
}
[data-ngs-headless-editor-placeholder] {
position: relative;
&::before {
content: attr(data-ngs-headless-editor-placeholder);
position: absolute;
inset: 0 auto auto 0;
color: var(--ngs-color-neutral-500);
pointer-events: none;
}
}
}
} Bind any button to a command with ngsHeadlessEditorCommand. The directive keeps the text selection when the button is pressed and reflects the command state with the active class, aria-pressed, and disabled. See Commands and Toolbar.
<button ngsButton="outlined" [ngsHeadlessEditorCommand]="bold">Bold</button>
<button ngsButton="outlined" [ngsHeadlessEditorCommand]="italic">Italic</button>
<!-- in the component: readonly bold = NGS_HEADLESS_EDITOR_TOGGLE_BOLD; -->::ng-deep or global styles.