Add a Custom Tool
Extend the editor with functionality it doesn't ship with — one-tap looks, brand
assets, stamps, AI actions — without forking the component. Register it through
config.customTools and it appears alongside the built-in tools. A custom tool
isn't limited to inert UI: it can mutate the image through the same engine the
built-in tools use, so edits are undoable and land on export.
When to use this
- You need domain-specific functionality (e.g. a signature look or an AI action).
- You want the new tool to feel native — same sidebar, same active-panel behaviour.
- You'd rather compose than fork the editor.
Register the tool
Each entry provides an id, a label, an icon component for the sidebar, and a
panel component shown when the tool is active.
const LooksTool = {
id: "looks",
label: "Looks",
icon: LooksIcon,
panel: LooksPanel,
};
<ImageEditor
src="/photo.jpg"
config={{ customTools: [LooksTool] }}
/>
Reach the engine from your panel
Panel components receive no props. Read editor state through the exported hooks
(useConfig, useImageEditorStore), and get the engine from the onReady
handle — share it with your panel via React context:
import { EFFECT_FILTER_NAME } from "@editx/engine";
import { ImageEditor, useImageEditorStore } from "@editx/image-editor";
import { createContext, useContext, useState } from "react";
const EngineContext = createContext(null);
// Ensure a single, undoable filter effect on the block, then set its preset.
function applyLook(engine, blockId, name) {
let eid = engine.block.getEffects(blockId).find((id) => engine.block.getKind(id) === "filter");
if (eid == null) {
engine.beginSilent();
eid = engine.block.createEffect("filter");
engine.block.appendEffect(blockId, eid);
engine.endSilent();
}
engine.block.setString(eid, EFFECT_FILTER_NAME, name); // "" clears the look
}
const LooksPanel = () => {
const engine = useContext(EngineContext);
const blockId = useImageEditorStore((s) => s.editableBlockId);
return (
<button
type="button"
disabled={!engine || blockId == null}
onClick={() => engine && blockId != null && applyLook(engine, blockId, "Sepia")}
>
Apply Sepia
</button>
);
};
function Editor() {
const [engine, setEngine] = useState(null);
return (
<EngineContext.Provider value={engine}>
<ImageEditor
src="/photo.jpg"
config={{ customTools: [{ id: "looks", label: "Looks", icon: LooksIcon, panel: LooksPanel }] }}
onReady={(handle) => setEngine(handle.engine)}
/>
</EngineContext.Provider>
);
}
Because the effect goes through the engine's command system, it participates in undo/redo and is baked into the exported image.
Try it
A Looks button appears in the sidebar — open it and pick a look to watch the image change on the canvas:
Next steps
- Configure the Filter Tool — curate the built-in filter presets.
- Customize the Toolbar — control which tools appear.
- Inject Custom UI — add UI outside the sidebar.
- React to Editor Events — respond when your tool activates.
Verified by
tests/guides/custom-tool.spec.tsxin the@editx/image-editorpackage.