Core concepts
Blok is driven by two plain objects: a config (what blocks exist and how they look) and a data document (the current content). Everything you see in the editor is derived from these.
<Blok config={config} data={data} onSave={...} onPublish={...} />Config
The Config object declares:
components— your block library. Each key is a block type.root— optional root wrapper (an HTML scaffold, theme provider, layout grid).categories— optional grouping for the block palette.
import type { Config } from "@useblok/core";
export const config: Config = {
components: {
Hero: { /* ... */ },
Features: { /* ... */ },
Testimonial: { /* ... */ },
},
root: {
fields: {
description: { type: "textarea", label: "SEO description" },
},
render: ({ children }) => <main>{children}</main>,
},
categories: {
marketing: { title: "Marketing" },
layout: { title: "Layout" },
},
};Components
A component is a user-definable block type. It has:
fields— field definitions used to generate the auto-form.defaultProps— values used when the block is inserted.render— the React component drawn on the canvas.getSummary— short label shown in the Layers tree and row headings.
Hero: {
label: "Hero",
description: "Big banner with a headline.",
folder: "Marketing", // grouping in the Blocks palette
accent: "#6366f1", // icon tint
fields: {
title: { type: "text", label: "Title", required: true },
subtitle: { type: "textarea", label: "Subtitle", rows: 3 },
},
defaultProps: { title: "Hello", subtitle: "" },
render: (props) => <section>{props.title}</section>,
getSummary: (props) => props.title as string,
}See the full field reference for all 12 field types.
Data
The data document is a plain JSON object:
interface Data {
root: { props?: Record<string, unknown> };
content: BlockInstance[]; // top-level blocks
zones?: Record<string, BlockInstance[]>; // nested (slot) blocks
}
interface BlockInstance {
type: string; // matches config.components[type]
props: { id: string } & Record<string, unknown>;
version?: number; // for migrations
}A minimal document:
{
"root": { "props": { "title": "Untitled" } },
"content": [
{ "type": "Hero", "props": { "id": "abc", "title": "Welcome" } }
]
}The shape is JSON-serialisable by design: save it, ship it between client and server, diff it, version it.
Slots (nested blocks)
A slot field lets a block contain other blocks — e.g. a Container that
wraps a stack of other blocks.
Container: {
fields: {
children: { type: "slot", label: "Contents" },
},
render: ({ blok }) => (
<div style={{ padding: 24 }}>
{blok.renderSlot("children")}
</div>
),
}Slot contents live in data.zones keyed by
`${blockId}:${slotFieldName}`. Use the slotZoneId(blockId, name)
helper if you need to compute it yourself.
Root
The root component wraps the whole document. Use it for an HTML
scaffold, a theme provider, or a layout grid. It has its own
fields (so the document can have settings like title, slug, meta
description) and optional render.
root: {
fields: {
title: { type: "text", label: "Page title" },
description: { type: "textarea", label: "SEO description" },
},
defaultProps: { title: "Untitled" },
render: ({ children, title }) => (
<main>
<title>{title}</title>
{children}
</main>
),
}Only one root exists per document. The Config panel in the editor exposes its fields.
The <Blok /> component
Everything above is passed to a single React component:
<Blok
config={config}
data={initialData}
title="My page"
onSave={(data) => { /* persist */ }}
onPublish={(data) => { /* go live */ }}
/>The editor owns the document state once mounted — don’t pass data as a
controlled prop. Treat it as the initial state. You receive the current
document in onSave, onPublish, and similar callbacks.