There are several options for creating your own custom plugin. On this page, explore relevant APIs .
The Builder.register() method accepts two arguments:
- A string, representing the types of custom plugin.
- A configuration value, which can be of any type.
Builder.register(key, config);The config data type is dependent on the key. Some custom plugins require a callback function, while others require an object or a different data type.
The method does not have a return value.
Listed below are several different options for custom plugin types. Each of these types can be passed as a string to the first argument of Builder.register().
Adds a new section of Components of your choice that are registered and grouped into the newly described menu. Selected components can be custom or built in.
Builder.register("insertMenu", {
name: "My Custom Menu",
items: [{ name: "Box" }, { name: "Text" }, { name: "Image" }],
});The code above results in a new section within the Visual Editor, as shown below.
For more details, visit Register custom components.
The app.onLoad plugin type runs code when the Builder application is opened by a user.
Builder.register('app.onLoad', (appActions) => {
// ...
});For this plugin type, a callback function is passed as the second argument. This callback function provides an AppActions object:
interface AppActions {
triggerSettingsDialog(pluginId: string): Promise<void>;
}The triggerSettingsDialog() function is typically called on first load to prompt the user to configure the plugin if it hasn't been connected yet.
The code below provides an example of prompting users to configure the plugin on load.
/** @jsx jsx */
import { jsx } from "@emotion/core";
import { Builder } from '@builder.io/react';
import appState from '@builder.io/app-context';
const PLUGIN_ID = 'my-plugin';
Builder.register('app.onLoad', async ({ triggerSettingsDialog }) => {
// Check if the user has already configured the plugin
const pluginSettings = appState.user.organization.value.settings.plugins.get(PLUGIN_ID);
const hasConnected = pluginSettings?.get('hasConnected');
if (!hasConnected) {
// Open the settings dialog on first load so the user can configure it
await triggerSettingsDialog(PLUGIN_ID);
}
});The editor.onLoad type runs code when the Visual Editor is opened.
Builder.register('app.onLoad', (contentEditorActions) => {
// ....
});For this plugin type, a callback function is passed as the second argument. This callback function provides a ContentEditorActions object:
interface ContentEditorActions {
updatePreviewUrl(url: string): void;
safeReaction<T>(
watchFunction: () => T,
reactionFunction: (arg: T) => void,
options?: { fireImmediately: true }
): void;
}This object contains two keys:
updatePreviewUrl(url): programmatically sets the preview URL shown in the editor iframe.safeReaction(watchFn, reactionFn, options?): a MobX-style reactive subscription.watchFn()is tracked for observable changes; whenever its return value changes,reactionFn()is called with the new value. The optional{ fireImmediately: true }runsreactionFn()once immediately on registration.
The code below demonstrates forcing previewUrl changes when the locale is updated.
/** @jsx jsx */
import { jsx } from "@emotion/core";
import { Builder } from '@builder.io/react';
import appState from '@builder.io/app-context';
Builder.register('editor.onLoad', ({ updatePreviewUrl, safeReaction }) => {
// safeReaction watches an observable expression.
// Whenever the watched value changes, the reaction function is called.
safeReaction(
// Watch: return the value you want to observe from appState
() => appState.designerState.editingContentModel?.data.get('locale'),
// React: called whenever the watched value changes
(locale) => {
if (!locale) return;
const baseUrl = appState.editingModel?.examplePageUrl;
if (baseUrl) {
// Update the preview iframe to reflect the new locale
const previewUrl = `${baseUrl}?locale=${locale}`;
updatePreviewUrl(previewUrl);
}
},
// Optional: fire immediately on load with the current value
{ fireImmediately: true }
);
});The editor.settings option accepts a configuration object to control the visibility and behavior of Builder's visual editor UI.
All properties are optional booleans.
Builder.register('editor.settings', {
// Toolbar & chrome
hideToolbar: true, // Hides the top editor toolbar
hideHeatMap: true, // Hides the heatmap overlay toggle
// Main tab bar
hideMainTabs: true, // Hides the top-level tab navigation
// Right panel tabs
hideStyleTab: true, // Hides the Style tab in the right panel
hideDataTab: true, // Hides the Data tab in the right panel
hideLayersTab: true, // Hides the Layers tab in the right panel
hideAnimateTab: true, // Hides the Animate tab in the right panel
hideABTab: true, // Hides the A/B test tab in the right panel
hideOptionsTab: true, // Hides the Options tab in the right panel
// Insert panel
hideFormComponents: true, // Hides form-related components from the Insert panel
hideTemplates: true, // Hides the Templates section in the Insert panel
hideSymbols: true, // Hides the Symbols section in the Insert panel
customInsertMenu: true, // Replaces the default Insert menu with only custom-registered insert menus
// Other editor UI
hidePageUrlEditor: true, // Hides the page URL editor
componentsOnlyMode: true, // Restricts the editor to component insertion only, hiding layout/style tooling
});The editor.header type a custom React component to replace or augment the header bar rendered at the top of the content editor. It accepts a single property.
Builder.register("editor.header", {
component: () => <h1>Hello, Builder!</h1>,
});This option can be used to include an important banner or notification for users.
The editor.toolarButton type registers a custom React component to be rendered inside the content editor's top toolbar.
Builder.register("editor.toolbarButton", {
component: () => <button>Toolbar Button</button>,
});This is well-suited for controls that need to interact with the editor's live state, such as dropdowns, toggles, or icon buttons that modify the preview.
For more details, visit Build a custom plugin.
Registers a custom React component as an additional tab in the left panel of the content editor. It accepts two properties:
- name: a string
- component: a React component
For example, the code below adds a new tab to the Visual Editor, with the name My Tab.
const MyTab = () => {
const content = context.designerState.editingContentModel;
return (
<div style={{ padding: 16 }}>
<h3>My Custom Tab</h3>
<p>Editing: {content?.name}</p>
</div>
);
};
Builder.register("editor.editTab", {
name: "My Tab",
component: MyTab,
});This type of plugin is good for complex components that interact with the Visual Editor and other integrations.
Registers a custom React component as an additional tab in the content editor's center panel. It accepts two properties:
- name: a string
- component: a React component
For example, the code below adds a new tab to the Visual Editor, with the name My Tab.
const MyTab = () => {
const content = context.designerState.editingContentModel;
return (
<div style={{ padding: 16 }}>
<h3>My Custom Tab</h3>
<p>Editing: {content?.name}</p>
</div>
);
};
Builder.register("editor.mainTab", {
name: "My Tab",
component: MyTab,
});This type of plugin is good for complex components that interact with the Visual Editor and other integrations.
The video below shows an example of where this tab appears.
Registers a custom React component to replace the toolbar rendered directly above the preview iframe in the content editor. It accepts a single property:
- component: a React component
Builder.register('editor.previewToolbar', {
component: () => <h1>My Component</h1>,
});This can be used to simply remove the preview toolbar, however a new toolbar can be added in its place.
In the example below, a new toolbar is used, leveraging mobx-react. This replaces some of the existing functionality of the preview toolbar.
import { useObserver } from 'mobx-react';
const context = require('@builder.io/app-context').default;
const PreviewToolbar = () =>
useObserver(() => (
<div style={{ display: 'flex', alignItems: 'center', padding: '0 8px' }}>
<button onClick={() => (context.designerState.artboardSize.width = 2000)}>Desktop</button>
<button onClick={() => (context.designerState.artboardSize.width = 642)}>Tablet</button>
<button onClick={() => (context.designerState.artboardSize.width = 321)}>Mobile</button>
<button
style={{ marginLeft: 'auto' }}
onClick={() => context.designerState.undo()}
disabled={!context.designerState.canUndo}
>
Undo
</button>
<button
onClick={() => context.designerState.redo()}
disabled={!context.designerState.canRedo}
>
Redo
</button>
</div>
));
Builder.register('editor.previewToolbar', { component: PreviewToolbar });The video below shows an example of the new toolbar.
Registers a custom action that appears in the content list's per-item action menu. The action menu is the three-dot menu next to each content entry.
The content.action type has the following method signature:
Builder.register('content.action', {
label: string,
showIf(content: any, model: any): boolean,
onClick(content: any): Promise<void>
});Each key-value pair is required and can be described as follows:
| Property | Type | Description |
|---|---|---|
|
| The display text for the action in the menu. |
|
| Controls visibility. Receives the current content entry and its model; return true to show the action. |
|
| Called when the user selects the action. Receives the full content entry object. |
The example below demonstrates how you could make a request to your own internal server with content entry details.
import appState from "@builder.io/app-context";
Builder.register("content.action", {
label: "Record Internally",
showIf(content, model) {
return model.kind === "page";
},
async onClick(content) {
await fetch("https://your-deploy-hook.example.com/record", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contentId: content.id,
modelName: content.modelName,
url: content.data?.url,
}),
});
appState.snackBar.show("Successfully recorded.", 3000);
},
});
The video below shows an example of this action in use.
Registers a custom action that appears in the content list's per-item action menu. The action menu is the three-dot menu next to each content entry.
The content.action type has the following method signature:
Builder.register('content.action', {
label: string,
showIf(content: any, model: any): boolean,
onClick(content: any): Promise<void>
});Each key-value pair is required and can be described as follows:
| Property | Type | Description |
|---|---|---|
|
| The display text for the action in the menu. |
|
| Controls visibility. Receives the current content entry and its model; return true to show the action. |
|
| Called when the user selects the action. Receives the full content entry object. |
The example below demonstrates how you could make a request to your own internal server with content entry details.
import appState from "@builder.io/app-context";
Builder.register("content.action", {
label: "Record Internally",
showIf(content, model) {
return model.kind === "page";
},
async onClick(content) {
await fetch("https://your-deploy-hook.example.com/record", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contentId: content.id,
modelName: content.modelName,
url: content.data?.url,
}),
});
appState.snackBar.show("Successfully recorded.", 3000);
},
});
The video below shows an example of this action in use.
Registers a custom action that appears when one or more content entries are selected in the content list, enabling operations across multiple items at once.
The content.bulkAction type has the following method signature:
Builder.register('content.bulkAction', {
label: string,
showIf(selectedContentIds: string[], content: any[], model: any): boolean,
onClick(
actions: { refreshList: () => void },
selectedContentIds: string[],
content: any[],
model: any
): Promise<void>,
});Each key-value pair is required and can be described as follows:
The label is a string. It is the display text for the action in the bulk actions menu.
The return value of this method controls the visibility of the action based on the current selection and model.
(selectedContentIds, content, model) => booleanThe method has access to the following parameters:
selectedContentIds: array of IDs of the currently selected content entries.content: array of all content entry objects currently loaded in the list.model: the model for the current content list view.
When the bulk action is clicked, this function is called.
(actions, selectedContentIds, content, model) => Promise<void>The method has access to the following parameters:
actions: this object contains a single key,refreshList, which is a function that can be called upon completing the operation to reload the content list and reflect any changes.selectedContentIds: array of IDs of the currently selected content entries.content: array of all content entry objects currently loaded in the list.model: the model for the current content list view.
The example below demonstrates how you might trigger a bulk call to your own internal server.
import appState from '@builder.io/app-context';
Builder.register('content.bulkAction', {
label: 'Store Page Details',
showIf(selectedContentIds, content, model) {
// Only relevant for page models
return model.kind === 'page';
},
async onClick(actions, selectedContentIds, content, model) {
const pages = selectedContentIds.map(id => {
const entry = content.find(e => e.id === id);
return {
id: entry.id,
name: entry.name,
url: entry.data?.url,
published: entry.published,
};
});
await fetch('https://your-service.example.com/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pages }),
});
appState.snackBar.show(`Stored details for ${pages.length} pages.`, 3000);
actions.refreshList();
},
});The video below shows an example of this action in use.
Registers a custom action that appears in the model settings menu, operating on a Builder model rather than content entries or canvas elements.
The model.action type has the following method signature:
Builder.register('model.action', {
name: string,
showIf?(model?: any): boolean,
onClick(model: any): void | Promise<void>,
});Each key-value pair is required and can be described as follows:
| Property | Type | Required | Description |
|---|---|---|---|
|
| Yes | The display label for the action. |
|
| No | Controls visibility of the action. |
|
| Yes | Called when the user selects the action. Receives the full model object. |
The example below demonstrates how you could make a request to your own internal server with details of the Model. This example makes use of appState.
import appState from "@builder.io/app-context";
Builder.register("model.action", {
name: "Report Model for Review",
showIf() {
return appState.user.can("admin");
},
async onClick(model) {
const confirmed = await appState.dialogs.confirm(
`Report the "${model.name}" model for review?`,
"Report",
);
if (!confirmed) return;
appState.globalState.showGlobalBlockingLoadingIndicator = true;
await fetch("https://your-service.example.com/models", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: model.name,
kind: model.kind,
}),
});
appState.globalState.showGlobalBlockingLoadingIndicator = false;
appState.snackBar.show(`Model "${model.name}" reported for review.`, 3000);
},
});The video below shows an example of this action in use.
Configures global appearance and navigation settings for the Builder app. Please note that this action requires both an Enterprise subscription and white labeling. Contact Builder support for more details.
The appSettings action has the following method signature:
Builder.register('appSettings', {
settings?: {
hideDefaultTabs?: boolean,
hideLeftSidebar?: boolean,
defaultRoute?: string,
},
theme?: {
logo?: string,
colors?: {
primary?: string,
secondary?: string,
},
mui?: object,
},
});An example is included below:
Builder.register('appSettings', {
settings: {
hideDefaultTabs: true,
hideLeftSidebar: true,
defaultRoute: '/apps/my-plugin',
},
theme: {
logo: 'https://your-cdn.example.com/logo.png',
colors: {
primary: 'rgb(30, 100, 200)',
secondary: 'rgb(20, 80, 160)',
},
mui: {
typography: {
fontFamily: 'Inter, sans-serif',
},
},
},
});