Reaching the editor instance
The methods below run on the editor you created with new Blok(). They are available once editor.isReady resolves.
// 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` — set to `'image' | 'video' | 'audio' | 'file'` on media tools that store an uploaded asset URL at `data.url`, and `undefined` otherwise. Use it to discover the media-bearing tool set at runtime (instead of hardcoding each tool's data shape) and reconcile a saved document's `data.url`s against your CDN — e.g. to garbage-collect orphaned uploads.
When to use
Enumerate the registered block tools at runtime — handy for a custom block picker or debugging tool config.
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()
ToolsConfigReturns the tools-related configuration of this instance — { tools, inlineToolbar?, tunes?, theme? } — for creating nested Blok editors with the same tool set.
const nested = new Blok({ holder, ...editor.tools.getToolsConfig() });tools.update(name, config)
voidShallow-merge new configuration into an installed tool at runtime — no editor recreation. A `toolbox` key is treated as the tool-level setting (same as `toolbox` in the `tools` map): pass `toolbox: false` to hide the tool from every insertion surface (existing blocks keep rendering) or a toolbox object to (re)show it — permission gating without rebuilding the editor. Under the React adapter this is automatic: change the `toolbox` value in the `tools` prop and `useBlok`/`BlokEditor` applies it in place.
// 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)
voidRuntime setter for the global `inlineToolbar` config. Re-assigns inline tools for every block tool and recomposes the memoized sanitize configs — so paste-time sanitization follows the new set immediately, and the inline toolbar reflects it on the next selection. Tool-scoped `inlineToolbar` settings (arrays and opt-outs) stay authoritative. Pass `true` for all inline tools, `false` for none, or an ordered list of inline tool names. 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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
config | boolean | 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. |
// 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)
booleanReturns true when a tool with the given name is installed and available on this editor instance — block, inline or tune. Public introspection over the installed tool set, e.g. 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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Required | — | Registered tool name to look up. |
if (editor.tools.isInstalled('image')) {
editor.tools.update('image', { uploader: { uploadByFile } });
}defineTool(toolClass, settings?)
ExternalToolSettingsA registration helper exported from `@bloklabs/core/tools`, 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>` — the type that does the recovery — is exported alongside it, and falls back to `Record<string, unknown>` for tool classes whose constructor declares no concrete config.
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)
voidThe child-holder reconciler for container blocks, exported from `@bloklabs/core/tools`. Call it from your tool's `rendered()` hook — it is what the built-in toggle, callout and column tools use, and what the React/Vue/Angular block adapters run on every commit. It is idempotent and cheap, so run it on every render. Per child it: leaves a holder already inside `container` alone; RECLAIMS a holder stranded in a nested container that ENCLOSES `container`, inserting it at its model position rather than appending it last; leaves holders sitting in any OTHER nested container alone, so two containers can never steal each other's blocks; and 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 it a child of a nested container renders one level out — permanently, when your container's child slot had not been created yet at insert time (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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
container | HTMLElement | Required | — | The element child holders belong in — the element carrying data-blok-nested-blocks. |
children | { holder: HTMLElement }[] | Required | — | The block's children in model order, normally api.blocks.getChildren(blockId). |
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
BlockOriginThe create-vs-restore signal on the tool contract, handed to every block tool's constructor alongside `data`, `block` and `readOnly`. A container tool that seeds default children — a two-column row, 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, and 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 — 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 — 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 — it is never inserted, yet it still runs `render()` and `rendered()`, so it must not touch the block tree at all). Blok always supplies it; 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, and it fires after the adapter's first commit — the tick at which the block's DOM and its adopted child holders actually exist.
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();
}
}BlockToolConstructable.keepsChildrenOnEnter
booleanA 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, and as a sole child a fresh block is inserted after the whole container — Notion's callout behaviour. A layout container whose children ARE its content (a column, a card, a `steps` block) wants the opposite, and without the declaration the escape strands the new line beside the container. Set it to `true` and the new line stays inside, 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), so 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.
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 — and 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 robust against a foreign child, 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.
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 — no new tool instance, no new holder, so ephemeral state (an open menu, a scroll position, a framework component's local state), the adopted child holders and the caret survive. Without it core destroys the block and builds a replacement, which is why a host that called `blocks.update()` per keystroke used to watch a component-backed block go blank. Return `false` when you cannot apply the data in place — the list tool does that for a style change, which needs a different DOM shape — and core falls back to the full recompose; returning `true` or nothing means it was applied. Throwing has the same effect as `false` (logged, then recomposed). The React/Vue/Angular block factories implement it for you, so adapter blocks are on the in-place path automatically.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
newData | BlockToolData | Required | — | The block's full data after the update — the existing data merged with the caller's patch, not the patch alone. |
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;
}
}