Skip to content
FrameworkJavaScript

Marks API: range-aware inline formatting

Range-aware inline-mark operations for building inline formatting tools. Where selection.findParentTag only inspects the selection's anchor node, api.marks operates on the WHOLE range: has answers "is every text node in the selection covered", apply and remove split partially-covered wrappers at the range boundaries, update fully-covering wrappers in place, and restore the selection afterwards — and apply and remove extend the range over trailing whitespace browsers exclude from double-click selections. A mark is described declaratively by a MarkSpec (tag, aliasTags, className, attributes, style); aliasTags lets legacy tag variants (e.g. <b> next to <strong>, <em> next to <i>) match as the SAME mark while new wrappers always use the canonical tag. String values are static and participate in the mark's identity; function-form values are resolved from the state passed to apply/toggle and are deliberately EXCLUDED from identity — that is what makes a colour picker ONE mark updating in place rather than N mutually-cancelling marks. Two specs sharing tag, classNames and static attributes belong to the same family and compose on a single element — e.g. a text-colour spec and a background-colour spec both on one <mark>. Every method defaults to the live selection's first range when no range is passed. The core export markSanitizerConfig(spec) derives the sanitizer rule a mark produces — allowlist the spec's tag, strip style properties and classes the spec does not declare, keep declared attributes, with function-form values handled by property name so dynamic values are never dropped on save. The React adapter's createReactInlineTool applies the same derivation automatically when a tool declares a mark spec.

Last updated Jul 22, 2026Edit this page on GitHub

Reaching the editor instance

The methods below run on the editor you created with new Blok(). They are available once editor.isReady resolves.

TypeScript
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;

// Call any API method on it.
editor.caret.setToLastBlock('end');

Methods

marks.has(spec, range?)

boolean

Whether every text node in the range is inside a wrapper matching the spec — whitespace-only text nodes are ignored, and at a collapsed caret the caret's ancestors are checked. Unlike selection.findParentTag (anchor node only), a selection that only partially carries the mark reports false.

When to use

The range-aware replacement for the findParentTag activity check — a selection that only partly carries the mark reports false, so your toolbar icon cannot lie.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequiredDeclarative mark description: tag plus optional className, attributes, and style.
rangeRangecurrent selectionRange to check; defaults to the live selection's first range.
TypeScript
const highlight = { tag: 'span', className: 'my-highlight' };

// True only when the WHOLE selection is covered — a half-highlighted
// selection reports false, so a toolbar icon cannot lie
const active = editor.marks.has(highlight);

marks.find(spec, from?)

HTMLElement | null

Nearest ancestor element matching the spec, starting from the given node (or the current selection's start container). Matching respects the full spec — tag, classNames, and static attribute/style values — not just the tag name.

When to use

Ancestor lookup by spec — like findParentTag, but matching classNames and static attributes/styles, not just the tag name.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequiredDeclarative mark description: tag plus optional className, attributes, and style.
fromNodeselection start containerNode to start the upward search from; defaults to the current selection's start container.
TypeScript
const wrapper = editor.marks.find({ tag: 'mark' });

if (wrapper) {
  editor.selection.expandToTag(wrapper);
}

marks.read(spec, range?)

MarkSnapshot | null

Read the current values of the spec's declared properties from the wrapper at the range start. Returns null when the range is not inside a matching wrapper. The snapshot carries the matched element plus its declared style properties and attributes — unset and transparent-valued style properties are omitted.

When to use

Drive your tool's UI from the document: preselect the current colour in a picker before the user changes it.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequiredDeclarative mark description: tag plus optional className, attributes, and style.
rangeRangecurrent selectionRange to read from; defaults to the live selection's first range.
TypeScript
const colorMark = {
  tag: 'mark',
  style: { color: (state) => state.color },
};

// Preselect the current colour in a picker UI
const snapshot = editor.marks.read(colorMark);
const current = snapshot?.style['color']; // e.g. 'rgb(37, 99, 235)'

marks.apply(spec, state?, range?)

HTMLElement[]

Wrap the range in the mark, or update matching wrappers in place. Splits partially-covered same-family wrappers at the range boundaries, extends the range over trailing whitespace browsers exclude from double-click selections, and leaves the new contents selected. Returns the created or updated wrapper elements.

When to use

Idempotent per family: re-applying with new state updates the SAME wrapper in place — that is what makes a colour picker one mark instead of nested cancelling wrappers.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequiredDeclarative mark description: tag plus optional className, attributes, and style.
stateStateValue passed to the spec's function-form attribute/style properties.
rangeRangecurrent selectionRange to format; defaults to the live selection's first range.
TypeScript
editor.marks.apply(colorMark, { color: '#d97706' });

// Same identity → the SAME wrapper is updated in place, not nested
editor.marks.apply(colorMark, { color: '#2563eb' });

marks.remove(spec, range?)

HTMLElement[]

Remove the spec's declared properties and classes from wrappers in the range, unwrapping wrappers left bare. Partially-covered wrappers are split so text outside the range keeps its formatting, and the selection is restored. Returns the wrappers that survived because they still carry other properties.

When to use

Only the spec's own properties are removed — a wrapper shared with another mark of the family survives with the rest intact.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequiredDeclarative mark description: tag plus optional className, attributes, and style.
rangeRangecurrent selectionRange to deformat; defaults to the live selection's first range.
TypeScript
editor.marks.remove(colorMark);
// A <mark> that also carried a background-colour spec survives
// with only the colour stripped

marks.toggle(spec, state?, range?)

boolean

remove when the range already carries the mark, apply otherwise. Returns the resulting state: true when the mark is now applied.

When to use

The one-liner behind simple toggle tools; pair with has() for the active state.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequiredDeclarative mark description: tag plus optional className, attributes, and style.
stateStateValue passed to the spec's function-form attribute/style properties.
rangeRangecurrent selectionRange to toggle; defaults to the live selection's first range.
TypeScript
const highlight = { tag: 'span', className: 'my-highlight' };

const nowApplied = editor.marks.toggle(highlight);
TypeScript
// One spec = one mark. Static values (tag, className, attribute/style
// strings) form the mark's identity; function-form values do not.
const textColor = {
  tag: 'mark',
  style: { color: (state) => state.color },
};
const bgColor = {
  tag: 'mark',
  style: { 'background-color': (state) => state.color },
};

// Range-aware: splits partially-covered wrappers at the boundaries,
// updates fully-covering wrappers in place, restores the selection
editor.marks.apply(textColor, { color: '#2563eb' });

// Same family (same tag, no conflicting statics) → composes on ONE element
editor.marks.apply(bgColor, { color: '#fef3c7' });
// → <mark style="color: #2563eb; background-color: #fef3c7">…</mark>

// Derive the sanitizer rule the mark produces for a vanilla inline tool
import { markSanitizerConfig } from '@bloklabs/core';

class TextColorTool {
  static get sanitize() {
    return markSanitizerConfig(textColor);
  }
}