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:

Loading...
Loading image...

Next steps

Verified by tests/guides/custom-tool.spec.tsx in the @editx/image-editor package.