Open Source · MIT · Zero deps

Build interactive
mind maps
on the web

Drop in one JS file. Full CRUD API, SVG rendering, drag & drop, pan, zoom, and lossless PNG/SVG export — no build step required.

Live Demo GitHub npm
npm version license no dependencies bundle size

Live Demo

Click any node to select it. Use the panel below to add children, rename, or delete. Scroll to zoom, drag background to pan.

No node selected
Click any node in the map above to see actions.

Everything you need

A complete mind map solution in a single file. No bundler, no framework, no dependencies.

🖼️

SVG Rendering

Pure SVG — crisp at any resolution, no canvas artifacts. Drop-shadow filters, colour-coded branches, smooth bezier links.

🖱️

Interactive Canvas

Drag individual nodes. Pan by dragging the background. Scroll-wheel zoom centred on the cursor. Pinch-to-zoom on touch.

✏️

Full CRUD API

Add, update, delete nodes programmatically. Insert a node between any two connected nodes without breaking the tree.

📤

Export PNG & SVG

One-line export to high-DPI PNG or vector SVG. The export is pixel-perfect — no dependencies, pure web platform APIs.

🔄

JSON Import / Export

Load any tree as JSON with fromJSON(). Snapshot the current state with toJSON() and save or share it.

📡

Event System

Subscribe to nodeClick, nodeAdd, nodeUpdate, nodeDelete, nodeToggle and linkAdd to build reactive UIs on top.

🏢

Org Charts

layout: 'orgchart' draws managers above their reports, with collapsible branches. Typed nodes (person, team, company, project) get a colour, badge and job title. See the 40-person sample.

🔗

Cross-links

Dashed, labelled links between any two nodes for many-to-many ties: people to projects, advisors to companies. Portfolio sample.

🔀

Reorg Diff

MindkeeperMap.diff(before, after) finds who joined, left, moved or changed title; highlightDiff() marks them on the chart. January vs July sample.

Install

Two ways to add mindkeeper-map.js to your project.

1
CDN (no build step)
<!-- Drop this in your HTML -->
<script src="https://cdn.jsdelivr.net/npm/mindkeeper-map/src/mindkeeper-map.js"></script>

<!-- Create a container -->
<div id="map" style="width:100%;height:500px"></div>

<!-- Initialise -->
<script>
const map = new MindkeeperMap('#map');
map.init({ topic: 'Root', children: [] });
</script>
2
npm
# Install
npm install mindkeeper-map

// CommonJS
const MindkeeperMap = require('mindkeeper-map');

// Or copy the file directly from
// node_modules/mindkeeper-map/src/mindkeeper-map.js

// Initialise
const map = new MindkeeperMap('#map');
map.init(treeData);

API Reference

All methods return this unless noted, so they chain.

new MindkeeperMap(selector, options?)

Create an instance. selector is a CSS selector string or an Element. All options are optional:

{
  layout:     'mindmap', // or 'orgchart' (top-down)
  levelWidth: 240,   // px between depth levels
  levelHeight:110,   // orgchart: px between levels
  hSpacing:   24,    // orgchart: gap between subtrees
  stackLeaves:4,     // orgchart: list 4+ leaf reports in a column
  vSpacing:   54,    // px between siblings
  linkStroke: 3,     // link line width
  nodeBorder: 2.5,   // node border width
  fontFamily: 'system-ui'
}
.init(data) → this

Render the map from a tree object. Ids are optional — any node without one gets a stable auto-generated id.

map.init({
  id: 'root',
  topic: 'My Mind',
  children: [
    { topic: 'Work', children: [
      { topic: 'Q3 launch' }
    ]},
    { topic: 'Personal' }
  ]
});
.addNode(parentId, { topic }) → string

Add a child node to the given parent. Returns the new node's id.

const id = map.addNode('root', {
  topic: 'New branch'
});
.insertBetween(parentId, childId, { topic }) → string

Insert a new node between a parent and one of its children. The existing child becomes a child of the new node.

// Before: Root → Work
const id = map.insertBetween(
  'root', 'work',
  { topic: 'Category' }
);
// After:  Root → Category → Work
.updateNode(id, { topic }) → this

Update a node's label. The map re-renders automatically.

map.updateNode(id, { topic: 'New label' });
.deleteNode(id) → this

Delete a node and all its descendants. Cannot delete the root node.

map.deleteNode(id);
.getNode(id) → object | null

Returns lightweight node info: { id, topic, children: string[] }.

const node = map.getNode('work');
// { id: 'work', topic: 'Work',
//   children: ['q3', 'team'] }
.fromJSON(data) / .toJSON() → object

fromJSON is an alias for init. toJSON returns the current tree as a plain JS object you can serialise.

const json = map.toJSON();
localStorage.setItem('map', JSON.stringify(json));

map.fromJSON(JSON.parse(saved));
.setLayout(name) / .collapse(id) / .expand(id) / .toggle(id)

Switch between 'mindmap' and 'orgchart', and fold branches. collapseAll() / expandAll() do every branch; collapsed nodes are saved as collapsed: true.

map.setLayout('orgchart');
map.collapse('engineering');
.addLink(from, to, type?, label?) / .removeLink(from, to, type?)

Cross-links outside the tree, drawn as dashed curves with a label. getLinks() lists them; they are saved in toJSON().links.

map.addLink('lin', 'apollo', 'leads');
MindkeeperMap.diff(a, b) / .highlightDiff(diff)

Compare two trees by id: { added, removed, moved, changed, links, info }. highlightDiff rings the cards (green added, red removed, amber changed or moved) and adds a legend; clearDiff() removes it.

const diff = MindkeeperMap.diff(jan, jul);
map.init(jul).highlightDiff(diff);
.exportPNG(filename?) / .exportSVG(filename?)

Download the full mindmap as a high-DPI PNG or a clean vector SVG. No dependencies — built on XMLSerializer and Canvas 2D.

map.exportPNG('my-map.png');
map.exportSVG('my-map.svg');
.on(event, handler) / .off(event, handler)

Subscribe and unsubscribe from events. Returns this for chaining.

map
  .on('nodeClick', ({ id, topic }) => {
    console.log('Clicked:', topic);
  })
  .on('nodeAdd', ({ node, parentId }) => {
    save(map.toJSON());
  });

Events

Event Payload Description
nodeClick { id, topic } A node was clicked
nodeAdd { node: { id, topic }, parentId } A node was added via addNode or insertBetween
nodeUpdate { id, data } A node was updated via updateNode
nodeDelete { id } A node was deleted via deleteNode
nodeDeselect {} The background was clicked, clearing selection
nodeToggle { id, collapsed } A branch was collapsed or expanded
linkAdd / linkRemove { link } / { from, to, type } A cross-link was added or removed

JSON Format

The input and output format is a recursive tree. The id field is optional on input — the library generates stable ids for any node that omits it.

{
  "id": "root",
  "topic": "Project Plan",
  "children": [
    {
      "id": "design",
      "topic": "Design",
      "children": [
        { "topic": "Wireframes" },
        { "topic": "Prototype" }
      ]
    },
    {
      "id": "dev",
      "topic": "Development",
      "children": [
        { "topic": "API" },
        { "topic": "Frontend" }
      ]
    }
  ]
}
🏷️

Optional org fields

Any node can carry "kind" (person, company, team, role, project, idea), "meta" (title, email, url, avatar, start, end, status) and "collapsed": true. The root can carry "links": [{ "from", "to", "type" }]. Maps without them render as before.

💡

Works with mindkeeper-mcp

Export your mind map from the mindkeeper-mcp CLI or MCP server (~/.mindkeeper/mindmap.json, or its export_orgchart tool) and pass it directly to fromJSON().

💾

Persist to localStorage

Call toJSON() after any mutation to snapshot the tree and save it wherever you need — localStorage, a database, or a file.

map.on('nodeAdd', () =>
  localStorage.setItem(
    'map',
    JSON.stringify(map.toJSON())
  )
);