---
title: "Create a Custom Block Tool for Blok"
description: "Implement the BlockTool interface: render, save, validate, toolbox, pasteConfig, and sanitize, with a working example."
source: https://blokeditor.com/docs/custom-block-tool/
lastmod: 2026-09-07
---

Framework JavaScript

Getting started Create a custom block tool

# 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.

Last updated Jun 30, 2026 [Edit this page on GitHub](https://github.com/JackUait/blok/blob/main/docs/src/components/api/HowToCustomToolContent.tsx)

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.

1

## 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.

TypeScript

```
// callout-tool.ts
export class CalloutTool {
  private data: { text: string };

  // Shows the tool in the "/" menu. `section` groups the entry under a
  // labeled heading ('basic' | 'media' | 'database' | 'advanced'); omit it
  // to list the entry in the trailing unlabeled group.
  static get toolbox() {
    return { title: 'Callout', icon: '💡', section: 'basic' };
  }

  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 ?? '' };
  }
}
```

2

## 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.

TypeScript

```
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`
  },
});
```

3

## 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.

TypeScript

```
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()`.

TypeScript

```
// callout-tool.ts (extended)
export class CalloutTool {
  // ...constructor unchanged; render() now keeps its element as `this.box`.

  // Opt into read-only mode. Without it, creating the editor with
  // `readOnly: true` — or calling `readOnly.toggle(true)` — throws
  // "To enable read-only mode all connected tools should support it".
  static isReadOnlySupported = true;

  save(block: HTMLElement) {
    return { text: block.textContent ?? '' };
  }

  // Drop empty callouts when the editor saves.
  validate(savedData: { text: string }) {
    return savedData.text.trim().length > 0;
  }

  // Optional: flip the DOM in place. If any registered tool lacks
  // setReadOnly(), every toggle re-runs a full save/clear/render cycle.
  setReadOnly(state: boolean) {
    this.box.contentEditable = String(!state);
  }
}

// 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 */} };
  }
}
```

TypeScript

```
import { Blok } from '@bloklabs/core';
import { Paragraph } from '@bloklabs/core/tools';
import { CalloutTool } from './callout-tool';
import { TextColorTune } from './text-color-tune';

const editor = new Blok({
  holder: 'editor',
  tools: {
    paragraph: Paragraph, // the default block every empty editor starts with
    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. Read-only support is opt-in: without `static isReadOnlySupported = true`, creating the editor with `readOnly: true` — or calling `readOnly.toggle(true)` — throws "To enable read-only mode all connected tools should support it". Adding `setReadOnly(state)` on top lets the block flip in place; when any registered tool lacks it, every toggle re-runs a full save/clear/render cycle. 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](https://blokeditor.com/docs/tools-api/) and check [BlockData](https://blokeditor.com/docs/block-data/) for the exact shape your `save()` feeds into.
