A mind map template you can tap open and closed

A mind map on paper fills up fast. An interactive one shows the whole idea first and opens one branch at a time.

You have a big topic and a blank page. Ideas come in any order.

So you draw a circle in the middle and start adding lines. Twenty minutes later the page is full, the writing is tiny, and the best idea is squeezed into a corner.

A mind map should help you see the shape of a topic. On paper it often ends up hiding it.

These days people ask ChatGPT, Claude or Gemini for it and get a finished page in one reply. You write a plain outline; the page draws the map, and each branch opens when you tap it.

Here is a mind map template for a school event. Tap a branch to open or close it.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mind map: science fair</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: radial-gradient(circle at 15% 0%, #e4f3ff, transparent 45%), radial-gradient(circle at 100% 40%, #fff0e3, transparent 40%), #fbfaf7; color: #1f2433; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  .page { max-width: 760px; margin: 0 auto; padding: 16px 12px 26px; }
  .top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 4px; }
  h1 { margin: 0; font-size: 19px; }
  .tools { display: flex; gap: 6px; }
  .tools button { border: 0; border-radius: 10px; padding: 8px 10px; background: #1f2433; color: #fff; font: 600 13px system-ui; cursor: pointer; }
  .tools button.alt { background: #e9e6df; color: #1f2433; }
  .hint { font-size: 13px; color: #6a7080; margin: 0 0 10px; }
  .stage { position: relative; transition: height .38s ease; }
  .stage svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
  .node { position: absolute; left: 0; top: 0; padding: 8px 10px; border-radius: 12px; font: 600 13px/1.25 system-ui; text-align: center; cursor: default; user-select: none; will-change: transform, opacity; }
  .node.kids { cursor: pointer; }
  .node.d0 { background: #1f2433; color: #fff; font-size: 16px; font-weight: 800; padding: 14px 12px; border-radius: 18px; box-shadow: 0 10px 24px rgba(31,36,51,.25); cursor: pointer; }
  .node.d1 { color: #fff; font-weight: 700; font-size: 14px; border-radius: 99px; padding: 9px 12px; box-shadow: 0 6px 14px rgba(0,0,0,.12); }
  .node.d2, .node.d3 { background: #fff; border: 2px solid; font-weight: 500; box-shadow: 0 3px 8px rgba(0,0,0,.06); }
  .node.kids:hover { filter: brightness(1.05); }
  .badge { display: inline-block; margin-left: 6px; min-width: 20px; padding: 1px 6px; border-radius: 99px; background: rgba(255,255,255,.3); font-size: 11px; font-weight: 800; }
  .d2 .badge, .d3 .badge { background: #eef0f4; color: #1f2433; }
  .outline { margin-top: 18px; padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,.8); border: 1px solid #ece8df; font-size: 14px; }
  .outline h2 { margin: 0 0 6px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: #6a7080; }
  .outline ul { margin: 2px 0; padding-left: 20px; }
  .outline > ul { padding-left: 4px; list-style: none; }
  .outline li { margin: 2px 0; }
</style>
</head>
<body>
<main class="page">
  <div class="top">
    <h1>Science fair plan</h1>
    <div class="tools"><button id="openAll">Open all</button><button class="alt" id="closeAll">Close all</button></div>
  </div>
  <p class="hint">Tap a branch to open or close it. The map is drawn from the outline below.</p>
  <div class="stage" id="stage"><svg id="lines"></svg></div>

  <section class="outline">
    <h2>The outline</h2>
    <ul id="outline">
      <li>Science fair 2027
        <ul>
          <li>Date and place
            <ul><li>Friday 12 March</li><li>School hall, 1-4 pm</li><li>Set-up from 11 am</li></ul>
          </li>
          <li>Projects
            <ul><li>Grade 4: plants and light</li><li>Grade 5: simple machines</li><li>Grade 6: free choice</li><li>Max 3 students per table</li></ul>
          </li>
          <li>Volunteers
            <ul><li>Judges: 6 parents</li><li>Set-up crew: Mr Okafor's class</li><li>Snack table: PTA</li></ul>
          </li>
          <li>Budget
            <ul><li>Poster boards: $90</li><li>Ribbons and prizes: $60</li><li>Snacks: $40</li></ul>
          </li>
          <li>Getting the word out
            <ul><li>Newsletter, 2 weeks before</li><li>Posters in the corridors</li><li>Invite the other school</li></ul>
          </li>
        </ul>
      </li>
    </ul>
  </section>
</main>
<script>
  const stage = document.getElementById('stage'), svg = document.getElementById('lines');
  const PAL = ['#ff6b4a', '#18a999', '#7357f6', '#e9a400', '#2f80ed', '#e2468b'];
  const open = { 'r.0': true };
  const nodes = {};
  let tree, anim;

  // Read the tree from the visible outline. Editing a line there changes the map.
  function parse(list, path) {
    return [...list.children].filter((li) => li.tagName === 'LI').map((li, i) => {
      const key = path + '.' + i; let label = '';
      li.childNodes.forEach((n) => { if (n.nodeType === 3 || (n.nodeType === 1 && !/^(UL|OL)$/.test(n.tagName))) label += n.textContent; });
      const sub = li.querySelector(':scope > ul, :scope > ol');
      return { key, label: label.replace(/\s+/g, ' ').trim(), kids: sub ? parse(sub, key) : [] };
    }).filter((n) => n.label);
  }
  const vis = (n) => (n.key === 'r' || open[n.key]) ? n.kids : [];

  function ensure(n, d, color) {
    let e = nodes[n.key];
    if (!e) {
      const el = document.createElement('div'); stage.append(el);
      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill', 'none'); svg.append(path);
      e = nodes[n.key] = { el, path, isNew: true };
      el.addEventListener('click', () => toggle(e.n));
    }
    e.n = n; e.leaving = false;
    const el = e.el;
    el.className = 'node d' + Math.min(d, 3) + (n.kids.length ? ' kids' : '');
    el.textContent = n.label;
    if (n.kids.length && d > 0) { const b = document.createElement('span'); b.className = 'badge'; b.textContent = open[n.key] ? '−' : '+' + n.kids.length; el.append(b); }
    if (d === 1) el.style.background = color;
    if (d >= 2) { el.style.borderColor = color; el.style.background = color + '14'; }
    e.path.setAttribute('stroke', color || '#999'); e.path.setAttribute('stroke-width', d === 1 ? 4 : 2.2); e.path.setAttribute('stroke-linecap', 'round');
    e.path.style.opacity = d === 1 ? .55 : .45;
    return e;
  }

  function layout() {
    const W = stage.clientWidth, two = W >= 620;
    const wOf = (d) => two ? [120, 118, 120, 120][Math.min(d, 3)] : [96, 112, 116, 116][Math.min(d, 3)];
    const G = two ? 18 : 14, VG = 8, BG = 14;
    const out = {};
    function measure(n, d, color) { const e = ensure(n, d, color); e.el.style.width = wOf(d) + 'px'; return e.el.offsetHeight; }
    function tall(n, d, color) {
      n._h = measure(n, d, color); const ks = vis(n);
      if (!ks.length) return (n._t = n._h);
      const s = ks.reduce((a, k) => a + tall(k, d + 1, color), 0) + VG * (ks.length - 1);
      return (n._t = Math.max(n._h, s));
    }
    function place(n, d, x, top, side, color, parent) {
      const w = wOf(d), bx = side > 0 ? x : x - w, cy = top + n._t / 2;
      out[n.key] = { x: bx, y: cy - n._h / 2, w, h: n._h, parent, side, d };
      const ks = vis(n); let t = top + (n._t - (ks.reduce((a, k) => a + k._t, 0) + VG * (ks.length - 1))) / 2;
      ks.forEach((k) => { place(k, d + 1, side > 0 ? bx + w + G : bx - G, t, side, color, n.key); t += k._t + VG; });
    }
    const rootH = measure(tree, 0);
    const br = tree.kids;
    br.forEach((b, i) => tall(b, 1, PAL[i % PAL.length]));
    const total = br.reduce((a, b) => a + b._t, 0);
    let right = [], left = [], acc = 0;
    br.forEach((b) => { if (!two || acc < total / 2) { right.push(b); acc += b._t; } else left.push(b); });
    const sideH = (arr) => arr.reduce((a, b) => a + b._t, 0) + BG * Math.max(0, arr.length - 1);
    const H = Math.max(sideH(right), sideH(left), rootH) + 24;
    const rw = wOf(0), rx = two ? (W - rw) / 2 : 0;
    out[tree.key] = { x: rx, y: H / 2 - rootH / 2, w: rw, h: rootH, parent: null, d: 0 };
    const run = (arr, side) => { let t = H / 2 - sideH(arr) / 2; arr.forEach((b) => { place(b, 1, side > 0 ? rx + rw + G : rx - G, t, side, PAL[br.indexOf(b) % PAL.length], tree.key); t += b._t + BG; }); };
    run(right, 1); run(left, -1);
    stage.style.height = H + 'px';
    return out;
  }

  function anc(key, tgt) { let k = key; while (k.includes('.')) { k = k.slice(0, k.lastIndexOf('.')); if (tgt[k]) return tgt[k]; } return null; }

  function render(instant) {
    const tgt = layout();
    for (const [k, e] of Object.entries(nodes)) {
      const t = tgt[k];
      if (t) {
        if (e.isNew) { const p = t.parent && (nodes[t.parent].now || tgt[t.parent]); e.now = p && !instant ? { x: p.x + p.w / 2 - t.w / 2, y: p.y + p.h / 2 - t.h / 2, o: 0, w: t.w, h: t.h } : { x: t.x, y: t.y, o: 0, w: t.w, h: t.h }; e.isNew = false; }
        e.from = { ...e.now }; e.to = { x: t.x, y: t.y, o: 1, w: t.w, h: t.h }; e.t = t;
      } else {
        const p = anc(k, tgt);
        e.leaving = true; e.from = { ...e.now };
        e.to = p ? { x: p.x + p.w / 2 - e.now.w / 2, y: p.y + p.h / 2 - e.now.h / 2, o: 0, w: e.now.w, h: e.now.h } : { ...e.now, o: 0 };
      }
    }
    cancelAnimationFrame(anim);
    const t0 = performance.now(), D = instant ? 1 : 420;
    const ease = (x) => 1 - Math.pow(1 - x, 3);
    (function frame(now) {
      const p = Math.min(1, (now - t0) / D), q = ease(p);
      for (const e of Object.values(nodes)) {
        const f = e.from, to = e.to;
        e.now = { x: f.x + (to.x - f.x) * q, y: f.y + (to.y - f.y) * q, o: f.o + (to.o - f.o) * q, w: to.w, h: to.h };
        e.el.style.transform = `translate(${e.now.x}px, ${e.now.y}px)`; e.el.style.opacity = e.now.o;
      }
      for (const [k, e] of Object.entries(nodes)) {
        const pk = e.t && e.t.parent; const pe = pk && nodes[pk];
        if (!pe) { e.path.setAttribute('d', ''); continue; }
        const a = pe.now, b = e.now, side = e.t.side;
        const x1 = side > 0 ? a.x + a.w : a.x, y1 = a.y + a.h / 2, x2 = side > 0 ? b.x : b.x + b.w, y2 = b.y + b.h / 2, mx = (x1 + x2) / 2;
        e.path.setAttribute('d', `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`); e.path.style.opacity = b.o * (e.t.d === 1 ? .55 : .45);
      }
      if (p < 1) anim = requestAnimationFrame(frame);
      else for (const [k, e] of Object.entries(nodes)) if (e.leaving) { e.el.remove(); e.path.remove(); delete nodes[k]; }
    })(t0);
  }

  function toggle(n) {
    if (!n.kids.length) return;
    if (n.key === 'r') { const any = n.kids.some((k) => open[k.key]); n.kids.forEach((k) => { open[k.key] = !any; }); }
    else open[n.key] = !open[n.key];
    render();
  }
  function setAll(v) { (function walk(n) { n.kids.forEach((k) => { open[k.key] = v; walk(k); }); })(tree); render(); }
  document.getElementById('openAll').onclick = () => setAll(true);
  document.getElementById('closeAll').onclick = () => setAll(false);

  function build(instant) {
    const outline = document.getElementById('outline');
    tree = parse(outline, 'r')[0]; tree.key = 'r';
    const fix = (n, p) => { n.kids.forEach((k, i) => { k.key = p + '.' + i; fix(k, k.key); }); }; fix(tree, 'r');
    render(instant);
  }
  let timer;
  new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => build(), 150); })
    .observe(document.getElementById('outline'), { subtree: true, characterData: true, childList: true });
  let lastW = 0;
  addEventListener('resize', () => { if (stage.clientWidth !== lastW) { lastW = stage.clientWidth; build(true); } });
  build(true); lastW = stage.clientWidth;
</script>
</body>
</html>
A mind map for a made-up school science fair. Coloured branches open and close with a tap, and the map is drawn from the outline at the bottom of the page. All names and details are made up. Edit the code and the example reruns.

Three things that keep a mind map readable

1. Write it as an outline first. Topic, branches, points, each one indent deeper. It takes two minutes in any notes app, and it is the part you will edit later. The map is just a picture of it.

2. Five or so branches, short labels. "Budget" reads at a glance. "Things we still need to pay for before March" does not. Keep the branch names to a word or two and put the detail in the points underneath.

3. Start closed. A map with every point open is the paper problem again. Showing the main branches first gives the shape; people open the one they care about.

On the left, an indented list. On the right, the same list drawn as a mind map with a central topic and coloured branches.
On the left, an indented list. On the right, the same list drawn as a mind map with a central topic and coloured branches.

Make it with AI

Open ChatGPT, Claude or Gemini and paste this. Change the words in capitals and paste your outline at the end.

Make one complete HTML file: an interactive mind map.
Central topic: YOUR TOPIC
The map shows the topic in the middle and main
branches around it, each in its own colour, joined
by curved lines. Tap a branch to open or close its
points, with a short animation. Add "Open all" and
"Close all" buttons. Start with branches closed.
Show my outline as a plain nested list at the
bottom of the page, and draw the map from that
list.
Rules: all CSS and JavaScript inside the file, no
images or fonts from other sites, no libraries,
nothing sent anywhere, works on a phone.
Use only my outline. Do not add branches or points.
Outline: PASTE YOUR INDENTED LIST HERE

"Draw the map from that list" matters most. It keeps every word as plain text on the page, so changing the map later means editing one line of a list instead of digging through code.

A mind map template can do more than show. This one is an essay plan, with one extra line in the prompt: tap a point to tick it off as you write.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mind map: essay plan</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: radial-gradient(circle at 50% 0%, #2b2f5c, transparent 60%), #14152b; color: #eef0ff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  .page { max-width: 760px; margin: 0 auto; padding: 16px 12px 26px; }
  .top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 6px; }
  h1 { margin: 0; font-size: 19px; }
  .prog { flex: none; font-size: 13px; color: #c9ccff; background: rgba(255,255,255,.08); border-radius: 99px; padding: 6px 10px; }
  .prog b { color: #b6f36a; }
  .meter { height: 6px; border-radius: 9px; background: rgba(255,255,255,.1); overflow: hidden; margin-bottom: 8px; }
  .meter i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, #6a7bff, #b6f36a); transition: width .4s; }
  .hint { font-size: 13px; color: #a9acd6; margin: 0 0 10px; }
  .tools { display: flex; gap: 6px; margin-bottom: 8px; }
  .tools button { border: 0; border-radius: 10px; padding: 8px 10px; background: #eef0ff; color: #14152b; font: 600 13px system-ui; cursor: pointer; }
  .tools button.alt { background: rgba(255,255,255,.1); color: #eef0ff; }
  .stage { position: relative; transition: height .38s ease; }
  .stage svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
  .node { position: absolute; left: 0; top: 0; padding: 8px 10px; border-radius: 12px; font: 600 13px/1.25 system-ui; text-align: center; cursor: pointer; user-select: none; will-change: transform, opacity; transition: filter .3s; }
  .node.d0 { background: linear-gradient(135deg, #fefefe, #dfe3ff); color: #14152b; font-size: 15px; font-weight: 800; padding: 14px 12px; border-radius: 18px; box-shadow: 0 0 0 6px rgba(255,255,255,.08), 0 12px 30px rgba(0,0,0,.4); }
  .node.d1 { color: #fff; font-weight: 700; font-size: 14px; border-radius: 99px; padding: 9px 12px; box-shadow: 0 0 18px rgba(0,0,0,.35); }
  .node.d2, .node.d3 { background: #1e2042; border: 2px solid; color: #e6e8ff; font-weight: 500; text-align: left; padding-left: 30px; }
  .node.d2::before, .node.d3::before { content: ""; position: absolute; left: 9px; top: 50%; width: 14px; height: 14px; margin-top: -7px; border-radius: 50%; border: 2px solid currentColor; opacity: .55; }
  .node.done { color: #b6f36a; }
  .node.done::before { background: #b6f36a; border-color: #b6f36a; opacity: 1; box-shadow: inset 0 0 0 3px #1e2042; }
  .node.dim { filter: grayscale(.8) brightness(.55); }
  .badge { display: inline-block; margin-left: 6px; min-width: 20px; padding: 1px 6px; border-radius: 99px; background: rgba(255,255,255,.25); font-size: 11px; font-weight: 800; }
  .outline { margin-top: 18px; padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1); font-size: 14px; color: #d9dbf5; }
  .outline h2 { margin: 0 0 6px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: #a9acd6; }
  .outline ul { margin: 2px 0; padding-left: 20px; }
  .outline > ul { padding-left: 4px; list-style: none; }
  .outline li { margin: 2px 0; }
</style>
</head>
<body>
<main class="page">
  <div class="top">
    <h1>Essay plan: should school start later?</h1>
    <span class="prog"><b id="doneN">0</b> of <span id="allN">0</span> points ready</span>
  </div>
  <div class="meter"><i id="meter"></i></div>
  <p class="hint">Tap a branch to open it and focus on it. Tap a point to tick it off. The map is drawn from the outline below.</p>
  <div class="tools"><button id="openAll">Open all</button><button class="alt" id="closeAll">Close all</button></div>
  <div class="stage" id="stage"><svg id="lines"></svg></div>

  <section class="outline">
    <h2>The outline</h2>
    <ul id="outline">
      <li>Should school start later?
        <ul>
          <li>Opening
            <ul><li>The 7:15 bus story</li><li>My question: would 9:30 work?</li><li>My answer: yes, with limits</li></ul>
          </li>
          <li>Tired mornings
            <ul><li>Ask 20 classmates when they wake up</li><li>First-period yawns: my notes</li></ul>
          </li>
          <li>Better first lessons
            <ul><li>Ask two teachers what they see</li><li>My own maths grades by period</li></ul>
          </li>
          <li>The other side
            <ul><li>Parents who start work early</li><li>Shared buses</li><li>Sports end in the dark</li></ul>
          </li>
          <li>Ending
            <ul><li>Back to the bus story</li><li>Try it first: late Mondays</li></ul>
          </li>
        </ul>
      </li>
    </ul>
  </section>
</main>
<script>
  const stage = document.getElementById('stage'), svg = document.getElementById('lines');
  const PAL = ['#ff6fae', '#4fc3ff', '#9b7bff', '#ffb547', '#3ddc97', '#ff7a59'];
  const done = {};
  let focus = null;
  const open = {};
  const nodes = {};
  let tree, anim, e;

  // Read the tree from the visible outline. Editing a line there changes the map.
  function parse(list, path) {
    return [...list.children].filter((li) => li.tagName === 'LI').map((li, i) => {
      const key = path + '.' + i; let label = '';
      li.childNodes.forEach((n) => { if (n.nodeType === 3 || (n.nodeType === 1 && !/^(UL|OL)$/.test(n.tagName))) label += n.textContent; });
      const sub = li.querySelector(':scope > ul, :scope > ol');
      return { key, label: label.replace(/\s+/g, ' ').trim(), kids: sub ? parse(sub, key) : [] };
    }).filter((n) => n.label);
  }
  const vis = (n) => (n.key === 'r' || open[n.key]) ? n.kids : [];

  function ensure(n, d, color) {
    let e = nodes[n.key];
    if (!e) {
      const el = document.createElement('div'); stage.append(el);
      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill', 'none'); svg.append(path);
      e = nodes[n.key] = { el, path, isNew: true };
      el.addEventListener('click', () => toggle(e.n));
    }
    e.n = n; e.leaving = false;
    const el = e.el;
    el.className = 'node d' + Math.min(d, 3) + (n.kids.length ? ' kids' : '');
    el.textContent = n.label;
    if (n.kids.length && d > 0) { const b = document.createElement('span'); b.className = 'badge'; b.textContent = open[n.key] ? '−' : '+' + n.kids.length; el.append(b); }
    if (d === 1) el.style.background = color;
    if (d >= 2) { el.style.borderColor = color; }
    if (!n.kids.length && done[n.key]) el.classList.add('done');
    const br = n.key.split('.').slice(0, 2).join('.');
    if (focus && d > 0 && br !== focus) el.classList.add('dim');
    e.path.setAttribute('stroke', color || '#999'); e.path.setAttribute('stroke-width', d === 1 ? 4 : 2.2); e.path.setAttribute('stroke-linecap', 'round');
    e.path.style.opacity = d === 1 ? .55 : .45;
    return e;
  }

  function layout() {
    const W = stage.clientWidth, two = W >= 620;
    const wOf = (d) => two ? [120, 118, 120, 120][Math.min(d, 3)] : [96, 112, 116, 116][Math.min(d, 3)];
    const G = two ? 18 : 14, VG = 8, BG = 14;
    const out = {};
    function measure(n, d, color) { const e = ensure(n, d, color); e.el.style.width = wOf(d) + 'px'; return e.el.offsetHeight; }
    function tall(n, d, color) {
      n._h = measure(n, d, color); const ks = vis(n);
      if (!ks.length) return (n._t = n._h);
      const s = ks.reduce((a, k) => a + tall(k, d + 1, color), 0) + VG * (ks.length - 1);
      return (n._t = Math.max(n._h, s));
    }
    function place(n, d, x, top, side, color, parent) {
      const w = wOf(d), bx = side > 0 ? x : x - w, cy = top + n._t / 2;
      out[n.key] = { x: bx, y: cy - n._h / 2, w, h: n._h, parent, side, d };
      const ks = vis(n); let t = top + (n._t - (ks.reduce((a, k) => a + k._t, 0) + VG * (ks.length - 1))) / 2;
      ks.forEach((k) => { place(k, d + 1, side > 0 ? bx + w + G : bx - G, t, side, color, n.key); t += k._t + VG; });
    }
    const rootH = measure(tree, 0);
    const br = tree.kids;
    br.forEach((b, i) => tall(b, 1, PAL[i % PAL.length]));
    const total = br.reduce((a, b) => a + b._t, 0);
    let right = [], left = [], acc = 0;
    br.forEach((b) => { if (!two || acc < total / 2) { right.push(b); acc += b._t; } else left.push(b); });
    const sideH = (arr) => arr.reduce((a, b) => a + b._t, 0) + BG * Math.max(0, arr.length - 1);
    const H = Math.max(sideH(right), sideH(left), rootH) + 24;
    const rw = wOf(0), rx = two ? (W - rw) / 2 : 0;
    out[tree.key] = { x: rx, y: H / 2 - rootH / 2, w: rw, h: rootH, parent: null, d: 0 };
    const run = (arr, side) => { let t = H / 2 - sideH(arr) / 2; arr.forEach((b) => { place(b, 1, side > 0 ? rx + rw + G : rx - G, t, side, PAL[br.indexOf(b) % PAL.length], tree.key); t += b._t + BG; }); };
    run(right, 1); run(left, -1);
    stage.style.height = H + 'px';
    return out;
  }

  function anc(key, tgt) { let k = key; while (k.includes('.')) { k = k.slice(0, k.lastIndexOf('.')); if (tgt[k]) return tgt[k]; } return null; }

  function render(instant) {
    const tgt = layout();
    for (const [k, e] of Object.entries(nodes)) {
      const t = tgt[k];
      if (t) {
        if (e.isNew) { const p = t.parent && (nodes[t.parent].now || tgt[t.parent]); e.now = p && !instant ? { x: p.x + p.w / 2 - t.w / 2, y: p.y + p.h / 2 - t.h / 2, o: 0, w: t.w, h: t.h } : { x: t.x, y: t.y, o: 0, w: t.w, h: t.h }; e.isNew = false; }
        e.from = { ...e.now }; e.to = { x: t.x, y: t.y, o: 1, w: t.w, h: t.h }; e.t = t;
      } else {
        const p = anc(k, tgt);
        e.leaving = true; e.from = { ...e.now };
        e.to = p ? { x: p.x + p.w / 2 - e.now.w / 2, y: p.y + p.h / 2 - e.now.h / 2, o: 0, w: e.now.w, h: e.now.h } : { ...e.now, o: 0 };
      }
    }
    cancelAnimationFrame(anim);
    const t0 = performance.now(), D = instant ? 1 : 420;
    const ease = (x) => 1 - Math.pow(1 - x, 3);
    (function frame(now) {
      const p = Math.min(1, (now - t0) / D), q = ease(p);
      for (const e of Object.values(nodes)) {
        const f = e.from, to = e.to;
        e.now = { x: f.x + (to.x - f.x) * q, y: f.y + (to.y - f.y) * q, o: f.o + (to.o - f.o) * q, w: to.w, h: to.h };
        e.el.style.transform = `translate(${e.now.x}px, ${e.now.y}px)`; e.el.style.opacity = e.now.o;
      }
      for (const [k, e] of Object.entries(nodes)) {
        const pk = e.t && e.t.parent; const pe = pk && nodes[pk];
        if (!pe) { e.path.setAttribute('d', ''); continue; }
        const a = pe.now, b = e.now, side = e.t.side;
        const x1 = side > 0 ? a.x + a.w : a.x, y1 = a.y + a.h / 2, x2 = side > 0 ? b.x : b.x + b.w, y2 = b.y + b.h / 2, mx = (x1 + x2) / 2;
        e.path.setAttribute('d', `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`); e.path.style.opacity = b.o * (e.t.d === 1 ? .55 : .45);
      }
      if (p < 1) anim = requestAnimationFrame(frame);
      else for (const [k, e] of Object.entries(nodes)) if (e.leaving) { e.el.remove(); e.path.remove(); delete nodes[k]; }
    })(t0);
  }

  function toggle(n) {
    if (!n.kids.length) { done[n.key] = !done[n.key]; e = nodes[n.key]; e.el.classList.toggle('done', !!done[n.key]); count(); return; }
    if (n.key === 'r') { const any = n.kids.some((k) => open[k.key]); n.kids.forEach((k) => { open[k.key] = !any; }); focus = null; }
    else { open[n.key] = !open[n.key]; focus = open[n.key] ? n.key : null; if (focus) tree.kids.forEach((k) => { if (k.key !== n.key) open[k.key] = false; }); }
    render(); count();
  }
  function count() {
    const leaves = []; (function walk(n) { if (!n.kids.length) leaves.push(n.key); n.kids.forEach(walk); })(tree);
    const k = leaves.filter((x) => done[x]).length;
    document.getElementById('doneN').textContent = k; document.getElementById('allN').textContent = leaves.length;
    document.getElementById('meter').style.width = (leaves.length ? k / leaves.length * 100 : 0) + '%';
  }
  function setAll(v) { focus = null; (function walk(n) { n.kids.forEach((k) => { open[k.key] = v; walk(k); }); })(tree); render(); }
  document.getElementById('openAll').onclick = () => setAll(true);
  document.getElementById('closeAll').onclick = () => setAll(false);

  function build(instant) {
    const outline = document.getElementById('outline');
    tree = parse(outline, 'r')[0]; tree.key = 'r';
    const fix = (n, p) => { n.kids.forEach((k, i) => { k.key = p + '.' + i; fix(k, k.key); }); }; fix(tree, 'r');
    render(instant); count();
  }
  let timer;
  new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => build(), 150); })
    .observe(document.getElementById('outline'), { subtree: true, characterData: true, childList: true });
  let lastW = 0;
  addEventListener('resize', () => { if (stage.clientWidth !== lastW) { lastW = stage.clientWidth; build(true); } });
  build(true); lastW = stage.clientWidth;
</script>
</body>
</html>
An essay plan as a mind map. Tap a branch to focus on it; tap a point to tick it off, and the counter at the top fills up. The map is drawn from the outline below it. The essay topic and notes are made up.

Getting the map in front of people

You have a mind map. The AI gave it to you as HTML code, though, and a teacher or a team cannot tap branches in a block of code.

The first wall is sharing it. Attach the file and on many phones it opens as code or a download, not a map. To be a page people tap, it has to live somewhere with an address.

In NOS you paste the code into a document and it becomes the map straight away. Choose Create share link and send that link to your teacher, your group or yourself.

AI reply, pasted into NOS, share link, a message. The map opens in a browser without a download.
AI reply, pasted into NOS, share link, a message. The map opens in a browser without a download.

The second wall is a renamed branch. Your teacher reads the plan and says one thing: call "The other side" by its proper name, "Counter-arguments".

If you ask the AI again, you wait for a new reply. It often rearranges the layout or the colours while it is at it, so you check the whole map again. Then you have a new file and a new link to send.

In NOS you click "The other side" in the outline on the page and type "Counter-arguments". The link stays the same, and anyone who opens it sees the renamed branch. A typo in a point works the same way.

In NOS, "The other side" is crossed out in the outline and "Counter-arguments" typed in its place. The same link shows the renamed branch on the map.
In NOS, "The other side" is crossed out in the outline and "Counter-arguments" typed in its place. The same link shows the renamed branch on the map.

Colours, line styles and the opening animation live in the code. Change those in the code view, or ask the AI for just that part.

What it does not do

This mind map template is a view of your outline. You cannot drag branches around on it, and it does not save which points were ticked once the page is closed.

For steps in order rather than ideas around a topic, see HTML flowchart. For a page of notes to revise from, see Study notes website.

Questions people ask

Can AI make a mind map?

Yes. Ask ChatGPT, Claude or Gemini for one complete HTML file and give it your outline. Ask for a central topic, coloured branches, and branches that open and close when tapped.

Why build the map from an outline?

An indented list is the fastest way to write ideas down, and it is easy to edit. If the page draws the map from that list, changing a line changes the map.

How many branches should a mind map have?

Few enough to read at a glance. Four to six main branches with two to four points each is a comfortable size. More than that, and it is often two maps.

Do people need an account to open it?

No. A NOS share link opens in an ordinary browser. People can tap the branches but cannot edit the map.

Can I rename a branch after sharing?

Yes. Click the branch name in the outline on the page in NOS and type. The same link shows the renamed branch.

Keep reading