How the Interactive Roadmap Node Editor Works in developer-roadmap
The interactive roadmap node editor in kamranahmedse/developer-roadmap is a React wrapper around the @roadmapsh/editor SVG renderer that fetches roadmap JSON, registers DOM event listeners for clicks and right-clicks, decodes node attributes to toggle learning/skipped/done states via backend API calls, and emits custom events for external UI components.
The developer-roadmap repository powers the popular roadmap.sh website, providing interactive visual guides for developer career paths. At the heart of this experience lies the interactive roadmap node editor, which transforms static SVG diagrams into dynamic learning trackers. This component architecture enables users to click nodes, mark progress, and explore resources while maintaining a clean separation between data loading, rendering, and interaction logic.
Architecture Overview
The interactive roadmap node editor is composed of three main pieces that work together to provide a seamless editing experience:
| Component | Role | Key Source File |
|---|---|---|
| EditorRoadmap | Wrapper that loads roadmap JSON and injects the editor UI | src/components/EditorRoadmap/EditorRoadmap.tsx |
| EditorRoadmapRenderer | The actual SVG renderer and interaction layer | src/components/EditorRoadmap/EditorRoadmapRenderer.tsx |
| ReadonlyEditor | Read-only view used for custom roadmaps in user profiles | src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx |
Data Loading and Layout with EditorRoadmap
The EditorRoadmap component serves as the entry point for the interactive roadmap node editor. It handles data fetching, layout configuration, and chat widget integration.
In src/components/EditorRoadmap/EditorRoadmap.tsx, the component first clears any stale migration data and then calls loadRoadmapData:
const loadRoadmapData = async () => {
setIsLoading(true);
const { r: switchRoadmapId } = getUrlParams();
const { response, error } = await httpGet<Omit<RoadmapRendererProps,'resourceId'>>(
`${import.meta.env.PUBLIC_API_URL}/v1-official-roadmap/${switchRoadmapId || resourceId}`
);
if (!error) {
setRoadmapData(response);
setHasSwitchedRoadmap(!!switchRoadmapId);
}
setIsLoading(false);
};
The component supports roadmap switching via the ?r= URL parameter, allowing users to view alternative versions of the same roadmap. It also maintains responsive aspect ratios using CSS custom properties:
<div style={!hasSwitchedRoadmap ? {'--aspect-ratio': aspectRatio} : undefined}
className="mt-5 flex aspect-[var(--aspect-ratio)] w-full flex-col justify-center">
<EditorRoadmapRenderer {...roadmapData} dimensions={dimensions} resourceId={resourceId}/>
{hasChat && <RoadmapFloatingChat roadmapId={resourceId}/>}
</div>
SVG Rendering and Interaction Handling
The EditorRoadmapRenderer component in src/components/EditorRoadmap/EditorRoadmapRenderer.tsx is where the interactive roadmap node editor handles all user interactions. It wraps the Renderer component from @roadmapsh/editor and attaches event listeners to the SVG container.
Node Identification and Event Delegation
Rather than attaching individual listeners to thousands of SVG elements, the component uses event delegation. The handleSvgClick callback uses getNodeDetails to traverse up the DOM tree and extract node metadata:
const handleSvgClick = useCallback((e: MouseEvent) => {
const target = e.target as SVGElement;
const { nodeId, nodeType, targetGroup, title } = getNodeDetails(target) ?? {};
if (!nodeId || !nodeType || !allowedNodeTypes.includes(nodeType)) return;
// Handle different node types...
}, []);
The allowedNodeTypes whitelist ensures that clicks on decorative elements or unsupported nodes are ignored.
Handling Different Interaction Types
The interactive roadmap node editor supports multiple interaction patterns based on the clicked element type and modifier keys:
External Links: For button, link-item, or resourceButton nodes, the editor opens the associated URL:
if (['button','link-item','resourceButton'].includes(nodeType)) {
const link = targetGroup?.dataset?.link ?? '';
const isExternal = link.startsWith('http');
isExternal ? window.open(link,'_blank') : (window.location.href = link);
return;
}
Progress Toggling: Checkboxes and checklist items toggle between done and pending states. This requires authentication via isLoggedIn() from src/lib/jwt.ts:
if (nodeType === 'todo-checkbox' || (nodeType === 'checklist-item' && target.tagName === 'rect')) {
e.preventDefault();
if (!isLoggedIn()) { showLoginPopup(); return; }
const newStatus = targetGroup?.classList.contains('done') ? 'pending' : 'done';
updateTopicStatus(nodeId, newStatus);
return;
}
Modifier Key Shortcuts: Shift-click toggles "learning" status, while Alt-click toggles "skipped" status, providing power-user workflows without opening modals.
Right-Click Shortcuts: The handleSvgRightClick handler provides a context-menu alternative for quickly marking items as done or pending:
const handleSvgRightClick = useCallback((e: MouseEvent) => {
e.preventDefault();
const { nodeId, nodeType, targetGroup } = getNodeDetails(e.target as SVGElement) ?? {};
if (!nodeId || !nodeType || !allowedNodeTypes.includes(nodeType) || nodeType === 'button') return;
if (!isLoggedIn()) { showLoginPopup(); return; }
const newStatus = targetGroup?.classList.contains('done') ? 'pending' : 'done';
updateTopicStatus(nodeId, newStatus);
}, []);
Custom Events for External Integration
The interactive roadmap node editor dispatches custom DOM events that external components can listen for:
roadmap.node.click: Emitted when a regular topic node is clicked, containingtopicIdandresourceIdroadmap.checklist.click: Emitted when checklist item text is clicked, used for analytics
These events allow the editor to remain decoupled from modal implementations or analytics services.
Read-Only Mode for Custom Roadmaps
For user-generated content displayed in public profiles, the repository uses ReadonlyEditor via src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx. This variant renders the same SVG structure but omits all event listeners and mutation capabilities:
<ReadonlyEditor
roadmap={{ nodes, edges }}
className="min-h-[1000px]"
onRendered={(wrapperRef) => {
done?.forEach(id => topicSelectorAll(id, wrapperRef?.current!).forEach(el => el.classList.add('done')));
learning?.forEach(id => topicSelectorAll(id, wrapperRef?.current!).forEach(el => el.classList.add('learning')));
skipped?.forEach(id => topicSelectorAll(id, wrapperRef?.current!).forEach(el => el.classList.add('skipped')));
}}
fontFamily="Balsamiq Sans"
fontURL="/fonts/balsamiq.woff2"
/>
The onRendered callback still applies progress styling by adding CSS classes (done, learning, skipped) to the SVG elements, ensuring visual consistency with the interactive version.
Progress Updates and Authentication Flow
Status mutations in the interactive roadmap node editor follow a strict authentication-gated flow implemented in src/lib/resource-progress.ts and src/lib/jwt.ts:
- Authentication Check: Before any mutation,
isLoggedIn()verifies the JWT token stored in cookies - Login Gate: If unauthenticated,
showLoginPopup()fromsrc/lib/popup.tsinterrupts the flow - API Update:
updateResourceProgresssends the status change to the backend API - UI Synchronization:
renderTopicProgressandrefreshProgressCountersupdate the SVG classes and global counters without requiring a full re-render
This architecture ensures that the interactive roadmap node editor remains responsive while maintaining data consistency across sessions.
Integration Examples
Using the Editor in a Page
To embed the interactive roadmap node editor in a React page, import the EditorRoadmap wrapper and provide the resource ID and dimensions:
import { EditorRoadmap } from '@/components/EditorRoadmap/EditorRoadmap';
function RoadmapPage() {
const roadmapId = 'frontend';
const dimensions = { width: 1200, height: 800 };
return (
<section className="my-8">
<EditorRoadmap
resourceId={roadmapId}
dimensions={dimensions}
hasChat={true}
/>
</section>
);
}
Listening for Node-Click Events
External components can react to user interactions by listening for the custom events dispatched by EditorRoadmapRenderer:
useEffect(() => {
const onNodeClick = (e: Event) => {
const { topicId, resourceId } = (e as CustomEvent).detail;
openTopicModal(topicId, resourceId);
};
window.addEventListener('roadmap.node.click', onNodeClick);
return () => window.removeEventListener('roadmap.node.click', onNodeClick);
}, []);
Manually Toggling Progress
For sidebar integrations or bulk operations, use the resource progress utilities directly:
import { updateResourceProgress, renderTopicProgress } from '@/lib/resource-progress';
async function markAsDone(topicId: string) {
await updateResourceProgress(
{ resourceId: 'frontend', resourceType: 'roadmap', topicId },
'done'
);
renderTopicProgress(topicId, 'done');
}
Summary
- The interactive roadmap node editor consists of three core components:
EditorRoadmapfor data fetching,EditorRoadmapRendererfor SVG rendering and interactions, andReadonlyEditorfor static displays. - Event delegation powers all interactions; the system uses
getNodeDetailsto traverse the DOM and identify clicked nodes viadata-node-idanddata-typeattributes. - Authentication gates all mutations through
isLoggedIn()andshowLoginPopup(), whileupdateResourceProgresssynchronizes state with the backend. - Custom events (
roadmap.node.click,roadmap.checklist.click) decouple the editor from modal and analytics implementations. - The editor supports power-user shortcuts: Shift-click toggles "learning" status, Alt-click toggles "skipped" status, and right-click provides quick done/pending toggles.
Frequently Asked Questions
How does the interactive roadmap node editor identify which node was clicked?
The editor uses a helper function called getNodeDetails that traverses up the DOM tree from the click target to find the nearest <g> element containing data-node-id, data-type, and data-title attributes. These attributes are embedded by the @roadmapsh/editor package during SVG generation. The allowedNodeTypes whitelist ensures only valid interactive elements trigger state changes.
What happens when a user clicks a node while not logged in?
Before executing any mutation, the code checks isLoggedIn() from src/lib/jwt.ts. If the user lacks a valid JWT token, the flow immediately calls showLoginPopup() from src/lib/popup.ts, which triggers the authentication modal. The actual state toggle only proceeds after successful authentication and backend confirmation via updateResourceProgress.
Can I use the interactive roadmap node editor for custom roadmaps in user profiles?
Yes, but with limitations. Custom roadmaps displayed in user profiles use the ReadonlyEditor component located in src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx. This variant renders the same SVG structure and applies progress styling through the onRendered callback, but it omits all click and right-click event listeners, preventing user interactions while maintaining visual consistency with the interactive version.
How do external components know when a user clicks a specific topic node?
The EditorRoadmapRenderer dispatches a custom DOM event named roadmap.node.click whenever a valid topic node is clicked. The event detail object contains topicId (formatted as ${slugify(title)}@${nodeId}) and resourceId. External components can listen for this event on the window object to trigger modals, analytics tracking, or navigation without tightly coupling to the editor's internal implementation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →