Skip to content
FrameworkJavaScript

BlockAPI: work with a single block

Interface for working with individual blocks. Returned by blocks.getById(), blocks.getBlockByIndex(), and blocks.insert().

Last updated Oct 7, 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

block.save()

Promise<void|SavedData>

Save the block content and return its data.

When to use

Returns the data for this block only. Use it to inspect or persist one block without the full walk that editor.save() does.

TypeScript
const block = editor.blocks.getById('block-123');
const saved = await block.save();
// saved resolves to a SavedData object (or undefined if extraction fails):
// { id: 'block-123', tool: 'paragraph', data: { text: 'Block content' }, time: 1717000000000 }
console.log(saved?.data); // { text: 'Block content' }

block.validate(data)

Promise<boolean>

Validate block data against the tool's validation rules.

When to use

Runs the tool's validate() against the given data. Use it to reject empty or malformed blocks before saving.

TypeScript
const block = editor.blocks.getById('block-123');
const isValid = await block.validate({ text: 'Hello' });
if (!isValid) {
  console.log('Block data is invalid');
}

block.call(methodName, param?)

void

Call a custom method on the block's tool.

When to use

An escape hatch for calling a custom method your tool exposes. Use it for behaviour outside the standard BlockTool API.

TypeScript
const block = editor.blocks.getById('block-123');
// Call a custom method defined in the tool
block.call('showNotification', { message: 'Hello' });

block.dispatchChange()

void

Manually trigger the onChange callback for this block.

Pass { derived: true } when the change is data the tool worked out itself (a measured size, a fetched preview), not a user edit. It is saved but adds no undo step and keeps redo. Add from with the data keys it came from ({ derived: true, from: ['url'] }): it then joins the undo step that wrote those values.

When to use

Call it when you change a block outside Blok's knowledge (e.g. an async update), so change events and CRDT sync fire.

TypeScript
const block = editor.blocks.getById('block-123');
// Trigger change after invisible modification
block.dispatchChange();

block.getActiveToolboxEntry()

Promise<ToolboxConfigEntry | undefined>

Get the active toolbox entry for this block (e.g., Heading 1 vs Heading 2).

When to use

Resolves which toolbox variant is active (e.g. Heading 1 vs 2). Useful for reflecting state in custom UI.

TypeScript
const block = editor.blocks.getById('block-123');
const entry = await block.getActiveToolboxEntry();
if (entry) {
  console.log('Active entry:', entry.title);
}

block.getChildren()

BlockAPI[]

This block's direct children as BlockAPI objects, in order. Every BlockAPI the editor hands out is live. So you can walk the children it returns recursively (child.getChildren()) and change them (child.setParent(...), child.insertChild(...)).

TypeScript
const block = editor.blocks.getById('toggle-123');
block?.getChildren().forEach((child) => console.log(child.id));

block.setParent(parentId)

void

Reparent this block under parentId, or back to the root level with null. It goes through core's single setBlockParent entry point, so the parent's contentIds is updated together with this block's parentId.

TypeScript
const block = editor.blocks.getById('block-123');
block?.setParent('parent-block-id');

// Back to the root level
block?.setParent(null);

block.insertChild(childData?, position?, toolName?, options?)

BlockAPI

Insert a child block under THIS block atomically. Creation and parent assignment land in a single undo entry (it delegates to blocks.insertInsideParent).

  • position is a BlockChildPosition: 'start' | 'end' | { before: childId } | { after: childId }.
    • It defaults to 'end', which appends past the whole subtree.
  • toolName picks the child's block tool and defaults to config.defaultBlock, so a TYPED child is one operation instead of insert-then-reparent.
    • A tool that is restricted inside table cells is demoted to the default block when the new child would land inside one.
  • childData defaults to { text: '' } for the default block, and to {} when toolName names a different tool.
  • options uses the same { focus, caret, id, tunes, replace } vocabulary as the framework adapters' rich insert spec.

So a container tool never has to place the caret by hand, or follow up with an update to apply tunes.

Parameters

ParameterTypeRequiredDefaultDescription
childDataBlockToolData—{ text: '' } / {}

Data for the new child block. It defaults to an empty paragraph blob for the default block tool. When toolName is given it defaults to {}, which lets the tool apply its own defaults.

positionBlockChildPosition—'end'

Where among the existing children to insert: 'start', 'end', { before: childId } or { after: childId }.

toolNamestring—config.defaultBlock

Block tool to create for the child. It is demoted to the default block when it is restricted inside table cells and the new child would land inside one.

optionsInsertChildOptions—{}

focus makes the new child the current block. caret places the caret inside it ({ position?, offset? }), and applies only when a child is actually created.

id sets an explicit id. An id that already exists works as insert-if-absent: that child is returned and nothing is created.

tunes is block tune data applied at creation. replace overwrites the child named by an object position instead of inserting beside it. With 'start' or 'end' there is nothing to overwrite, so the call throws.

TypeScript
const block = editor.blocks.getById('toggle-123');
const child = block?.insertChild({ text: 'Hidden content' });

// Place it among the existing children instead of appending
block?.insertChild({ text: 'First' }, 'start');
block?.insertChild({ text: 'After that one' }, { after: 'child-id' });

// A typed child, in a single undo entry
block?.insertChild({ text: 'Section', level: 3 }, 'end', 'header');

// Drop the caret into the new child at a specific offset
block?.insertChild({ text: 'Draft' }, 'end', undefined, { caret: { offset: 5 } });

// Idempotent: a re-running effect cannot duplicate this child
block?.insertChild({ text: 'Intro' }, 'start', undefined, { id: 'intro-row' });

// Child-level "turn into" — overwrite an existing child, keeping it parented
block?.insertChild({ text: 'Now a heading', level: 3 }, { before: 'child-id' }, 'header', { replace: true });

block.moveChild(childId, delta)

void

Move a direct child by delta positions among its siblings, clamped to the valid range. A child carrying its own subtree lands past the target sibling's descendants, not inside them. It does nothing when delta is 0, when childId is not a direct child, or when the clamped move would not change the position.

TypeScript
const block = editor.blocks.getById('toggle-123');
block?.moveChild('child-id', -1); // one position toward the start
block?.moveChild('child-id', 1);  // one position toward the end

Properties

PropertyTypeDescription
idstring

Unique block identifier

namestring

Tool name (e.g., "paragraph", "header")

configToolConfig

Tool config passed on initialization

holderHTMLElement

Wrapper of Tool's HTML element

isEmptyboolean

True if block content is empty

selectedboolean

True if the block is part of a BLOCK-level selection (rubber-band drag, Shift+Click, Shift+Arrow, Cmd/Ctrl+A). A drag across the text of several blocks makes a character-level selection instead. That marks no block as selected, so read it from the document's own Selection.

focusableboolean

True if block has inputs to be focused

stretchedboolean

Getter/setter for block stretch state

parentIdstring | null

Id of the parent block, or null if this block has no parent

contentIdsreadonly string[]

Ids of this block's direct children, in order. It is a read-only copy, so changing it does nothing. This is the block-level counterpart of parentId: it lets a container tool read its children without reaching for the editor API

preservedDataBlockToolData

Last successfully extracted block tool data. It is synchronous, so it helps when async save() is not feasible, e.g. clipboard operations.

It is an internal snapshot, so rich text fields here stay HTML strings. save() returns them as segments.

preservedTunes{ [name: string]: BlockTuneData }

Last successfully extracted block tune data. It is synchronous, so it helps when async save() is not feasible, e.g. clipboard operations