by Jaderson-bit
A Claude Code skill that turns Markdown outlines into interactive, offline mind maps with markmap.js — white-on-dark, searchable (match + ancestors), with a zoom / expand / export toolbar.
# Add to your Claude Code skills
git clone https://github.com/Jaderson-bit/mindmap-markmap-viewerGuides for using ai agents skills like mindmap-markmap-viewer.
mindmap-markmap-viewer is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by Jaderson-bit. A Claude Code skill that turns Markdown outlines into interactive, offline mind maps with markmap.js — white-on-dark, searchable (match + ancestors), with a zoom / expand / export toolbar. It has 61 GitHub stars.
mindmap-markmap-viewer's catalog security scan is still queued. You can run an instant dependency and prompt-injection check now with the "Scan for vulnerabilities" button above.
Clone the repository with "git clone https://github.com/Jaderson-bit/mindmap-markmap-viewer" and add it to your Claude Code skills directory (see the Installation section above). mindmap-markmap-viewer ships a SKILL.md manifest, so compatible agents can discover and load it automatically.
mindmap-markmap-viewer is primarily written in Python. It is open-source under Jaderson-bit on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh mindmap-markmap-viewer against similar tools.
No comments yet. Be the first to share your thoughts!
Unlocks once the catalog security scan passes (runs nightly).
The deep catalog scan for this skill is still queued. Run an instant dependency check now instead.
Render hierarchical Markdown as an interactive SVG mind map with markmap.js, plus three custom layers: white-font CSS, expand-by-level control, and search that filters the tree.
source.md ──► filter / set expand level (Python) ──► build_html() ──► vendored markmap libs (local, offline) ──► SVG + toolbar
(outline) (manipulate the text) (HTML + CSS) (transform + render in the browser) (screen)
Helper functions live in scripts/render_markmap.py; a minimal source file is in assets/example.md; regression tests are in evals/. Deeper docs: references/internals.md (how the helpers work) and references/lessons.md (real-world lessons + the adversarial counter-review behind the current code).
.html that works offline.markmap derives hierarchy from headings (#) and nested list items — -, *, +, or numbered (1. / 1)) — indented by 2 spaces per level. (Tabs work too; the filter treats one tab as one level.) A YAML frontmatter block controls behavior:
---
markmap:
colorFreezeLevel: 2 # freeze color from level 2 down (branches keep the parent color)
initialExpandLevel: 2 # levels kept open; root is level 1, so 2 = root + branches (-1 = all)
maxWidth: 380 # max node width in px (forces wrapping)
---
# Root <- root (level 1)
## Branch A <- level 2
### Sub-branch A1 <- level 3
- Leaf <- level 4 (bullet under a level-3 heading)
- Detail <- level 5 (bullet indented +2 spaces)
Presets (apply_presets). You don't have to hand-write the markmap: block — apply_presets(src, color=None, max_width=380) fills sensible defaults without overriding anything you set: colorFreezeLevel: 2, maxWidth, and an initialExpandLevel sized by node count (expand-all for ≤30 nodes, else level 2). Pass color=["#7fd1ff", "#ffd479"] for a custom palette. Any key already in your frontmatter wins, so you can set one value and let presets fill the rest.
Content rule — term → parent / description → child. Put the label on the node and its explanation as a child, not on one line. Prefer:
- Term
- description of the term
over - Term — description. This keeps nodes short and the tree scannable.
Node text may contain <, >, and & freely (a < b, List<String>, even </div>). build_html HTML-escapes the source before embedding it and the browser decodes it back, so markmap sees exactly what you wrote. (Earlier versions broke on a literal <; that footgun is gone — don't pre-escape to < yourself or it shows up literally.)
The renderer is: the vendored markmap <script>s (local files), an <svg class="markmap"> plus a hidden source <div> holding the Markdown, a small init script (transform → Markmap.create → toolbar), and the CSS. See build_html() / render_markmap() in scripts/render_markmap.py, with the rationale in references/internals.md.
Non-obvious points (these cost rework):
build_html inlines the vendored markmap stack (d3 + markmap-view/-lib/-toolbar, pinned exact in assets/vendor/) directly into the HTML — no CDN, no network, and no sibling files — so the one .html opens offline anywhere you move or share it. (That single-file default is the fix for maps that failed to load their libs when opened alone.) Pass inline=False for a smaller HTML that instead references a vendor/ folder via vendor="vendor", which must then travel beside it.window.katex, which isn't vendored, so $...$ / $$...$$ show as plain text. Everything else renders fully offline.toolbar=False to omit it. Expand/collapse set each node's fold then re-render with setData() and no argument — passing data re-derives fold from initialExpandLevel and would wipe the manual fold. Export snapshots the current fold state; the SVG inlines the white-font CSS + a dark backdrop so it stands alone, and PNG rasterizes at 2× (falling back to SVG if a browser refuses to rasterize the <foreignObject> labels — markmap draws node text as HTML-in-SVG, not <text>)..html opens on the browser's white default, and a Streamlit components.html iframe is white by default too — neither inherits the host's dark theme. So build_html paints its own dark backdrop (background="#0e1117" by default). Only pass background="transparent" when you know the host behind the iframe is already dark and you want a seamless blend. White font + dark background travel together — never set one without the other.!important. markmap draws text as SVG <text> and sometimes as <foreignObject> (HTML inside SVG). Style both or half the labels stay dark:
svg.markmap text { fill: #ffffff !important; }
svg.markmap foreignObject, svg.markmap foreignObject * { color: #ffffff !important; }
components.html in Streamlit, or a standalone .html file). Host CSS won't leak in and the map's CSS won't leak out, so the <style> goes inline in the HTML string.textContent, which the browser decodes — so escaping round-trips losslessly. Without it, a </div> or List<String> in the outline closes the div early and silently truncates the map.Don't rebuild the map — just rewrite initialExpandLevel in the frontmatter before rendering. Use set_expand_level(src, level) from the helper module:
level 1/2/3 expands that many levels.level = -1 expands everything (the "expand all" button).initialExpandLevel, else add one under the markmap: key (block or inline form), else prepend a minimal frontmatter — but only when none exists. It never rewrites the word "initialExpandLevel" sitting in your body text, and never stacks a second --- block on top of existing frontmatter (markmap reads only the first block, so stacking would silently drop your other settings).When there is a query, keep only nodes that match + their path to the root (ancestors) + their subtree (descendants), so a hit appears in context instead of floating alone. Algorithm in filter_markmap():
#. List-item level = (last heading level) + 1 + indentation, where indentation counts 2-space or tab units (expandtabs), and the marker may be -/*/+/numbered.#-rank numerically exceeds a bullet's level. (Heading rank and bullet indentation share one number line, so comparing raw depths mis-nests an H4-after-a-bullet; the parent tree is what keeps ancestry correct.)_norm).initialExpandLevel: -1._norm strips accents via Unicode NFD so "compliance", "COMPLIANCE", and accented variants all match. Zero matches yields a frontmatter-only (blank) map by design — callers branch on the returned count.
The helpers live in this skill's scripts/ directory — and the working directory is
normally the user's project, not this folder, so a bare "scripts" on sys.path
won't resolve. Build paths from the skill's base directory (the path announced when
this skill loaded):
import sys
from pathlib import Path
SKILL_DIR = Path(r"<this skill's base directory>") # announced when the skill loaded
sys.path.insert(0, str(SKILL_DIR / "scripts"))
from render_markmap import write_mindmap, apply_presets, set_expand_level, filter_markmap
Single self-contained file (recommended) — writes the .md source of truth and a
fully inlined .html that opens offline anywhere, with no sibling vendor/:
src = Path("outline.md").read_text(encoding="utf-8") # the user's outline (sample: SKILL_DIR / "assets/example.md")
src = apply_presets(src) # fill default markmap options (no override)
# optional: src, n = filter_markmap(src, "branch b") # search + keep context
# optional: src = set_expand_level(src, -1) # expand all
write_mindmap(src, "out/mapa.html") # -> out/mapa.md + a self-contained out/mapa.html
# smaller HTML + a sibling vendor/ folder instead: write_mindmap(src, "out/mapa.html", inline=False)
Just the HTML string (e.g. to embed) — build_html(src) returns one self-contained
document with the libraries inlined:
from render_markmap import build_html
open("mindmap.html", "w", encoding="utf-8").write(build_html(src, height=850))
Inside Streamlit:
sys.path.insert(0, str(SKILL_DIR / "scripts")) # SKILL_DIR as in the setup above
from render_markmap import render_markmap, set_expand_level
render_markmap(set_expand_level(src, level), height=850)
The shape of a mind map carries meaning, so structure the content deliberately:
apply_presets set the frontmatter (color, wrap width, expand level) so you don't hand-tune each map (§1).apply_presets applied, or the frontmatter set deliberately..html with the network off (and from a different folder) — it still renders (self-contained).build_html paints its own dark backdrop by default; only go transparent over a host you know is dark. And style both text and foreignObject * with !important, or half the labels stay dark.build_html HTML-escapes the source, so </>/& in node text are safe and round-trip to markmap unchanged — don't pre-escape them yourself.--- block.initialExpandLevel: -1 = expand all.A Claude Code skill that turns hierarchical Markdown into an interactive SVG mind map with markmap.js — readable white-on-dark by default, with expand-by-level control and a search that filters the tree to matches plus their ancestors and descendants (so a hit always shows up in context, not floating alone). Works standalone or embedded in Streamlit.

▶ Live demo — an actual generated map, in your browser: pan, zoom, fold branches, export SVG/PNG. The page is one self-contained HTML file, exactly what the skill produces.
No install needed — paste this into Claude (Claude Code or the app):
Using this skill https://github.com/Jaderson-bit/mindmap-markmap-viewer, create a mindmap of Product Management.
Claude clones the skill on the fly, follows its authoring rules (balanced branches, short labels), and hands back an interactive .html that opens offline plus the editable .md source of truth. Swap "Product Management" for anything you want mapped.

markmap renders a Markdown outline as a zoomable mind map, but three things need a layer on top to be genuinely usable:
build_html / write_mindmap) — the markmap stack (vendored locally and pinned exact) is inlined into the page, so the one .html opens offline anywhere with no CDN, no network, and no sibling files to keep alongside it.set_expand_level), including expand-all (-1).filter_markmap).<, >, & in node text (List<String>, a < b) round-trip correctly instead of breaking the page.Ask Claude Code to install it from this repo:
Install this skill from https://github.com/Jaderson-bit/mindmap-markmap-viewer
Or, as a developer, clone it into your skills directory:
git clone https://github.com/Jaderson-bit/mindmap-markmap-viewer.git ~/.claude/skills/mindmap-markmap-viewer
Or copy a folder you already have:
cp -r mindmap-markmap-viewer ~/.claude/skills/
import sys
from pathlib import Path
SKILL = Path.home() / ".claude" / "skills" / "mindmap-markmap-viewer" # wherever you cloned it
sys.path.insert(0, str(SKILL / "scripts"))
from render_markmap import build_html, set_expand_level, filter_markmap
src = (SKILL / "assets" / "example.md").read_text(encoding="utf-8") # or your own outline
# optional: src, n = filter_markmap(src, "branch b") # search + keep context
# optional: src = set_expand_level(src, -1) # expand all
open("mindmap.html", "w", encoding="utf-8").write(build_html(src, height=850))
Inside Streamlit:
sys.path.insert(0, str(SKILL / "scripts")) # SKILL as above
from render_markmap import render_markmap, set_expand_level
render_markmap(set_expand_level(src, 2), height=850)
See SKILL.md for the source format and authoring rules.
mindmap-markmap-viewer/
├── SKILL.md # operational guide (loads when the skill triggers)
├── scripts/render_markmap.py # build_html / set_expand_level / filter_markmap
├── assets/
│ ├── example.md # minimal sample outline
│ └── vendor/ # pinned markmap + d3 libs, loaded locally (offline)
├── evals/ # dependency-free regression suite + eval prompts
└── references/
├── internals.md # how the helpers work and why
└── lessons.md # real-world lessons + adversarial counter-review
python evals/test_render_markmap.py
A dependency-free suite; every check is labeled with the bug it locks down.
The renderer and text transforms were hardened against a multi-agent adversarial review (12 confirmed findings, each verified by a second agent that tried to refute it). The findings — and the process — are documented in references/lessons.md.