# How the Interactive Roadmap Node Editor Works in developer-roadmap

> Explore the interactive roadmap node editor in kamranahmedse/developer-roadmap. Learn how it uses React and SVG to manage your learning progress with event listeners and API calls.

- Repository: [Kamran Ahmed/developer-roadmap](https://github.com/kamranahmedse/developer-roadmap)
- Tags: internals
- Published: 2026-02-23

---

**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](https://github.com/kamranahmedse/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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/components/EditorRoadmap/EditorRoadmap.tsx) |
| **EditorRoadmapRenderer** | The actual SVG renderer and interaction layer | [`src/components/EditorRoadmap/EditorRoadmapRenderer.tsx`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/components/EditorRoadmap/EditorRoadmapRenderer.tsx) |
| **ReadonlyEditor** | Read-only view used for custom roadmaps in user profiles | [`src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx`](https://github.com/kamranahmedse/developer-roadmap/blob/main/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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/components/EditorRoadmap/EditorRoadmap.tsx), the component first clears any stale migration data and then calls `loadRoadmapData`:

```typescript
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:

```tsx
<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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/jwt.ts):

```typescript
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:

```typescript
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, containing `topicId` and `resourceId`
- `roadmap.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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx). This variant renders the same SVG structure but omits all event listeners and mutation capabilities:

```tsx
<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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/resource-progress.ts) and [`src/lib/jwt.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/jwt.ts):

1. **Authentication Check**: Before any mutation, `isLoggedIn()` verifies the JWT token stored in cookies
2. **Login Gate**: If unauthenticated, `showLoginPopup()` from [`src/lib/popup.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/popup.ts) interrupts the flow
3. **API Update**: `updateResourceProgress` sends the status change to the backend API
4. **UI Synchronization**: `renderTopicProgress` and `refreshProgressCounters` update 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:

```tsx
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`:

```tsx
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:

```tsx
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: `EditorRoadmap` for data fetching, `EditorRoadmapRenderer` for SVG rendering and interactions, and `ReadonlyEditor` for static displays.
- Event delegation powers all interactions; the system uses `getNodeDetails` to traverse the DOM and identify clicked nodes via `data-node-id` and `data-type` attributes.
- Authentication gates all mutations through `isLoggedIn()` and `showLoginPopup()`, while `updateResourceProgress` synchronizes 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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/src/lib/jwt.ts). If the user lacks a valid JWT token, the flow immediately calls `showLoginPopup()` from [`src/lib/popup.ts`](https://github.com/kamranahmedse/developer-roadmap/blob/main/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`](https://github.com/kamranahmedse/developer-roadmap/blob/main/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.