Block Kit
EmDash’s Block Kit lets sandboxed plugins describe their admin UI as JSON. The host renders the blocks — no plugin-supplied JavaScript ever runs in the browser.
How it works
Section titled “How it works”- User navigates to a plugin’s admin page
- The admin sends a
page_loadinteraction to the plugin’s admin route - The plugin returns a
BlockResponsecontaining an array of blocks - The admin renders the blocks using the
BlockRenderercomponent - When the user interacts (clicks a button, submits a form), the admin sends the interaction back to the plugin
- The plugin returns new blocks, and the cycle repeats
// Plugin admin route handlerroutes: { admin: { handler: async (ctx, { request }) => { const interaction = await request.json();
if (interaction.type === "page_load") { return { blocks: [ { type: "header", text: "My Plugin Settings" }, { type: "form", block_id: "settings", fields: [ { type: "text_input", action_id: "api_url", label: "API URL" }, { type: "toggle", action_id: "enabled", label: "Enabled", initial_value: true }, ], submit: { label: "Save", action_id: "save" }, }, ], }; }
if (interaction.type === "form_submit" && interaction.action_id === "save") { await ctx.kv.set("settings", interaction.values); return { blocks: [/* ... updated blocks ... */], toast: { message: "Settings saved", type: "success" }, }; } }, },}Block types
Section titled “Block types”| Type | Description |
|---|---|
header | Large bold heading |
section | Text with optional accessory element |
divider | Horizontal rule |
fields | Two-column label/value grid |
table | Data table with formatting, sorting, pagination |
actions | Horizontal row of buttons and controls |
stats | Dashboard metric cards with trend indicators |
form | Input fields with conditional visibility and submit |
image | Block-level image with caption |
context | Small muted help text |
columns | 2-3 column layout with nested blocks |
Element types
Section titled “Element types”| Type | Description |
|---|---|
button | Action button with optional confirmation dialog |
text_input | Single-line or multiline text input |
number_input | Numeric input with min/max |
select | Dropdown select |
toggle | On/off switch |
secret_input | Masked input for API keys and tokens |
Builder helpers
Section titled “Builder helpers”The @emdash-cms/blocks package exports builder helpers for cleaner code:
import { blocks, elements } from "@emdash-cms/blocks";
const { header, form, section, stats } = blocks;const { textInput, toggle, select, button } = elements;
return { blocks: [ header("SEO Settings"), form({ blockId: "settings", fields: [ textInput("site_title", "Site Title", { initialValue: "My Site" }), toggle("generate_sitemap", "Generate Sitemap", { initialValue: true }), select("robots", "Default Robots", [ { label: "Index, Follow", value: "index,follow" }, { label: "No Index", value: "noindex,follow" }, ]), ], submit: { label: "Save", actionId: "save" }, }), ],};Conditional fields
Section titled “Conditional fields”Form fields can be conditionally shown based on other field values:
{ "type": "toggle", "action_id": "auth_enabled", "label": "Enable Authentication"}{ "type": "secret_input", "action_id": "api_key", "label": "API Key", "condition": { "field": "auth_enabled", "eq": true }}The api_key field only appears when auth_enabled is toggled on. Conditions are evaluated client-side with no round-trip.
Try it
Section titled “Try it”Use the Block Playground to interactively build and test block layouts.