Skip to content
Block Kit for React
Esc
↑↓navigate↵open⌘Jpreview
On this page

Custom blocks

Render your own block and element types, or replace a built-in one.

Every block and element is looked up by its type in a registry. Add an entry to render a type the package doesn’t know, or replace an entry to change how a built-in type looks.

Unknown types otherwise render as a placeholder that names the type.

Add a block

A block component receives the block’s JSON, its blockId and its index:

import { blockComponents, Mrkdwn, type BlockProps } from "@nkootstra/block-kit";

interface StatusBlock {
  type: "status";
  state: "up" | "down";
  text: string;
}

function Status({ block }: BlockProps<StatusBlock>) {
  return (
    <div className={`status status--${block.state}`}>
      <Mrkdwn text={block.text} />
    </div>
  );
}

blockComponents.status = Status;

Register it once, before rendering, for example next to the stylesheet import. Now { "type": "status", "state": "up", "text": "All systems *operational*" } renders with your component, wherever it appears: messages, modals, the Home tab, and blocks nested in containers.

The block is wrapped in <div class="sbk-block sbk-block--status" data-block-id="…">, so it gets Slack’s spacing between blocks.

Add an element

Element components receive the element’s JSON and the blockId of the block they’re in. Use useBlockKit to report actions:

import { elementComponents, useBlockKit, type ElementProps } from "@nkootstra/block-kit";

interface RatingElement {
  type: "rating";
  action_id: string;
  max: number;
}

function Rating({ element, blockId }: ElementProps<RatingElement>) {
  const { dispatch } = useBlockKit();
  return (
    <span>
      {Array.from({ length: element.max }, (_, i) => (
        <button
          key={i}
          type="button"
          onClick={() =>
            dispatch({
              type: "rating",
              action_id: element.action_id,
              block_id: blockId,
              value: String(i + 1),
            })
          }
        >
          ★
        </button>
      ))}
    </span>
  );
}

elementComponents.rating = Rating;

The element works in actions, input and section accessories, and its clicks reach onAction like any built-in element’s. For an input, call setValue(blockId, element.action_id, { type: "rating", value }) on change so the value appears in state and the view_submission payload.

Replace a built-in

Assign over an existing entry, keeping the original to fall back to:

import { blockComponents } from "@nkootstra/block-kit";

const BuiltInImage = blockComponents.image;

blockComponents.image = (props) =>
  props.block.image_url?.startsWith("https://internal.") ? (
    <PrivateImage {...props} />
  ) : (
    <BuiltInImage {...props} />
  );

Rendering blocks yourself

Inside a custom block, render nested blocks and elements with <Block> and <Element> so they go through the registries too:

import { Block, type BlockProps, type Json } from "@nkootstra/block-kit";

function Panel({ block }: BlockProps<{ type: "panel"; blocks: Json[] }>) {
  return (
    <section className="panel">
      {block.blocks.map((child, i) => (
        <Block key={i} block={child} index={i} />
      ))}
    </section>
  );
}

Was this page helpful?