How to create a custom block tool in Blok
Build a block tool from scratch — a callout box that renders, edits, and saves like any built-in block.
Every built-in block is just a tool that follows the BlockTool interface, and you can add your own. Here you'll build a Callout block: an editable box that saves its text and reloads it like any other block.
Write the tool class
A block tool needs three things: a static toolbox getter so it shows up in the / menu, a render() that returns the block's element, and a save() that returns the data to persist.
// callout-tool.ts
export class CalloutTool {
private data: { text: string };
// Shows the tool in the "/" menu.
static get toolbox() {
return { title: 'Callout', icon: '💡' };
}
constructor({ data }: { data: { text?: string } }) {
this.data = { text: data.text ?? '' };
}
// Return the element Blok mounts for this block.
render() {
const box = document.createElement('div');
box.classList.add('callout');
box.contentEditable = 'true';
box.textContent = this.data.text;
return box;
}
// Return the data Blok stores when the editor is saved.
save(block: HTMLElement) {
return { text: block.textContent ?? '' };
}
}Register it in the editor
Pass your class under the tools config. The key you give it becomes the block's type in saved data.
import { Blok } from '@bloklabs/core';
import { Paragraph } from '@bloklabs/core/tools';
import { CalloutTool } from './callout-tool';
const editor = new Blok({
holder: 'editor',
tools: {
paragraph: Paragraph, // the default block every empty editor starts with
callout: CalloutTool, // the key becomes the block's `type`
},
});Use it and save
Open the / menu, pick Callout, type into the box, and call save(). Your block appears in the output alongside the built-in ones.
const data = await editor.save();
// Your block round-trips exactly like a built-in one:
// {
// id: 'x9k2f1',
// type: 'callout',
// data: { text: 'Heads up — this is a callout.' },
// }Going further
Add a validate() method to drop empty blocks, a settings menu through renderSettings(), or paste handling with onPaste. The BlockTool interface covers the full lifecycle — rendered(), updated(), moved(), and removed().
// callout-tool.ts (extended)
export class CalloutTool {
// ...constructor, render() unchanged
save(block: HTMLElement) {
return { text: block.textContent ?? '' };
}
// Drop empty callouts when the editor saves.
validate(savedData: { text: string }) {
return savedData.text.trim().length > 0;
}
}
// text-color-tune.ts — a block tune adds a control to the settings menu.
export class TextColorTune {
static isTune = true;
render() {
return { title: 'Text color', icon: '🎨', onActivate: () => {/* recolor */} };
}
}import { Blok } from '@bloklabs/core';
import { CalloutTool } from './callout-tool';
import { TextColorTune } from './text-color-tune';
const editor = new Blok({
holder: 'editor',
tools: {
callout: { class: CalloutTool, tunes: ['textColor'] },
// Register the tune as a tool so a block can list it by name.
textColor: TextColorTune,
},
});validate() runs before save() resolves, so an empty callout never makes it into the output. Tunes are opt-in per tool — register the tune class as a tool (a class with static isTune = true), then list its name in tunes: [...] and Blok adds it to the block's settings menu.
For the complete tool contract, see the Tools API and check BlockData for the exact shape your `save()` feeds into.