Skip to Content
AdvancedPlugin API

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-plugin

It 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, not foo. 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.x matches 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".
Last updated on