Skip to content
FrameworkJavaScript

Marks API: range-aware inline formatting

Range-aware inline-mark operations for building inline formatting tools. selection.findParentTag inspects only the selection's two boundary nodes (anchor and focus) and their ancestors. api.marks works on the WHOLE range instead.

  • 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.
    • apply and remove also extend the range over trailing whitespace that browsers exclude from double-click selections.

A MarkSpec describes a mark declaratively: tag, aliasTags, className, attributes, style. aliasTags lets legacy tag variants match as the SAME mark, for example <b> next to <strong>, or <em> next to <i>. New wrappers always use the canonical tag.

String values are static and take part 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 that updates in place, rather than N mutually-cancelling marks.

Two specs sharing tag, classNames and static attributes belong to the same family. They compose on a single element. A text-colour spec and a background-colour spec can both sit 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. It allowlists the spec's tag, strips style properties and classes the spec does not declare, and keeps declared attributes. Function-form values are 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 Sep 8, 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. At a collapsed caret, the caret's ancestors are checked.

selection.findParentTag checks only the selection's boundary nodes and their ancestors. This method is stricter: 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
specMarkSpecRequired—

Declarative mark description: tag plus optional className, attributes, and style.

rangeRange—current selection

Range 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, not just the tag name: tag, classNames, and static attribute/style values.

When to use

Ancestor lookup by spec. Like findParentTag, but it also matches classNames and static attributes/styles, not just the tag name.

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequired—

Declarative mark description: tag plus optional className, attributes, and style.

fromNode—selection start container

Node 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
specMarkSpecRequired—

Declarative mark description: tag plus optional className, attributes, and style.

rangeRange—current selection

Range 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 that browsers exclude from double-click selections.
  • Leaves the new contents selected.
  • Returns the created or updated wrapper elements.

A collapsed range (a bare caret with nothing selected) is a no-op: apply returns an empty array without touching the DOM or the selection.

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
specMarkSpecRequired—

Declarative mark description: tag plus optional className, attributes, and style.

stateState——

Value passed to the spec's function-form attribute/style properties.

rangeRange—current selection

Range 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[]

Removes the spec's declared properties and classes from wrappers in the range, and unwraps wrappers left bare.

  • For a non-collapsed range, partially-covered wrappers are split so text outside the range keeps its formatting.
  • A collapsed caret instead targets its enclosing wrapper whole: no split happens, and the spec's properties are stripped from the entire wrapper.

Either way, 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
specMarkSpecRequired—

Declarative mark description: tag plus optional className, attributes, and style.

rangeRange—current selection

Range 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

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

At a collapsed caret with no existing mark it returns true without applying anything, because apply is a no-op on a collapsed range. Call toggle on a non-collapsed range, or treat the return value as the intended state rather than a confirmation.

When to use

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

Parameters

ParameterTypeRequiredDefaultDescription
specMarkSpecRequired—

Declarative mark description: tag plus optional className, attributes, and style.

stateState——

Value passed to the spec's function-form attribute/style properties.

rangeRange—current selection

Range 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);
  }
}