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:

PropTypeStabilityDescription
blockEditorBlockStableThe block node (type, ID, data, children, layout)
pathnumber[]StablePath in the tree (array of indices from root)
depthnumberStableNesting depth (0 = top level)
readOnlybooleanStableWhether the editor is in read-only mode
blockTypesBlockTypeInfo[]StableAvailable child block types (filtered by depth/allowlist)
maxDepthnumberStableMaximum nesting depth for this block's children
onaddBlock(index: number) => voidStableInsert a child at the given index
onremoveChild(index: number) => voidStableRemove a child at the given index
onmoveChild(from: number, to: number) => voidStableReorder a child
ondataChange(field: string, value: unknown) => voidStableUpdate a data field on this block
renderChildSnippet<[EditorBlock, BlockPath, number]>StableSnippet for recursive child rendering
onlayoutChange(layout: ContainerLayout) => voidExperimentalUpdate the container layout
adminFieldsRecord<string, VextroAdminFieldConfig> | nullExperimentalField definitions for inline editors
childCollapsedCountnumberExperimentalNumber of collapsed children
oncollapseAllChildren() => voidExperimentalCollapse all direct children
onexpandAllChildren() => voidExperimentalExpand all direct children
ongridZonesChange(zones: Record<string, string[]>) => voidExperimentalUpdate grid zone assignments
onaddBlockToCell(cellKey: string) => voidExperimentalInsert 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
Previous
Layout Block