Headless rich text editor built on ProseMirror with extensible architecture for building custom editors
npx @tessl/cli install tessl/npm-tiptap--core@3.4.0@tiptap/core is a headless rich text editor built on top of ProseMirror. It provides a flexible, extensible architecture for building custom rich text editors with nodes, marks, extensions, and commands. The library is framework-agnostic and can be integrated with React, Vue, Svelte, or used in vanilla JavaScript/TypeScript applications.
npm install @tiptap/core @tiptap/pmimport { Editor, Extension, Node, Mark } from '@tiptap/core';For CommonJS:
const { Editor, Extension, Node, Mark } = require('@tiptap/core');JSX Runtime (optional):
import { createElement, Fragment, h } from '@tiptap/core';import { Editor, Extension } from '@tiptap/core';
// Create a basic extension
const BasicExtension = Extension.create({
name: 'basicExtension',
addCommands() {
return {
insertText: (text: string) => ({ commands }) => {
return commands.insertContent(text);
}
};
}
});
// Create and mount editor
const editor = new Editor({
element: document.querySelector('#editor'),
content: '<p>Hello World!</p>',
extensions: [BasicExtension],
onUpdate: ({ editor }) => {
console.log('Content updated:', editor.getHTML());
}
});
// Use commands
editor.commands.insertText('Hello Tiptap!');
// Chain commands
editor
.chain()
.focus()
.insertContent('More content')
.run();
// Check if commands can be executed
if (editor.can().insertContent('test')) {
editor.commands.insertContent('test');
}@tiptap/core is built around several key components:
Central editor class that manages the editor state, extensions, and provides the main API interface.
class Editor {
constructor(options: Partial<EditorOptions>);
// Core properties
view: EditorView;
state: EditorState;
schema: Schema;
storage: Storage;
isEditable: boolean;
isFocused: boolean;
isEmpty: boolean;
isDestroyed: boolean;
isInitialized: boolean;
// Lifecycle methods
mount(element?: Element): Editor;
unmount(): Editor;
destroy(): void;
// Command execution
commands: SingleCommands;
chain(): ChainedCommands;
can(): CanCommands;
// Content methods
getHTML(): string;
getJSON(): JSONContent;
getText(options?: GetTextOptions): string;
setOptions(options: Partial<EditorOptions>): void;
setEditable(editable: boolean): void;
// State inspection
getAttributes(nameOrType: string | NodeType | MarkType): Record<string, any>;
isActive(name: string, attributes?: Record<string, any>): boolean;
// Content querying
$pos(pos: number): NodePos;
$node(selector: string, attributes?: Record<string, any>): NodePos | null;
$nodes(selector: string, attributes?: Record<string, any>): NodePos[];
// Plugin management
registerPlugin(plugin: Plugin, handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[]): EditorState;
unregisterPlugin(nameOrPluginKey: string | PluginKey | (string | PluginKey)[]): EditorState | undefined;
}
interface EditorOptions {
element?: Element;
content?: Content;
extensions?: Extensions;
injectCSS?: boolean;
injectNonce?: string;
autofocus?: FocusPosition;
editable?: boolean;
editorProps?: EditorProps;
parseOptions?: ParseOptions;
enableInputRules?: boolean;
enablePasteRules?: boolean;
enableCoreExtensions?: boolean | Record<string, boolean>;
enableContentCheck?: boolean;
emitContentError?: boolean;
coreExtensionOptions?: {
clipboardTextSerializer?: {
blockSeparator?: string;
};
delete?: {
asyncDeleteEvents?: boolean;
};
};
onBeforeCreate?(props: EditorEvents['beforeCreate']): void;
onCreate?(props: EditorEvents['create']): void;
onMount?(props: EditorEvents['mount']): void;
onUnmount?(props: EditorEvents['unmount']): void;
onUpdate?(props: EditorEvents['update']): void;
onSelectionUpdate?(props: EditorEvents['selectionUpdate']): void;
onTransaction?(props: EditorEvents['transaction']): void;
onFocus?(props: EditorEvents['focus']): void;
onBlur?(props: EditorEvents['blur']): void;
onContentError?(props: EditorEvents['contentError']): void;
onDestroy?(): void;
}Flexible extension system for adding functionality to the editor through Extensions, Nodes, and Marks.
class Extension<Options = any, Storage = any> {
static create<O = any, S = any>(config?: Partial<ExtensionConfig<O, S>>): Extension<O, S>;
configure(options?: Partial<Options>): Extension<Options, Storage>;
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
extendedConfig?: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>>
): Extension<ExtendedOptions, ExtendedStorage>;
}
class Node<Options = any, Storage = any> {
static create<O = any, S = any>(config?: Partial<NodeConfig<O, S>>): Node<O, S>;
configure(options?: Partial<Options>): Node<Options, Storage>;
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
extendedConfig?: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>>
): Node<ExtendedOptions, ExtendedStorage>;
}
class Mark<Options = any, Storage = any> {
static create<O = any, S = any>(config?: Partial<MarkConfig<O, S>>): Mark<O, S>;
configure(options?: Partial<Options>): Mark<Options, Storage>;
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(
extendedConfig?: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>>
): Mark<ExtendedOptions, ExtendedStorage>;
static handleExit(options: { editor: Editor; mark: ProseMirrorMark }): boolean;
}Powerful command system for executing, chaining, and validating editor actions.
interface SingleCommands {
[key: string]: (attributes?: Record<string, any>) => boolean;
}
interface ChainedCommands {
[key: string]: (attributes?: Record<string, any>) => ChainedCommands;
run(): boolean;
}
interface CanCommands {
[key: string]: (attributes?: Record<string, any>) => boolean;
}
class CommandManager {
constructor(props: { editor: Editor; state: EditorState });
readonly commands: SingleCommands;
chain(): ChainedCommands;
can(): CanCommands;
createChain(startTr?: Transaction, shouldDispatch?: boolean): ChainedCommands;
createCan(startTr?: Transaction): CanCommands;
}
interface CommandProps {
editor: Editor;
tr: Transaction;
commands: SingleCommands;
can: CanCommands;
chain: () => ChainedCommands;
state: EditorState;
view: EditorView;
dispatch: ((tr: Transaction) => void) | undefined;
}Comprehensive set of helper functions for document manipulation, content generation, and state inspection.
// Content generation
function generateHTML(doc: JSONContent, extensions: Extensions): string;
function generateJSON(content: string, extensions: Extensions): JSONContent;
function generateText(doc: JSONContent | Node, options?: GenerateTextOptions): string;
// Document creation
function createDocument(
content: Content,
schema: Schema,
parseOptions?: ParseOptions
): ProseMirrorNode;
function createNodeFromContent(
content: Content,
schema: Schema,
options?: CreateNodeFromContentOptions
): ProseMirrorNode | ProseMirrorNode[];
// Content queries
function findChildren(
node: ProseMirrorNode,
predicate: (child: ProseMirrorNode) => boolean,
descend?: boolean
): NodeWithPos[];
function findChildrenInRange(
node: ProseMirrorNode,
range: { from: number; to: number },
predicate: (child: ProseMirrorNode) => boolean,
descend?: boolean
): NodeWithPos[];
// State inspection
function isActive(
state: EditorState,
name?: string | NodeType | MarkType,
attributes?: Record<string, any>
): boolean;
function getAttributes(
state: EditorState,
nameOrType: string | NodeType | MarkType
): Record<string, any>;Input and paste rule systems for creating shortcuts and transforming content on input or paste.
class InputRule {
constructor(config: {
find: RegExp | ((value: string) => RegExpMatchArray | null);
handler: (props: {
state: EditorState;
range: { from: number; to: number };
match: RegExpMatchArray;
commands: SingleCommands;
chain: () => ChainedCommands;
can: () => CanCommands;
}) => void | null;
});
}
class PasteRule {
constructor(config: {
find: RegExp | ((value: string) => RegExpMatchArray | null);
handler: (props: {
state: EditorState;
range: { from: number; to: number };
match: RegExpMatchArray;
commands: SingleCommands;
chain: () => ChainedCommands;
can: () => CanCommands;
pastedText: string;
dropEvent?: DragEvent;
}) => void | null;
});
}Built-in JSX runtime support for creating custom extensions and components using JSX syntax.
/**
* Create JSX elements for use in extensions
* @param type - Element type or component
* @param props - Element properties
* @param children - Child elements
* @returns JSX element
*/
function createElement(
type: string | Component,
props: Record<string, any> | null,
...children: any[]
): JSXElement;
/**
* JSX Fragment component for wrapping multiple elements
*/
const Fragment: ComponentType;
/**
* Alias for createElement (h stands for hyperscript)
*/
const h: typeof createElement;Usage Examples:
import { createElement, Fragment, h } from '@tiptap/core';
// Using createElement directly
const element = createElement('div', { className: 'editor-toolbar' },
createElement('button', { onClick: () => editor.chain().focus().toggleBold().run() }, 'Bold'),
createElement('button', { onClick: () => editor.chain().focus().toggleItalic().run() }, 'Italic')
);
// Using JSX syntax (with proper JSX transform)
const toolbar = (
<div className="editor-toolbar">
<button onClick={() => editor.chain().focus().toggleBold().run()}>
Bold
</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()}>
Italic
</button>
</div>
);
// Using Fragment
const multipleElements = (
<Fragment>
<p>First paragraph</p>
<p>Second paragraph</p>
</Fragment>
);
// Using h (hyperscript style)
const hyperscriptElement = h('div', { id: 'editor' },
h('p', null, 'Content goes here')
);
// In custom extensions
const CustomExtension = Extension.create({
name: 'customExtension',
addNodeView() {
return ({ node, getPos, editor }) => {
const dom = createElement('div', {
className: 'custom-node',
'data-type': node.type.name
}, node.textContent);
return { dom };
};
}
});Collection of utility functions for type checking, object manipulation, and platform detection.
// Type guards
function isFunction(value: unknown): value is Function;
function isString(value: unknown): value is string;
function isNumber(value: unknown): value is number;
function isPlainObject(value: unknown): value is Record<string, any>;
// Object utilities
function mergeAttributes(...attributes: Record<string, any>[]): Record<string, any>;
function mergeDeep(target: Record<string, any>, source: Record<string, any>): Record<string, any>;
// Platform detection
function isAndroid(): boolean;
function isiOS(): boolean;
function isMacOS(): boolean;
// DOM utilities
function elementFromString(html: string): Element;
function createStyleTag(css: string, nonce?: string): HTMLStyleElement;// Core types
type Extensions = AnyExtension[];
type AnyExtension = Extension<any, any> | Node<any, any> | Mark<any, any>;
interface JSONContent {
type?: string;
attrs?: Record<string, any>;
content?: JSONContent[];
marks?: {
type: string;
attrs?: Record<string, any>;
}[];
text?: string;
}
interface NodeWithPos {
node: ProseMirrorNode;
pos: number;
}
// Event types
interface EditorEvents {
beforeCreate: { editor: Editor };
create: { editor: Editor };
mount: { editor: Editor };
unmount: { editor: Editor };
update: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[] };
selectionUpdate: { editor: Editor; transaction: Transaction };
transaction: { editor: Editor; transaction: Transaction };
focus: { editor: Editor; event: FocusEvent; transaction: Transaction };
blur: { editor: Editor; event: FocusEvent; transaction: Transaction };
contentError: {
editor: Editor;
error: Error;
disableCollaboration: () => void;
};
}
// Configuration types
interface ExtensionConfig<Options = any, Storage = any> {
name: string;
defaultOptions?: Options;
addStorage?(): Storage;
addCommands?(): Commands;
addKeymap?(): Record<string, any>;
addInputRules?(): InputRule[];
addPasteRules?(): PasteRule[];
addGlobalAttributes?(): GlobalAttributes[];
addNodeView?(): NodeViewRenderer;
onCreate?(this: { options: Options; storage: Storage }): void;
onUpdate?(this: { options: Options; storage: Storage }): void;
onDestroy?(this: { options: Options; storage: Storage }): void;
}