Skip to content
FrameworkJavaScript

Tools API: register and update tools

Access and manage editor tools.

Last updated Sep 24, 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

tools.getBlockTools()

BlockToolAdapter[]

Get all available block tool adapters. Each adapter exposes name plus tool metadata, including assetKind.

  • On media tools that store an uploaded asset URL at data.url, assetKind is 'image' | 'video' | 'audio' | 'file'.
  • On every other tool it is undefined.

Use it to find the media-bearing tools at runtime, instead of hardcoding each tool's data shape. You can then check a saved document's data.urls against your CDN, for example to clean up orphaned uploads.

When to use

Lists the registered block tools at runtime. Handy for a custom block picker or for debugging tool config.

TypeScript
const blockTools = editor.tools.getBlockTools();
blockTools.forEach(tool => {
  console.log('Available tool:', tool.name);
});

// Discover which block types hold uploaded media, then collect their URLs
const mediaTypes = new Set(
  editor.tools.getBlockTools().filter(t => t.assetKind).map(t => t.name)
);
const referenced = (await editor.save()).blocks
  .filter(b => mediaTypes.has(b.type))
  .map(b => b.data.url);

tools.getToolsConfig()

ToolsConfig

Returns the tools-related configuration of this instance: { tools, inlineToolbar?, tunes?, theme? }. Use it to create nested Blok editors with the same tool set.

TypeScript
const nested = new Blok({ holder, ...editor.tools.getToolsConfig() });

tools.update(name, config)

void

Shallow-merges new configuration into an installed tool at runtime, with no editor recreation. A toolbox key is treated as the tool-level setting, the same as toolbox in the tools map.

  • Pass toolbox: false to hide the tool from every insertion surface; existing blocks keep rendering.
  • Pass a toolbox object to show it again.

This gates permissions without rebuilding the editor. Under the React adapter it is automatic: change the toolbox value in the tools prop and useBlok/BlokEditor applies it in place.

TypeScript
// Swap a config value (e.g. an uploader) at runtime
editor.tools.update('image', { uploader: { uploadByFile } });

// Permission flip: hide the tool from the + / slash / convert menus.
// Existing goodsList blocks still render; insertion is gated.
editor.tools.update('goodsList', { toolbox: false });

// Re-enable it later
editor.tools.update('goodsList', { toolbox: { title: 'Goods List' } });

tools.setInlineToolbar(config)

void

Runtime setter for the global inlineToolbar config. It re-assigns inline tools for every block tool and recomposes the memoized sanitize configs.

  • Paste-time sanitization follows the new set immediately.
  • The inline toolbar reflects it on the next selection.
  • Tool-scoped inlineToolbar settings, both arrays and opt-outs, stay authoritative.

Pass true for all inline tools, false for none, or a list of inline tool names. The list selects which tools appear, while their left-to-right order stays fixed.

If you render saved content through @bloklabs/core/view, note that a viewSchema is composed from the inlineToolbar value it was defined with. After a runtime setInlineToolbar involving custom inline tools, recompose it with defineBlokSchema before calling blocksToHtml.

When to use

Takes effect on the next selection. Paste-time sanitization is recomposed immediately, so pasted content follows the new inline-tool set right away. If you render saved content through @bloklabs/core/view, note that a viewSchema is composed from the inlineToolbar value it was defined with. After a runtime setInlineToolbar involving custom inline tools, recompose it with defineBlokSchema before calling blocksToHtml.

Parameters

ParameterTypeRequiredDefaultDescription
configboolean | string[]Required—

true enables every registered inline tool. false disables the inline toolbar. An array restricts it to the listed inline tools, in that order.

TypeScript
// Restrict inline formatting to bold and italic at runtime
editor.tools.setInlineToolbar(['bold', 'italic']);

// Disable the inline toolbar entirely
editor.tools.setInlineToolbar(false);

// Back to every registered inline tool
editor.tools.setInlineToolbar(true);

tools.isInstalled(name)

boolean

Returns true when a tool with the given name is installed and available on this editor instance. That covers block, inline and tune tools. Use it to inspect the installed tool set, for example as a guard before tools.update(name, config), which throws for unknown names.

When to use

Guard runtime tool calls. tools.update() throws for names that are not installed.

Parameters

ParameterTypeRequiredDefaultDescription
namestringRequired—

Registered tool name to look up.

TypeScript
if (editor.tools.isInstalled('image')) {
  editor.tools.update('image', { uploader: { uploadByFile } });
}

defineTool(toolClass, settings?)

ExternalToolSettings

A registration helper exported from @bloklabs/core/tools. It is not a member of the tools namespace.

The plain tools map types every entry with a bare ToolSettings whose Config falls back to Record<string, unknown>. So a misspelled config key (defaultLevle for defaultLevel) compiles silently. defineTool recovers the tool's real config type from its constructor and applies it to settings.config, turning those typos into compile errors.

The type-checking happens on the settings argument. The RETURN type stays the erased ExternalToolSettings, so the result drops straight into the tools map.

ExtractToolConfig<TClass> is the type that does the recovery. It is exported alongside defineTool, and falls back to Record<string, unknown> for tool classes whose constructor declares no concrete config.

TypeScript
import { Blok } from '@bloklabs/core';
import { Header, defineTool } from '@bloklabs/core/tools';

new Blok({
  tools: {
    header: defineTool(Header, { config: { levels: [1, 2, 3] } }),
    // `defaultLevle: 2` here would now be a compile error
  },
});

mountChildBlocks(container, children)

void

The child-holder reconciler for container blocks, exported from @bloklabs/core/tools. Call it from your tool's rendered() hook. The built-in toggle, callout and column tools use it, and the React/Vue/Angular block adapters run it on every commit.

It is idempotent and cheap, so run it on every render. Per child it behaves like this.

  • It leaves a holder already inside container alone.
  • It RECLAIMS a holder stranded in a nested container that ENCLOSES container, inserting it at its model position rather than appending it last.
  • It leaves holders sitting in any OTHER nested container alone, so two containers can never steal each other's blocks.
  • It mounts everything else at its model position.

The reclaim is what makes a container survive the insert ordering. Core anchors a newly inserted first child as the container block's DOM sibling, so without the reclaim a child of a nested container renders one level out. That is permanent when your container's child slot had not been created yet at insert time, which is what happens when a framework portal commits a render after core inserts.

Mark container with data-blok-nested-blocks so the rest of the editor recognises it as a container.

Parameters

ParameterTypeRequiredDefaultDescription
containerHTMLElementRequired—

The element child holders belong in. It is the element carrying data-blok-nested-blocks.

children{ holder: HTMLElement }[]Required—

The block's children in model order, normally api.blocks.getChildren(blockId).

TypeScript
import { mountChildBlocks } from '@bloklabs/core/tools';

class CardTool {
  constructor({ api, block }) {
    this.api = api;
    this.blockId = block.id;
  }

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }

  // Runs after the holder is in the document, and on every re-render
  rendered() {
    mountChildBlocks(this.slot, this.api.blocks.getChildren(this.blockId));
  }
}

BlockToolConstructorOptions.origin

BlockOrigin

The create-vs-restore signal on the tool contract. Blok hands it to every block tool's constructor alongside data, block and readOnly.

A container tool that seeds default children, such as a two-column row or a card that starts with a heading, may only do that once, at creation. Every other time the tool is constructed, the document already says what its children are.

During a restore those children commonly land a tick AFTER rendered() runs, so an empty api.blocks.getChildren() there is only transient. Seeding on that read fabricates phantom children beside the real ones.

CREATION values, where you may seed:

  • 'user' (a direct editing gesture: Enter, the plus button, the slash menu, block settings, a markdown shortcut).
  • 'api' (a programmatic blocks.insert / insertMany / insertInsideParent).
  • 'convert' (a turn-into).

RESTORE values, where you never seed:

  • 'load' (a document render).
  • 'replay' (an undo/redo replay or a remote collaborative update).
  • 'paste' (pasted content that brings its own children).
  • 'probe' (the OFF-TREE instance blocks.composeBlockData() builds to read a tool's default data).

The probe instance is never inserted, yet it still runs render() and rendered(), so it must not touch the block tree at all.

Blok always supplies the value. Treat an absent value as 'api', and write the check as an allow-list of creation values, so a future origin fails closed. Pair it with blocks.insert(..., origin) if you drive insertion from your own UI.

On the React/Vue/Angular adapters you rarely read it by hand. The block spec's onCreated hook already encodes this allow-list. It fires after the adapter's first commit, the tick at which the block's DOM and its adopted child holders actually exist.

TypeScript
class TwoColumnCard {
  constructor({ api, block, origin }) {
    this.api = api;
    this.blockId = block.id;
    // Allow-list, so a future origin never silently opts into seeding.
    this.isCreation = ['user', 'api', 'convert', undefined].includes(origin);
  }

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }

  rendered() {
    const children = this.api.blocks.getChildren(this.blockId);

    if (children.length > 0) {
      mountChildBlocks(this.slot, children);

      return;
    }

    // Empty on a load / undo-redo replay / paste / probe means "my children
    // have not arrived yet", NOT "I am brand new". Only a creation may seed.
    if (!this.isCreation) {
      return;
    }

    this.seedColumns();
  }
}

BlockToolConstructorOptions.replaySource

'history' | 'remote' | undefined

Who rebuilt the block, set only when origin is 'replay'.

  • 'history': this client's own undo or redo.
  • 'remote': a peer's change arrived over collaboration.

Every client rebuilds the block for a peer's change. So a side effect that only the acting client should run, such as a network fetch or a derived write, belongs to 'history'. It is undefined for every other origin.

TypeScript
class Bookmark {
  constructor({ data, origin, replaySource }) {
    // Only the client that redid the paste fetches the preview again.
    this.refetch = origin === 'replay' && replaySource !== 'remote' && data.title === undefined;
  }
}

BlockToolConstructable.keepsChildrenOnEnter

boolean

A static on your tool CLASS that decides where Enter goes on the container's empty LAST child. By default Blok reads that empty trailing line as the author's way out.

  • With siblings present the line is outdented to the container's own parent.
  • When it is the sole child, Blok inserts a fresh block after the whole container.

That is Notion's callout behaviour. A layout container whose children ARE its content (a column, a card, a steps block) wants the opposite. Such a tool needs the declaration: without it, the escape strands the new line beside the container.

Set it to true and the new line stays inside. That is the same rule the built-in column, column_list and toggle follow. It cannot be inferred from the DOM: a callout renders the very same data-blok-nested-blocks slot as a column and deliberately keeps the escape, so this is per-tool policy.

Core reads it for the symmetric "remove one indent level" gesture too (Enter/Backspace on a block nested under a PLAIN parent). A declaring tool is treated as a container there as well, and its children never stepwise-outdent out of it.

On the React/Vue/Angular adapters, declare it in the block spec's statics bag like any other class static.

TypeScript
class StepsTool {
  static keepsChildrenOnEnter = true;

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }

  rendered() {
    mountChildBlocks(this.slot, this.api.blocks.getChildren(this.blockId));
  }
}

// Framework adapters forward it through `statics`:
export const StepsTool = createReactBlock({
  type: 'steps',
  statics: { ownsChildren: true, keepsChildrenOnEnter: true },
  component: StepsCard,
});

BlockToolConstructable.childTools

{ allow?: string[]; deny?: string[] }

A static on your tool CLASS declaring which block tools may be DIRECT children of its block, and core enforces it everywhere for you.

  • On INSERT a disallowed tool is demoted, never refused, because Enter must always produce a block.
    • The target is the first entry of allow, so allow: ['segment-item'] makes "Enter at the end of a segment" produce another segment instead of a stray paragraph.
  • On MOVE a drag or keyboard reorder that would carry a disallowed block across the container boundary is refused.
  • In the TOOLBOX the disallowed tools are hidden while the caret sits in a child.

deny wins over allow for a tool named in both, and empty lists read as "no restriction".

This is the selective, insert-aware counterpart to ownsChildren, which is all-or-nothing and clamps moves only. It is also the generic form of the Table tool's restrictedTools, whose enforcement is hard-wired to table cells.

Without it a container tool has to defend itself downstream: filtering child.name in render, keeping its CSS working when a foreign child appears, and migrating strays out of stored documents. On the React/Vue/Angular adapters, declare it in the block spec's statics bag like any other class static.

TypeScript
class Segments {
  static get childTools() {
    return { allow: ['segment-item'] };
  }

  render() {
    this.slot = document.createElement('div');
    this.slot.setAttribute('data-blok-nested-blocks', '');

    return this.slot;
  }
}

// Only forbid a few tools, accept everything else
class Callout {
  static childTools = { deny: ['table', 'column_list'] };
}

// Framework adapters forward it through `statics`:
export const Segments = createReactBlock({
  type: 'segments',
  statics: { childTools: { allow: ['segment-item'] } },
  component: SegmentsCard,
});

setData(newData)

boolean | void | Promise<boolean | void>

An optional method on your tool that applies new data to the LIVE instance. Declare it, and blocks.update(), undo/redo and remote collaborative edits all reuse the block you already rendered instead of recomposing it.

There is no new tool instance and no new holder, so ephemeral state survives: an open menu, a scroll position, a framework component's local state. The adopted child holders and the caret survive too.

Without it, core destroys the block and builds a replacement. That is why a host calling blocks.update() on every keystroke used to watch a component-backed block go blank.

  • Return false when you cannot apply the data in place, and core falls back to the full recompose.
    • The list tool does that for a style change, which needs a different DOM shape.
  • Returning true or nothing means the data was applied.
  • Throwing has the same effect as false: it is logged, then the block is recomposed.

The React/Vue/Angular block factories implement it for you, so adapter blocks take the in-place path automatically.

Parameters

ParameterTypeRequiredDefaultDescription
newDataBlockToolDataRequired—

The block's full data after the update. That is the existing data merged with the caller's patch, not the patch alone.

TypeScript
class CalloutTool {
  setData(newData) {
    if (newData.variant !== this.data.variant) {
      // A different variant renders a different DOM shape — let core rebuild.
      return false;
    }

    this.data = newData;
    this.box.textContent = newData.text ?? '';

    return true;
  }
}