Custom field types
Two ways to add a custom input to the Edit panel:
- Inline
type: "custom"— a one-off input on a single block. - Plugin field type — reusable across blocks (and possibly across projects).
Inline custom fields
For one-shot inputs, use a custom field:
{
brandColor: {
type: "custom",
label: "Brand color",
render: ({ value, onChange }) => (
<input
type="color"
value={(value as string) ?? "#000000"}
onChange={(e) => onChange(e.target.value)}
/>
),
},
}The render function receives:
interface CustomFieldRenderProps<Value> {
value: Value;
onChange: (value: Value) => void;
id: string;
name: string;
readOnly?: boolean;
}Keep the component controlled — always drive the DOM from value
and call onChange on edit. Blok handles history / undo around your
input automatically.
Plugin field types
For a field type used in many blocks (or shipped as a package), register it via a plugin:
import { definePlugin, type PluginFieldRenderProps } from "@useblok/core";
function ColorPickerField({ value, onChange, field }: PluginFieldRenderProps<string>) {
return (
<input
type="color"
value={value ?? "#000000"}
onChange={(e) => onChange(e.target.value)}
/>
);
}
export const colorFieldPlugin = definePlugin({
name: "@acme/color-field",
fields: {
"acme-color": { component: ColorPickerField },
},
});Then in block configs:
{
brandColor: { type: "acme-color", label: "Brand color" },
}The type string is matched at runtime. Namespaced names avoid
collisions across plugins.
Plugin field render props
interface PluginFieldRenderProps<Value> {
id: string;
name: string;
field: Record<string, unknown> & { type: string };
value: Value;
onChange: (value: Value) => void;
}field is the raw field config — cast to your own type shape if you
need extra options:
interface AcmeColorField {
type: "acme-color";
label?: string;
palette?: string[];
}
function ColorPickerField({ field, value, onChange }: PluginFieldRenderProps<string>) {
const { palette } = field as AcmeColorField;
// ...
}Good patterns
- Label + hint — Blok wraps your component in a
<FieldRow>with label, description, and required marker. You don’t need to add those. - Keyboard access — Make sure your input is reachable with Tab.
- Blur-commits — For expensive edits (like a large autocomplete),
debounce
onChange— but still fire on blur. - Undo friendliness — Each
onChangecreates a history entry. Batch rapid changes (like dragging a slider) with a debounce.
TypeScript note
Custom and plugin field types are validated at runtime — the
FieldType TypeScript union only knows about built-in types. If you
want type safety on your block configs, augment the module:
// your-types.d.ts
import "@useblok/core";
declare module "@useblok/core" {
interface FieldRegistry {
"acme-color": { palette?: string[] };
}
}(This is a forward-looking API — the registry augmentation shape may change before 1.0.)