Plugin API
Plugins let you ship reusable collections of blocks, field types, and
UI without modifying the editor. Pass them via the plugins prop on
<Blok>.
import { Blok } from "@useblok/core";
import { marketingBlocks } from "@acme/blok-marketing";
import { mapFieldPlugin } from "@acme/blok-map-field";
<Blok config={config} plugins={[marketingBlocks, mapFieldPlugin]} />BlokPlugin shape
interface BlokPlugin {
name: string; // Unique id for dedup and logging
version?: string;
blocks?: Record<string, ComponentConfig>;
categories?: Record<string, Category>;
fields?: Record<string, PluginFieldRenderer>;
leftPanels?: PluginLeftPanel[];
aiActions?: AiAction[];
onInit?: (ctx: { config: Config }) => void;
}Use definePlugin() for typed autocomplete:
import { definePlugin } from "@useblok/core";
export default definePlugin({
name: "@acme/marketing",
version: "1.0.0",
blocks: { /* ... */ },
});Contributing blocks
definePlugin({
name: "@acme/marketing",
blocks: {
PricingTable: { /* ComponentConfig */ },
Testimonial: { /* ComponentConfig */ },
},
categories: {
marketing: { title: "Marketing", defaultExpanded: true },
},
});Plugin blocks are merged into config.components. If a plugin defines a
block with the same name as an existing one, last plugin wins —
plugins override core config in the order they’re listed.
Contributing field types
import type { PluginFieldRenderProps } from "@useblok/core";
function ColorPickerField({ value, onChange }: PluginFieldRenderProps<string>) {
return <input type="color" value={value ?? "#000"} onChange={(e) => onChange(e.target.value)} />;
}
definePlugin({
name: "@acme/color-field",
fields: {
"color-picker": { component: ColorPickerField },
},
});Then use it in a block config:
{
brandColor: { type: "color-picker", label: "Brand color" },
}The type string must match the plugin’s fields key. TypeScript
won’t know about it — plugin field types are a runtime contract.
Contributing left-panel tabs
Add a whole new tab to the left rail:
import { Sparkles } from "lucide-react";
definePlugin({
name: "@acme/inbox",
leftPanels: [
{
key: "inbox",
label: "Inbox",
icon: Sparkles,
order: 150, // lower = higher in rail
component: InboxPanel,
},
],
});The component renders inside the left panel container. Use any of
Blok’s hooks (useData, useActions, useSelection, etc.) to read
and mutate editor state.
Contributing AI actions
See AI actions for the full reference. Plugins can ship pre-packaged field-level or block-level actions:
import { defineAiAction } from "@useblok/core";
definePlugin({
name: "@acme/writing-tools",
aiActions: [
defineAiAction({
kind: "field",
id: "shorten",
label: "Shorten",
appliesTo: (field) => field.type === "textarea",
run: async (ctx) => {
const shortened = await ctx.ai.complete({
prompt: `Shorten this:\n${ctx.value}`,
});
ctx.setValue(shortened);
},
}),
],
});onInit
Runs once when the editor mounts. Safe place for side-effect setup:
definePlugin({
name: "@acme/metrics",
onInit: ({ config }) => {
console.log(`Blok booted with ${Object.keys(config.components).length} components`);
},
});Packaging a plugin
See the create-blok-plugin scaffolder:
pnpm create blok-plugin my-pluginIt sets up a TypeScript package with definePlugin, the right peer
dependencies, and an example block — ready to publish to npm.
Best practices
- Namespace names —
@acme/foo, notfoo. Plugin name collisions are noisy. - Don’t ship React globally — mark React as a peer dependency.
- Pin Blok as a peer dependency —
@useblok/core: ^0.xmatches the API you tested against. - Avoid overriding core blocks — if you do, document it loudly in your README.
- Keep field type keys namespaced —
"acme-color-picker"beats"color".