@useblok/core
v0.2.1
Published
Visual block editor for React. Docs: https://docs.useblok.dev · Demo: https://demo.useblok.dev
Downloads
464
Maintainers
Readme
@useblok/core
Modern visual block editor for React. Drag-and-drop canvas, auto-generated forms from field definitions, slots, undo/redo, keyboard shortcuts, and a layered panel UI — with plugins, comments, versioning, audit, permissions, and realtime.
┌ Top bar ─────────────────────────────────────────────────────────┐
│ Title · History · Undo/Redo · Save · Publish │
├─┬──────────┬───────────────────────┬────────────┬─────────────────┤
│R│ Layers / │ │ Form / │ Edit · Info │
│a│ Blocks │ Live canvas │ Settings │ Tools · Config │
│i│ │ (device frame) │ │ │
│l└──────────┴───────────────────────┴────────────┴─────────────────┘Install
npm install @useblok/core
# or
pnpm add @useblok/core
# or
yarn add @useblok/corePeer deps: react ^18.2 || ^19 and react-dom ^18.2 || ^19.
Quickstart
// app/layout.tsx (once, globally)
import "@useblok/core/styles.css";// app/editor/page.tsx
"use client";
import { Blok, type Config, type Data } from "@useblok/core";
const config: Config = {
components: {
Hero: {
label: "Hero",
fields: {
title: { type: "text", label: "Title", required: true },
subtitle: { type: "textarea", label: "Subtitle", rows: 3 },
},
defaultProps: { title: "Hello", subtitle: "" },
render: ({ title, subtitle }) => (
<section>
<h1>{title}</h1>
<p>{subtitle}</p>
</section>
),
},
},
};
export default function Page() {
return (
<Blok
config={config}
onSave={(data: Data) => fetch("/api/save", { method: "POST", body: JSON.stringify(data) })}
/>
);
}The bundle ships with "use client", so it works from any Next.js Server Component import path too — you don't have to mark the importer yourself (though Blok itself must be rendered inside a client boundary, same as any interactive component).
Concepts
Components
A component is a user-defined block type. Declare its fields (which become the auto-form), a default shape, and a React renderer:
Hero: {
label: "Hero",
description: "Big banner with a headline and CTA.",
folder: "Marketing", // groups in the Blocks palette
accent: "#13a58f", // icon tint
fields: { /* ... */ },
defaultProps: { /* ... */ },
render: (props) => <section>{...}</section>,
getSummary: (props) => props.title as string, // shown in Layers/Edit rows
}Fields
The builtin field types:
| type | Renders | Value |
| --- | --- | --- |
| text | single-line input | string |
| textarea | multi-line | string |
| richtext | toolbar + textarea (markdown-flavoured) | string |
| number | number input (min/max/step) | number |
| boolean | toggle switch | boolean |
| select | dropdown | unknown |
| radio | segmented pill | unknown |
| link | URL + label + target | { url, label, target } |
| asset | URL + preview + alt | { url, alt } |
| array | collapsible list of sub-forms | Record<string, unknown>[] |
| object | inline nested form | Record<string, unknown> |
| slot | canvas drop zone | blocks stored in data.zones |
| custom | whatever you render | anything |
Every field supports label, description, required, icon. Required fields show a red * in the form.
Slots
A slot lets a block contain other blocks — think <Container> wrapping a stack of sections. Declare a slot field, then use the blok.renderSlot helper in your render:
Container: {
fields: {
background: { type: "select", options: [...] },
children: { type: "slot", label: "Contents" },
},
defaultProps: { background: "#fff" },
render: ({ background, blok }) => (
<div style={{ background, padding: 24 }}>
{blok.renderSlot("children")}
</div>
),
}Slot blocks are stored in data.zones[${blockId}:${fieldName}]. Use slotZoneId(blockId, fieldName) to compute the key.
Root
Wrap the whole document in a root component (an HTML scaffold, theme provider, layout grid, whatever):
root: {
fields: { description: { type: "textarea", label: "SEO description" } },
defaultProps: { title: "Untitled" },
render: ({ children }) => <main>{children}</main>,
}Categories
Organise the Blocks palette beyond single-folder grouping:
categories: {
marketing: { title: "Marketing", description: "Hero, CTAs, grids." },
layout: { title: "Layout", defaultExpanded: false },
},Props
<Blok
config={config}
data={initialData} // optional; defaults to empty doc
title="Untitled" // shown in TopBar
slug="docs / new" // shown under title
previewUrl="/preview" // link-to-preview icon
historyLimit={100} // undo ring buffer size
onSave={(data) => { /* ... */ }} // ⌘S, Save button, Publish
onPublish={(data) => { /* ... */ }}
/>onSave and onPublish receive the current document from the store — safe to read in async handlers.
Keyboard shortcuts
| Keys | Action |
| --- | --- |
| ⌘/Ctrl + S | Save |
| ⌘/Ctrl + Z · ⌘⇧Z | Undo · Redo |
| ⌘/Ctrl + D | Duplicate selected block |
| ↑ / ↓ | Select sibling block |
| ⌥ Alt + ↑ / ↓ | Move selected block |
| Delete · Backspace | Remove selected |
| Esc | Clear selection |
Hooks (for custom UI)
Everything the builtin UI does is driven by hooks you can re-use:
useData() // current document
useConfig() // full Config
useSelection() // selected block id (or null)
useEditing() // the block whose Edit form is open
useHistory() // { canUndo, canRedo }
useDirty() // true if changes since last save
useViewport() // "desktop" | "tablet" | "mobile" | "fullscreen"
useDrillPath() // drill tokens when focused into an array/slot
useActions() // { addBlock, moveBlock, duplicateBlock, undo, redo, ... }
useKeyboardShortcuts({ onSave }) // register the default shortcut setEvery hook must be called inside a <Blok> or a <BlokStoreProvider>.
Headless mode
Drive the editor yourself with BlokStoreProvider:
import { BlokStoreProvider, useData, useActions } from "@useblok/core";
<BlokStoreProvider config={config} data={initial}>
<MyOwnCanvas />
<MyOwnPanel />
</BlokStoreProvider>Data shape
interface Data {
root: { props?: Record<string, unknown> };
content: BlockInstance[]; // top-level blocks
zones?: Record<string, BlockInstance[]>; // keyed by `${blockId}:${slotFieldName}`
}
interface BlockInstance {
type: string; // key in config.components
props: { id: string } & Record<string, unknown>;
}The shape is JSON-serialisable by design: save it, ship it between server and client, version it.
License
MIT.
