React Custom Tool
In this example, we will create a custom tool with React.
Each custom tool is built in its own custom.js file, which is then passed to unlayer.init.
If you want to create React-authored content with reusable email-safe components, see the React Elements Custom Tool example. It shows a custom tool that uses Unlayer Elements under the hood.
1. Use Unlayer's React
Unlayer exposes React globally. Reusing these globals avoids loading React twice and helps prevent version conflicts:
window.unlayer.Reactwindow.unlayer.ReactDOM
This example does not install or import react, react-dom, or react-dom/server.
/** @jsx React.createElement */
const React = window.unlayer.React;
const ReactDOM = window.unlayer.ReactDOM;
This example still uses a bundler to compile JSX into browser-ready JavaScript. The key is to bundle your custom tool code without bundling React, ReactDOM, or ReactDOMServer.
The included .babelrc file compiles JSX to React.createElement, so JSX uses Unlayer's React global instead of importing React from your bundle.
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "React.createElement"
}
]
]
}
2. Render React to HTML for exporters
Custom tool exporters must return an HTML string. This example targets current Unlayer builds, where window.unlayer.ReactDOM exposes createRoot and flushSync. To render a React component synchronously, create a root, wrap the render in flushSync, read the HTML, and unmount the root.
const renderReactToHtml = (element) => {
const el = document.createElement('div');
const root = ReactDOM.createRoot(el);
ReactDOM.flushSync(() => {
root.render(element);
});
const html = el.innerHTML;
root.unmount();
return html;
};
3. Register your tool
Create a custom.js file that includes the JavaScript to create your custom tool.
custom.js
/** @jsx React.createElement */
const React = window.unlayer.React;
const ReactDOM = window.unlayer.ReactDOM;
const renderReactToHtml = (element) => {
const el = document.createElement('div');
const root = ReactDOM.createRoot(el);
ReactDOM.flushSync(() => {
root.render(element);
});
const html = el.innerHTML;
root.unmount();
return html;
};
const Viewer = () => {
return <div>I am a custom tool.</div>;
};
unlayer.registerTool({
name: 'my_tool',
label: 'My Tool',
icon: 'fa-smile',
supportedDisplayModes: ['web', 'email'],
options: {},
values: {},
renderer: {
Viewer: Viewer, // this is your React component
exporters: {
web: function (values) {
return renderReactToHtml(<Viewer values={values} />);
},
email: function (values) {
return renderReactToHtml(<Viewer values={values} />);
},
},
head: {
css: function (values) {},
js: function (values) {},
},
},
});
4. Bundle and host your custom tool
Use a module bundler like Parcel, Webpack, or Vite to compile your custom tool code into a standalone JavaScript file. This example uses Parcel:
npm run build
After bundling, upload the output JavaScript file to a publicly accessible URL, such as https://yourdomain.com/custom-tool.js.
When configuring your bundler, make sure React and ReactDOM stay out of the output bundle. This example does that by reading window.unlayer.React and window.unlayer.ReactDOM directly, and by compiling JSX to React.createElement.
Do not import react-dom/server for custom tool exporters. Use the renderReactToHtml helper above instead.
5. Add customJS to Builder configuration
Pass the bundled custom.js URL to the editor in the customJS init option.
- The URL must be absolute
- You can load multiple URLs by passing more URLs in the array
- If you cannot pass a URL, you can directly pass your JavaScript code as a string
unlayer.init({
id: 'editor',
displayMode: 'email',
customJS: [
'https://examples.unlayer.com/examples/react-custom-tool/custom.js',
],
});
Preview
Here's a live running preview of our custom tool. Drag and drop the custom tool with a smiley face :)
React Tool with Custom Property Editor
Let's update the same React custom tool with a React property editor. This example creates a color picker with React hooks.
You can also use our built-in property editors to save time, check out the full list of available built-in property editors.
Add a hook-based property editor
const defaultTextColor = '#ff0000';
const colorOptions = [
{ label: 'Red', value: '#ff0000' },
{ label: 'Green', value: '#00aa00' },
{ label: 'Blue', value: '#0066ff' },
];
const MyColorPicker = (props) => {
const { value, updateValue } = props;
const currentValue = value || defaultTextColor;
const [selectedColor, setSelectedColor] = React.useState(currentValue);
React.useEffect(() => {
setSelectedColor(currentValue);
}, [currentValue]);
function selectColor(nextColor) {
setSelectedColor(nextColor);
updateValue(nextColor);
}
return (
<div>
{colorOptions.map((color) => (
<button
key={color.value}
type="button"
onClick={() => selectColor(color.value)}
style={{ background: color.value }}
>
{color.label}
</button>
))}
<input
type="color"
value={selectedColor}
onChange={(event) => selectColor(event.target.value)}
/>
</div>
);
};
unlayer.registerPropertyEditor({
name: 'my_color_picker',
Widget: MyColorPicker,
});
Use the new value in the existing React component:
const Viewer = (props) => {
const { values } = props;
return <div style={{ color: values.textColor }}>I am a custom tool.</div>;
};
Then expose the property editor in the tool options:
unlayer.registerTool({
name: 'my_tool',
options: {
default: {
title: null,
},
text: {
title: 'Text',
position: 1,
options: {
textColor: {
label: 'Color',
defaultValue: defaultTextColor,
widget: 'my_color_picker',
},
},
},
},
});
After updating your custom tool, bundle and host the updated JavaScript file, then pass the URL to the editor in the customJS init option.
unlayer.init({
id: 'editor',
displayMode: 'email',
customJS: [
'https://examples.unlayer.com/examples/react-custom-tool/custom-property-editor.js',
],
});
Property editor preview
Here's a live running preview of our custom tool with built-in editor. Drag and drop the custom tool with a smiley face, and then select it on the canvas to play with the color picker.