A drag-and-drop, no-code-friendly visual builder for Slack Block Kit messages, packaged as an integration-agnostic React component.
Inspired by Slack's Block Kit Builder, reimagined as an embeddable React component you can drop into your own app.
Try the live demo → (mocked Slack — drag, drop, edit, preview)
The package owns the entire builder UX — palette, sortable preview surface, per-block popover editors, send dialog. It knows nothing about how channels are listed, who the user is, or how messages are sent. The consumer wires those concerns through callback props. A working end-to-end app is shown in block-kitchen-template.
pnpm add @tightknitai/block-kitchenPeer deps: react, react-dom.
import { BlockKitchen } from "@tightknitai/block-kitchen";
import "@tightknitai/block-kitchen/styles.css";
export function MyBuilderPage() {
return (
<BlockKitchen
workspaceName="Acme Inc."
loadChannels={async () => {
const res = await fetch("/api/slack/channels");
return res.json();
}}
loadSendAsUserStatus={async () => {
const res = await fetch("/api/slack/me/can-send-as-user");
return res.json();
}}
onSend={async ({ channelId, blocks, sendAsUser }) => {
const res = await fetch("/api/slack/messages/send", {
method: "POST",
body: JSON.stringify({ channelId, blocks, sendAsUser }),
});
return res.json();
}}
/>
);
}| Prop | Type | Required | Description |
|---|---|---|---|
workspaceName |
string |
no | Shown in the preview chrome to mimic a real Slack message header. |
initialBlocks |
SupportedBlock[] |
no | Starting draft. If omitted, the builder starts empty. |
onChange |
(blocks: SupportedBlock[]) => void |
no | Fires on every state change. Use this to persist the draft (URL, localStorage, etc). |
loadChannels |
() => Promise<{ id: string; name: string }[]> |
yes | Returns channels available to send to. The package never makes Slack API calls itself. |
loadSendAsUserStatus |
() => Promise<{ canSendAsUser: boolean; oauthUrl?: string }> |
yes | Whether the current user has a Slack user-token and can post as themselves. If canSendAsUser is false, oauthUrl is shown as a "Sign in with Slack" link. |
onSend |
(payload) => Promise<{ ok: boolean; error?: string }> |
yes | Called when the user submits the send dialog. Payload is { channelId, blocks, sendAsUser }. |
previewHooks |
PreviewHooks |
no | Hooks forwarded to slack-blocks-to-jsx's <Message> for resolving user / channel / emoji directives. |
palette |
PaletteSection[] |
no | The left-hand palette of draggable variants. Defaults to defaultPalette. Spread it to filter, reorder, or add your own pre-configured variants — see Customizing the palette. |
defaultOpenSections |
boolean | string[] |
no | Which palette section headers are expanded on first paint. true (default) opens all sections; false collapses all (Slack-style); an array opens only sections whose name is in the list (e.g. ['Section', 'Actions']). The palette also has a built-in search input that expands matching sections on demand. |
showPaletteSearch |
boolean |
no | Whether the palette renders the quick-search input above the section list. Defaults to true. Set false for compact palettes (e.g. when you've passed a small custom palette) where scanning by eye is faster than typing. |
paletteSearchPlaceholder |
string |
no | Placeholder text for the palette search input. Defaults to 'Search blocks…'. Useful for localization. |
allowedSurfaces |
PreviewSurface[] |
no | Allowlist of preview surfaces ('message', 'modal', 'app_home'). Defaults to ['message'] — surface dropdown is hidden when only one surface is allowed. The first entry is the initial selection. |
showThemeControl |
boolean |
no | Defaults to true. When false, the theme is locked to 'light'. |
defaultPreviewTheme |
'light' | 'dark' |
no | Pass the host app's current theme so the preview opens matched to the consuming app's appearance. |
theme |
BrandTheme | BrandPreset |
no | Branding tokens applied to the builder chrome (toolbar, palette, popovers, dialogs). Accepts a Partial<BrandTokens> map and optional light/dark overrides. See Styling below. |
The default palette ships with curated presets for every supported block type. To narrow what's available, or add your own pre-configured variants (e.g. a "Help footer" section), pass a palette array. Define it at module scope (or wrap in useMemo) so it stays referentially stable across renders.
import {
BlockKitchen,
defaultPalette,
type PaletteSection,
} from "@tightknitai/block-kitchen";
const PALETTE: readonly PaletteSection[] = [
...defaultPalette.filter((s) => s.blockType !== "input"),
{
name: "Company presets",
blockType: "section",
variants: [
{
id: "help_footer",
label: "help footer",
factory: () => ({
type: "section",
text: { type: "mrkdwn", text: "Need help? Reach out in <#C0HELP>." },
}),
},
],
},
];
<BlockKitchen palette={PALETTE} {...rest} />;Variant ids must be unique across the array — the drag-drop lookup keys by id.
The package is deliberately decoupled from any Slack SDK or backend. It does not import HTTP clients, OAuth libraries, or workspace-state systems. Everything I/O-shaped is brokered through props.
Helpers also exported:
import {
toSlackBlocks, // strips builder-only fields (e.g. header `level`) before sending
encodeBlocksToString, // base64url-encode a blocks array (for URL state)
decodeBlocksFromString,
defaultPalette, // the built-in palette — spread to customize
} from "@tightknitai/block-kitchen";
import type {
SupportedBlock,
SupportedBlockType,
BlockKitchenProps,
PaletteSection,
PaletteVariant,
SendPayload,
SendResult,
ChannelOption,
SendAsUserStatus,
PreviewHooks,
} from "@tightknitai/block-kitchen";The builder is frontend-only. For a full app that handles OAuth, channel listing, and chat.postMessage, see block-kitchen-template — a Vite + React SPA on Cloudflare Workers that wires this package to slack-hono on the backend.
Defense-in-depth: blocks are validated against slack-block-kit-validator before send. Issues are surfaced in the issues sheet with line numbers — users can fix them inline before posting.
Ships a compiled stylesheet at @tightknitai/block-kitchen/styles.css. The styles use CSS custom properties (--background, --primary, --border, etc.) for theming. Consumers must provide values for these vars — the standard shadcn/ui token set works as-is.
import "@tightknitai/block-kitchen/styles.css";For consumers who don't already have a shadcn token set on :root, the theme prop is a typed shortcut that writes a subset of tokens directly:
import type { BrandTheme } from "@tightknitai/block-kitchen";
const brand: BrandTheme = {
tokens: { primary: "262 83% 58%", radius: "0.75rem" },
dark: { primary: "263 70% 75%" }
};
<BlockKitchen theme={brand} {...rest} />tokensapplies in both light and dark contexts.lightanddarkoverride per mode; the dark variant kicks in under a standard.darkancestor class (next-themes default).- Color tokens take HSL component strings (
"262 83% 58%"), matching the underlying CSS variable contract;radiustakes a CSS length. - Scope is the builder chrome only. The embedded Slack preview keeps its native Slack styling regardless of
theme; usedefaultPreviewThemefor the preview's light/dark toggle.
The lower-level CSS-variable contract above keeps working; the theme prop simply layers on top of it.
Fonts are deliberately not part of BrandTheme. The builder sets no font-family of its own (aside from font-mono on the JSON viewer, which is intentional), so it inherits whatever the host page declares on <html> or <body>. Set your brand typography globally and the builder will pick it up automatically — no additional configuration needed. The Slack preview surface continues to render with Slack's own typography via slack-blocks-to-jsx.
MIT. See LICENSE.
Maintained by the Tightknit team.

