# draw.io Custom Shapes with JavaScript Logic — Architecture Analysis
## Executive Summary
**Can you build custom shapes with JS logic and import them as a library into draw.io?**
**YES** — via the **Plugin system**. draw.io supports fully programmable shapes written in JavaScript that can be loaded as plugins in both the desktop (Electron) app and self-hosted/web instances. These plugins can define shapes with conditional rendering, computed geometry, dynamic behavior, and expose them in the sidebar palette for drag-and-drop use.
The standard "library import" (File > Open Library) only supports declarative XML — but plugins give you **more** power, not less.
---
## Table of Contents
1. [Shape Systems Overview](#shape-systems-overview)
2. [XML Stencils (Declarative)](#xml-stencils-declarative)
3. [JavaScript Shape Classes (Programmatic)](#javascript-shape-classes-programmatic)
4. [The Plugin Architecture](#the-plugin-architecture)
5. [Loading Plugins in Desktop App](#loading-plugins-in-desktop-app)
6. [Loading Plugins in Web/Self-Hosted](#loading-plugins-in-webself-hosted)
7. [Shape Registration Flow](#shape-registration-flow)
8. [Canvas API Reference](#canvas-api-reference)
9. [Limitations and Considerations](#limitations-and-considerations)
10. [Comparison Matrix](#comparison-matrix)
---
## Shape Systems Overview
draw.io has **two completely separate shape definition systems**:
| System | Format | JS Logic? | Import Method |
|--------|--------|-----------|---------------|
| XML Stencils | `.xml` stencil files | ❌ No (declarative only) | `mxStencilRegistry` / Open Library |
| JS Shape Classes | `.js` files | ✅ Full programmatic | Plugin system / `mxCellRenderer.registerShape()` |
When a cell is rendered, the resolution order is:
```
Cell style "shape=NAME" →
1. mxCellRenderer.defaultShapes[NAME] → JS Shape class
2. mxStencilRegistry.stencils[NAME] → XML Stencil instance
3. Dynamic loading (if enabled) → Load from library registry
```
---
## XML Stencils (Declarative)
Located in: `src/main/webapp/stencils/*.xml`
XML stencils define shapes using a drawing vocabulary:
```xml
```
### Capabilities
- Drawing primitives: `move`, `line`, `quad`, `curve`, `arc`, `rect`, `roundrect`, `ellipse`, `close`
- Styling: `strokecolor`, `fillcolor`, `fontcolor`, `alpha`, `strokewidth`, `dashed`
- State: `save` / `restore`
- Sub-shapes: ``
### Limitations
- **No loops, conditionals, or computed geometry**
- **No JavaScript execution** (except limited `allowEval` for text/image values)
- Purely static vector drawing instructions
---
## JavaScript Shape Classes (Programmatic)
Located in: `src/main/webapp/shapes/*.js` (~47 files)
Examples: `mxBootstrap.js`, `mxKubernetes.js`, `mxElectrical.js`, `mxNetworks.js`, `mxRack.js`
JS shapes extend `mxShape` and have **full programmatic power**:
```javascript
function MyShape(bounds, fill, stroke, strokewidth) {
mxShape.call(this);
this.bounds = bounds;
this.fill = fill;
this.stroke = stroke;
this.strokewidth = (strokewidth != null) ? strokewidth : 1;
}
mxUtils.extend(MyShape, mxShape);
MyShape.prototype.paintVertexShape = function(c, x, y, w, h) {
c.translate(x, y);
// Full JS: conditionals, loops, math, style reading
var mode = mxUtils.getValue(this.style, 'mode', 'default');
var segments = parseInt(mxUtils.getValue(this.style, 'segments', '4'));
if (mode === 'alert') {
c.setFillColor('#ff4444');
}
// Computed geometry
for (var i = 0; i < segments; i++) {
var segH = h / segments;
c.rect(0, i * segH, w, segH - 2);
c.fillAndStroke();
}
};
// Register globally
mxCellRenderer.registerShape('mxgraph.myLib.myShape', MyShape);
```
### Capabilities
- Conditional rendering based on style properties
- Loops and computed geometry
- Math and dynamic calculations
- Reading per-instance style parameters via `mxUtils.getValue(this.style, ...)`
- Full canvas API access
- Custom connection points
- Override `getConstraints()` for dynamic connection point generation
---
## The Plugin Architecture
### Plugin Structure
Every draw.io plugin follows this pattern:
```javascript
// Shape definitions (execute immediately at load time)
function CustomShape() { /* ... */ }
mxUtils.extend(CustomShape, mxShape);
CustomShape.prototype.paintVertexShape = function(c, x, y, w, h) { /* ... */ };
mxCellRenderer.registerShape('mxgraph.custom.shapeName', CustomShape);
// UI integration (executes when draw.io is ready)
Draw.loadPlugin(function(ui) {
// ui = EditorUi instance — full access to the application
// Add sidebar palette
var fns = [
ui.sidebar.createVertexTemplateEntry(
'shape=mxgraph.custom.shapeName;fillColor=#dae8fc;',
120, 80, '', 'My Custom Shape', null, null, 'search keywords'
)
];
ui.sidebar.addPaletteFunctions('customPalette', 'My Custom Library', true, fns);
// Can also: add menus, actions, event listeners, modify graph behavior
});
```
### What `Draw.loadPlugin(callback)` Provides
The callback receives `ui` (EditorUi instance) with access to:
| Object | Purpose |
|--------|---------|
| `ui.editor.graph` | The mxGraph instance (model, view, styles) |
| `ui.sidebar` | Sidebar/palette management |
| `ui.menus` | Menu system |
| `ui.actions` | Action registry |
| `ui.editor` | Editor instance |
| Global: `mxShape`, `mxCellRenderer`, `mxUtils`, `mxConstants` | All mxGraph APIs |
### Production Example: `rackF5.js`
The `plugins/rackF5.js` file (1674 lines) is a real-world plugin that:
- Defines ~20+ shape classes with full JS rendering
- Uses conditionals based on style (`hasEars`, `isFront`, `isDC`, `psNum`)
- Registers all shapes with `mxCellRenderer.registerShape()`
- Adds a complete sidebar palette via `Draw.loadPlugin()`
---
## Loading Plugins in Desktop App
The draw.io Electron app (desktop) has **full plugin support** via **Extras > Plugins**:
### Method 1: Built-in Plugins
A dropdown of pre-packaged plugins (`App.publicPlugin`):
- `ex` (explore), `tips`, `svgdata`, `number`, `sql`, `props`, `text`, `anim`, `update`, `trees`, `replay`, `anon`, `webcola`, `tags`
### Method 2: External Plugins (Custom `.js` Files)
1. **Enable external plugins** (one-time):
- The app checks `requestSync('isPluginsEnabled')`
- If disabled, it shows "pluginsDisabled" message
- Enable via app preferences or configuration
2. **Install your plugin**:
- Extras > Plugins → "Select File..."
- Native file dialog opens, filtered for `.js` files
- Plugin is copied to App Data folder (`installPlugin` action)
- Registered in `mxSettings`
3. **Restart draw.io** — plugin loads on next startup
### Loading sequence (from source):
```
App startup
→ mxSettings.getPlugins() returns saved plugin list
→ App.initPluginCallback() creates Draw object + queue
→ For each plugin:
├─ Built-in (starts with ./plugins/) → load relative path
└─ External → requestSync('getPluginFile') → load from file:// App Data
→ Scripts loaded via