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