An editor may already have one list of available block types: text, image, code, quote, button, and others.
At first, that list is used by a single "Add block" button. Later, a new convenience appears: insert a block between two existing blocks.
The simplest implementation may look obvious: create another list of available block types for the "insert between" button.
That is where the problem starts.
Duplicated Capability Lists Almost Always Drift Over Time
const toolbarBlocks = [ 'text', 'image', 'code', 'quote',]; const insertBetweenBlocks = [ 'text', 'image', 'code',];At first, both arrays may be identical.
A month later, someone adds a new block to the toolbar and forgets the insert-between menu. Later, one list gets a different order, icon, or availability rule.
The UI begins to look as though the product supports different capabilities in different places even though the business action is the same.
The Registry Should Describe the Capability, Not a Specific Button
It is better to keep one registry that answers: "which block types can this editor create?"
The toolbar, insert-between button, command palette, or context menu simply reads that registry and decides how to present the same capabilities in its own UI.
export const blockRegistry = [ { type: 'text', labelKey: 'blocks.text', icon: TextIcon, create: () => ({ type: 'text', data: {}, }), }, { type: 'image', labelKey: 'blocks.image', icon: ImageIcon, create: () => ({ type: 'image',A New Entry Point Should Reuse the Registry Instead of Copying It
const availableBlocks = computed(() => blockRegistry); const insertBetween = ( index, definition,) => { const block = definition.create(); blocks.value.splice( index + 1, 0, block, );};The new button only knows the insertion position.
It does not decide which types exist, how an initial payload is created, or which icon belongs to a block. That responsibility already belongs to the registry.
A Capability Registry Can Hold More Than a List of Types
As the editor grows, a simple type value is often no longer enough.
The registry may contain:
- label and icon;
- factory for initial data;
- menu group;
- availability condition;
- whether the block can be inserted between other blocks;
- whether it is allowed in a particular editor mode.
If an Entry Point Has Restrictions, Filter the Registry Instead of Forking It
const insertBetweenBlocks = computed(() => blockRegistry.filter( (block) => block.canInsertBetween !== false, ) );This distinction matters.
An entry point may show only a subset of capabilities, but it should still start from the same canonical set.
We add a filtering rule, not a second source of truth.
The Same Action Should Share Not Only the List but the Creation Path
Even if two buttons read the same type array, duplication may remain elsewhere.
For example, the toolbar calls a factory from the registry while insert-between manually constructs the block object.
When a new initial field is added, one entry point creates the correct block while the other creates an outdated one.
A Shared Registry Does Not Mean Identical UI
A toolbar may show icons without labels. An insert-between menu may show an icon, label, and description. A command palette may add search.
That is fine.
The shared part is the knowledge about the capability, not the way it is visually presented.
Context Rules Should Live Close to the Capability
{ type: 'image', canUse: ({ settings, readonly, }) => settings.imagesEnabled && ! readonly,}If the "images disabled" rule is checked separately in the toolbar and separately in the insert-between menu, those checks can drift too.
It is better for both UI elements to ask the same capability definition.
What to Test
1. Toolbar and insert-between use the same registry.2. Adding a new block type makes it appear in both entry points.3. A capability disabled by settings disappears from both.4. Entry-point-specific filters do not mutate the registry.5. Both entry points use the same block factory.6. Initial block data is identical regardless of where creation started.7. Reordering registry items changes both menus consistently.8. One entry point can render different UI without duplicating capability data.This Pattern Applies Far Beyond a Block Editor
The same principle is useful for:
- toolbar commands and command palettes;
- table-row actions and bulk actions;
- entity creation from a header button and an empty state;
- context menus and primary toolbars;
- keyboard shortcuts and normal buttons.
If all of these entry points trigger the same capability, they should reuse one shared capability definition.
A New UI Entry Point Should Not Create a New Source of Truth
When a new button or menu is added, the main question is not "which list should it show?" but "where is this capability already defined?"
If the action is the same, its capability registry, creation factory, and availability rules should remain shared.
A second way to perform the same action should be a new entry point into the existing capability, not a second implementation of that capability.
A new UI entry point should add a new way to access a capability, not a new list of truth.



