From Interactive Diagram
Build beautiful, interactive diagrams (system design, architecture, sequence, flow) as a single self-contained HTML file using only pure HTML, CSS, and vanilla JavaScript — no frameworks, no build step, no npm. Use this skill whenever the user asks to create, draw, generate, or visualize a system diagram, architecture diagram, sequence diagram, flowchart, data-flow diagram, component diagram, or any interactive technical diagram, even if they don't explicitly say "interactive" or "HTML." Also trigger when the user wants to turn a description of a system, services, components, or a process into a visual diagram they can pan, zoom, click, and explore in a browser.
How this skill is triggered — by the user, by Claude, or both
Slash command
/interactive-diagram:interactive-diagramThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Produce a single self-contained `.html` file that renders a beautiful, interactive technical diagram. The user opens it by double-clicking — no install, no build, no server.
Produce a single self-contained .html file that renders a beautiful, interactive technical diagram. The user opens it by double-clicking — no install, no build, no server.
<script> and layer interactivity on top.<style> tag, all JS in a single <script> tag, all markup in one HTML document.Do not hand-write the renderer from a blank file. Start from
assets/skeleton.html — a complete, self-contained
baseline (theme + dark mode, toolbar, SVG defs, pan/zoom/drag, hover, side
panel, SVG/PNG export) whose headline feature is a baked-in layout guard.
The skeleton is a small framework: the AI writes a three-level structure — tab → container → block — and the runtime turns it into HTML/SVG. You never hand-write elements.
| Framework term | What it is | Renders as |
|---|---|---|
| Tab | a whole diagram / view | a DOM tab in the floating bar; each owns its own <svg> + layout-guard instance |
| Container | a rectangle grouping blocks (band / lane / box) | dashed SVG group rectangle with a title pill |
| Block | a node — one rounded rect (or diamond) inside a container | filled SVG rect, clickable, draggable |
Workflow with the skeleton:
assets/skeleton.html to the output path.CONFIG block (app) with the real system. Each entry
in app.tabs is one independent diagram. Inside a tab, give every block a
container id and define the containers array; x/y/w/h are starting
hints, not final coordinates.init() builds the first tab, runs the guard automatically, and
logs the audit result per tab to the console. Click ⚑ Audit overlap
(⚑ 檢查重疊) to re-check the active tab at any time; if it finds overlaps it
offers to auto-fix — autoFix() restores the tidy built layout (snapshotted
in buildTab as tab.home) and redraws, clearing drag-induced overlaps without
drifting the tuned container/title placement.Note on chrome language. The built-in UI (toolbar, legend, side-panel headings, help modal, audit/auto-fix dialogs) ships in Traditional Chinese (zh-TW). Diagram content — tab/container labels, block labels, descriptions — is whatever you author in the
appconfig. To switch the chrome to another language, edit the button text in the toolbar markup,TYPE_LABELS, the help modal, and theapplyTheme/audit strings.
app renders with the tab
strip hidden — it looks and behaves exactly like a plain single diagram.app.tabs.length > 1. Each tab is built lazily on first activation and
cached; each keeps its own pan/zoom, layout guard, and audit. Theme (light /
dark) is shared across all tabs.makeLayout(tab))Each tab gets its own guard instance via makeLayout(tab). It checks distance /
non-overlap across the four element categories a diagram is built from, and
resolves collisions:
| Category | What it is | Resolver |
|---|---|---|
block | block rectangles / diamonds | resolveBlocks() — push-apart loop, ≥40px clearance, axis of least resistance |
label | text riding on arrow (edge) lines | resolveLabels() — vertical nudge so label boxes never collide |
title | container captions | titleBox() places each in clear margin outside its container |
container | rectangles grouping blocks | fitContainers() grows each to contain its members + padding |
Two entry points (on each tab's layout):
layout.run() — call on the data before rendering: pushes blocks apart,
then grows containers around them. (buildTab() already does this.)layout.audit() — walks every conflicting category pair and returns
{ ok, count, conflicts[] }, logging a console.table tagged with the tab id.
Containment is not a conflict: a block inside its container, a title
captioning its container, and bands crossing lanes are all by design (see
POLICY.isConflict). Blocks that spill outside their declared container are
reported separately.layout.resolveLabels() runs on the DOM after render (labels need measured
positions); everything else runs on the in-memory tab data. Geometry primitives
(measureText, penetration, overlaps, contains) are exposed for custom
layouts. Before handing over the file, confirm the console shows
✓ Layout audit [<tab id>]: no overlaps for every tab — if it logs conflicts,
adjust the starting hints or container membership and reload.
Organize the <script> into clearly commented sections, in this order:
// === CONFIG === app data (tabs → containers, blocks, edges) lives here, nothing else
// === RENDER === SVG construction, layout, edge routing
// === INTERACTIONS === hover, click, drag, pan, zoom, panel
// === EXPORT === SVG and PNG download
// === INIT === wire it all up on DOMContentLoaded
The CONFIG block must be the first thing in the script and must be self-contained, so the user can edit only that object to change the diagram. Use this shape:
const app = {
title: "Diagram",
tabs: [
{
id: "system", // unique; used as cache key + svg/png filename
label: "System", // caption shown in the tab strip
type: "system" | "sequence" | "flow",
containers: [ // the grouping rectangles
{ id, label, orient, color, title },
// orient: "band" (horizontal stripe) | "lane" (vertical column) | "box" (free)
// title.side: "above" | "left" | "top"
],
blocks: [ // the nodes
{ id, container, label, type, x, y, w, h, description, tech, responsibilities },
// container = id of the container this block belongs to
// type drives the color (client / http / worker / infra / queue / db / external)
],
edges: [
{ from, to, label, style, bendDir },
// style: "sync" | "async" | "dashed"; bendDir: 1 | -1 to bow parallel edges apart
],
},
// …add more tabs for additional views; the tab strip appears automatically
],
};
For a single diagram, use exactly one entry in tabs — the tab strip stays
hidden and the file behaves like a plain single diagram.
Render with inline SVG. SVG is interactive, scalable, and exportable — do not use Canvas for the diagram itself.
Interactions, all of which must work:
chain() computes this; highlight() applies it.mouseleave restores the pinned one (per-tab state.pinned).+, −, and "Fit to screen" buttons in a floating toolbar.? Help) in the toolbar → opens a modal documenting every interaction. Close via the X, the backdrop, or Esc.type: "sequence"): Prev / Next buttons that reveal messages one at a time along vertical lifelines.
Visuals:Modern aesthetic — clean like Linear, Vercel, or Stripe docs. No clip-art, no skeuomorphism.
Rounded rectangles for nodes. Soft drop shadows via SVG <filter> (gaussian blur + offset). Subtle gradients via <linearGradient>.
Distinct colors per node type, with a small legend pinned to a corner.
Edges with curved or orthogonal routing — never just straight lines for system diagrams. Use SVG <marker> for arrowheads.
Edge labels positioned along the path so they do not overlap the line itself (place on a small white/background-colored rect for legibility).
Smooth CSS transitions for hover, panel slide, and zoom level changes.
Dark mode toggle in the top-right corner. Theme via CSS custom properties (--bg, --fg, --node-fill, --edge, etc.) so toggling flips one attribute on <html>.
System font stack (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif). Generous spacing. No cramped text.
Export buttons in the toolbar:
Download SVG — serialize the current <svg> and trigger a download.
Download PNG — rasterize the SVG via a temporary <canvas> at 2× pixel ratio for crispness, then download.
These constraints must be satisfied before the file is handed to the user. The goal is that someone who opens the file immediately sees a clean, readable diagram — not a puzzle of overlapping boxes. When you start from assets/skeleton.html, the layout guard enforces most of these automatically (each tab's layout.run() + layout.audit()); the rules below explain what it does and what you still set by hand (container membership, starting hints, edge routing).
Every node's bounding box must have at least 40 px of clearance on all sides from every other node's bounding box. After computing initial x, y positions in the config, run a simple collision-push loop in the RENDER section: iterate over all node pairs, compute overlap, and push them apart along the axis of least resistance. Repeat until no pair overlaps (cap at ~30 iterations to avoid infinite loops). This means the x/y values in the config are starting hints, not final positions.
Compute the diagram's actual bounding box (min/max of all node positions + their width/height) after layout finalization, then set the SVG viewBox to that bounding box with 60 px of padding on each side. This guarantees the user sees the whole diagram on first open without panning. The "Fit to screen" button should recalculate this same bounding box.
Node labels must fit inside their node rectangles. Measure the label string length (approximate: label.length * 7 px for the default font, or use a canvas measureText call) and size the node width to be at least label_width + 32 px. Multi-line labels (e.g., long service names) should either wrap or expand the node height — never clip or overflow. Do not use CSS overflow: hidden on SVG text.
system diagrams use curved Bézier paths that bow around congestion; for flow diagrams prefer orthogonal routing that steps around nodes.The floating toolbar, legend, and side panel must not permanently occlude diagram content. Pin the toolbar and legend to corners with a z-index above the SVG but ensure the initial viewBox computation (above) accounts for their footprint — add extra padding on whichever edges they occupy.
When the user drags a node, clamp its position so it cannot be dragged fully off the visible viewport. Nodes dragged to the edge should stop when their bounding box reaches the SVG canvas boundary.
system for architecture with services and data stores, sequence for time-ordered message exchanges between actors, flow for decision/process flows.Walk through this mentally (or scan your generated coordinates) before handing over the file:
If any checkbox would fail, fix it before handing over.
0 0 1200 800. Always derive it from the actual final node positions after layout, so nothing is cropped on first open.d attributes (or x1/y1/x2/y2) on every pointermove during a drag.transform: scale() on the whole SVG for zoom if you want crisp text and consistent stroke widths. Prefer manipulating the viewBox attribute.One .html file. Open it. It works. The user can edit the app config object at the top and reload to change the entire picture.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub lizardliang/interactive-diagram --plugin interactive-diagram