Custom Blocks Guide
Learn how to create your own custom blocks to extend the form builder with any functionality you need.
Quick Example
Here's a complete example of creating a custom Alert block:
1. Create Your Block Component
// components/blocks/AlertBlock.tsx
import React from 'react';
import BaseBlock from '@seanblock/form-builder/blocks/BaseBlock';
import { Block } from '@seanblock/form-builder';
interface AlertBlockProps {
block: Block;
}
export default function AlertBlock({ block }: AlertBlockProps) {
const { metadata = {} } = block;
const {
variant = 'info', // info | warning | error | success
message = ''
} = metadata;
const colors = {
info: 'bg-blue-100 border-blue-500 text-blue-700',
warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
error: 'bg-red-100 border-red-500 text-red-700',
success: 'bg-green-100 border-green-500 text-green-700'
};
return (
<BaseBlock
block={block}
as="div"
className={`border-l-4 p-4 ${colors[variant]}`}
>
<p>{message}</p>
{/* Any nested blocks will render here automatically */}
</BaseBlock>
);
}2. Register the Block
// app/page.tsx
import FormBuilderV3 from '@seanblock/form-builder';
import AlertBlock from './components/blocks/AlertBlock';
const customBlocks = {
AlertBlock // Key must match blockType in JSON
};
export default function MyForm() {
return (
<FormBuilderV3
config={formConfig}
context={initialContext}
additionalBlocks={customBlocks}
/>
);
}3. Use in JSON
{
"id": "welcome-alert",
"blockType": "AlertBlock",
"metadata": {
"variant": "success",
"message": "Welcome to the form!"
},
"blocks": [
{
"id": "alert-link",
"blockType": "LinkBlock",
"metadata": {
"label": "Learn More",
"href": "/docs"
}
}
]
}Block Anatomy
Required Props
Every block component receives a block prop:
interface BlockProps {
block: Block; // The JSON block definition
}
function MyBlock({ block }: BlockProps) {
// ...
}Using BaseBlock
⚠️ Always wrap your content in BaseBlock
BaseBlock handles rendering nested blocks, applies className/style, evaluates displayConditions, and manages event delegation.
function MyBlock({ block }: BlockProps) {
return (
<BaseBlock block={block} as="section">
{/* Your content */}
</BaseBlock>
);
}BaseBlock Props
| Prop | Type | Description |
|---|---|---|
| block | Block | ✅ Required - The block definition |
| as | string | HTML tag or component (default: "div") |
| className | string | Additional CSS classes |
| children | ReactNode | Explicit children |
| disabled | boolean | Disable interactions |
Common Patterns
1. Simple Display Block
No state, just display content:
function BadgeBlock({ block }: BlockProps) {
const { metadata = {} } = block;
const { label, color = 'blue' } = metadata;
return (
<BaseBlock
block={block}
as="span"
className={`inline-block px-2 py-1 text-xs rounded bg-${color}-500 text-white`}
>
{label}
</BaseBlock>
);
}2. Container Block
Wraps nested blocks:
function SectionBlock({ block }: BlockProps) {
const { metadata = {} } = block;
const { title, subtitle } = metadata;
return (
<BaseBlock block={block} as="section" className="space-y-4">
{title && <h2 className="text-2xl font-bold">{title}</h2>}
{subtitle && <p className="text-gray-600">{subtitle}</p>}
{/* Nested blocks render here automatically */}
</BaseBlock>
);
}3. Input Block with Context
Reads/writes to form context:
import { useFormBuilder } from '@seanblock/form-builder';
function SliderBlock({ block }: BlockProps) {
const { id, metadata = {} } = block;
const { label, min = 0, max = 100, contextPath } = metadata;
const { context, updateContextValue } = useFormBuilder();
const value = context[contextPath] || min;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value);
updateContextValue(contextPath, newValue);
};
return (
<BaseBlock block={block} as="div" className="space-y-2">
<label htmlFor={id}>{label}: {value}</label>
<input
id={id}
type="range"
min={min}
max={max}
value={value}
onChange={handleChange}
className="w-full"
/>
</BaseBlock>
);
}4. Block with Actions
Trigger actions on events:
function ToggleBlock({ block }: BlockProps) {
const { metadata = {} } = block;
const { label, contextPath } = metadata;
const { context, updateContextValue, executeActions } = useFormBuilder();
const [isOn, setIsOn] = useState(context[contextPath] || false);
const handleToggle = async () => {
const newValue = !isOn;
setIsOn(newValue);
updateContextValue(contextPath, newValue);
// Execute onChange actions
const onChangeActions = block.actions?.filter(a => a.trigger === 'onChange');
if (onChangeActions?.length) {
await executeActions(onChangeActions);
}
};
return (
<BaseBlock block={block} as="div">
<button
onClick={handleToggle}
className={`toggle ${isOn ? 'on' : 'off'}`}
>
{label}: {isOn ? 'ON' : 'OFF'}
</button>
</BaseBlock>
);
}Best Practices
✅ DO
- Always use BaseBlock as the root element
- Type your metadata for better autocomplete
- Provide sensible defaults for metadata values
- Use contextPath for storing form data
- Handle loading/error states gracefully
- Make blocks responsive by default
❌ DON'T
- Don't manually render
block.blocks - Don't mutate context directly
- Don't skip BaseBlock
- Don't use global state
- Don't forget accessibility
useFormBuilder Hook
Access form state and methods in custom blocks:
import { useFormBuilder } from '@seanblock/form-builder';
function CustomBlock() {
const {
context, // Current form data
updateContextValue, // Update form data
executeAction, // Trigger actions
executeActions, // Trigger multiple actions
currentPage, // Current page ID
setCurrentPage, // Navigate to page
invalidList, // Validation errors
errors, // General errors
isLoading, // API loading state
config, // Form configuration
importedBlocks // Registered blocks
} = useFormBuilder();
}