Blocks
Renderer Registry API
The renderer registry lets you replace the default block editor body with a custom Svelte 5 component for any block type. This is how the built-in Layout block renders its stack/flex/grid modes, and you can use the same mechanism for your own blocks.
Registering a custom body
Create a headless Svelte component that calls setBlockEditorBodies and render it with client:load:
<!-- src/components/BlockBodiesInit.svelte -->
<script lang="ts">
import { setBlockEditorBodies } from "vextro";
import type { VextroBlockEditorBodyComponent } from "vextro";
import HeroBody from "./blocks/HeroBody.svelte";
const bodies = new Map<string, VextroBlockEditorBodyComponent>();
bodies.set("hero", HeroBody);
setBlockEditorBodies(bodies);
</script> <!-- src/layouts/AdminLayout.astro -->
---
import BlockBodiesInit from "../components/BlockBodiesInit.svelte";
---
<BlockBodiesInit client:load />
<slot /> To register a single body without replacing others:
import { addBlockEditorBody } from "vextro";
import GalleryBody from "./blocks/GalleryBody.svelte";
addBlockEditorBody("gallery", GalleryBody); Props contract
Your component receives VextroBlockEditorBodyProps:
| Prop | Type | Stability | Description |
|---|---|---|---|
block | EditorBlock | Stable | The block node (type, ID, data, children, layout) |
path | number[] | Stable | Path in the tree (array of indices from root) |
depth | number | Stable | Nesting depth (0 = top level) |
readOnly | boolean | Stable | Whether the editor is in read-only mode |
blockTypes | BlockTypeInfo[] | Stable | Available child block types (filtered by depth/allowlist) |
maxDepth | number | Stable | Maximum nesting depth for this block's children |
onaddBlock | (index: number) => void | Stable | Insert a child at the given index |
onremoveChild | (index: number) => void | Stable | Remove a child at the given index |
onmoveChild | (from: number, to: number) => void | Stable | Reorder a child |
ondataChange | (field: string, value: unknown) => void | Stable | Update a data field on this block |
renderChild | Snippet<[EditorBlock, BlockPath, number]> | Stable | Snippet for recursive child rendering |
onlayoutChange | (layout: ContainerLayout) => void | Experimental | Update the container layout |
adminFields | Record<string, VextroAdminFieldConfig> | null | Experimental | Field definitions for inline editors |
childCollapsedCount | number | Experimental | Number of collapsed children |
oncollapseAllChildren | () => void | Experimental | Collapse all direct children |
onexpandAllChildren | () => void | Experimental | Expand all direct children |
ongridZonesChange | (zones: Record<string, string[]>) => void | Experimental | Update grid zone assignments |
onaddBlockToCell | (cellKey: string) => void | Experimental | Insert a block into a grid cell |
Semver policy
- Stable props will not change without a major version bump. Build against these confidently.
- Experimental props may change in minor releases. Use them, but expect to update your component when upgrading Vextro.
- Props not listed above are internal and may change or disappear without notice.
Custom body components run with full client-side privileges inside the admin panel. Only register components from trusted sources. A malicious body could exfiltrate document data or corrupt the block tree.
Rendering children
Always use the renderChild snippet for recursive child rendering:
{#each block.children ?? [] as child, i (child.blockId)}
{@const childPath = [...path, i]}
{@render renderChild(child, childPath, depth + 1)}
{/each} Do not implement your own block chrome or nested rendering — renderChild handles collapse state, DnD, context menus, and all block chrome functionality.
Fallback behavior
If no custom body is registered for a block type:
- Blocks with children use a default stack layout renderer that arranges children vertically with insertion points between them
- Blocks without children render their inline fields and show a collapsed preview when minimized