Building Node Editors and Graph Visualizations with Dear ImGui

Dear ImGui's immediate-mode architecture allows you to construct custom node editors by combining low-level drawing primitives like ImDrawList::AddBezierCurve() with high-level widget helpers and the built-in drag-and-drop API, all without external dependencies.

Dear ImGui is a bloat-free graphical user interface library for C++ that outputs optimized vertex buffers for rendering by your application. Because the ocornut/imgui repository follows an immediate-mode paradigm where UI is rebuilt every frame, you retain complete control over layout and interaction, making it ideal for building node editors and graph visualizations that require custom positioning and complex connections between elements.

Core Architectural Concepts

Understanding a few foundational mechanisms in Dear ImGui reveals why it is particularly well-suited for graph visualization interfaces.

Immediate-Mode Rendering Paradigm

Unlike retained-mode GUI systems, Dear ImGui requires you to describe the entire interface every frame. According to the source code in imgui.h and imgui.cpp, this design guarantees that the UI always reflects the current state of your data model. For node editors, this means you can freely position nodes anywhere on screen by calculating world coordinates each frame without fighting a layout engine.

Canvas Rendering with ImDrawList

The ImDrawList API, defined in imgui_draw.cpp, exposes low-level drawing commands that execute on the GPU. Key functions include ImDrawList::AddLine() and ImDrawList::AddRectFilled(), which allow you to render connection wires, node backgrounds, and custom shapes. You obtain the active draw list via ImGui::GetWindowDrawList().

Widget Grouping and Positioning

To treat a collection of buttons and text as a single movable unit, Dear ImGui provides grouping primitives. The functions ImGui::BeginGroup() and ImGui::EndGroup(), located in imgui_widgets.cpp, encapsulate widgets so that interaction tests apply to the whole block. Combined with ImGui::SetCursorScreenPos(), you can place a node group at arbitrary screen coordinates.

ID Stack Management

ImGui uses an internal ID system to track stateful interactions like hover and selection. As implemented in imgui_internal.h, functions such as ImGui::GetID(), ImGui::PushID(), and ImGui::PopID() generate unique identifiers for each node. This allows the library to store per-node data—such as whether a node is active—across frames without requiring you to manage persistent widget objects.

Drag-and-Drop API

The core implementation in imgui.cpp provides ImGui::BeginDragDropSource(), ImGui::SetDragDropPayload(), and ImGui::AcceptDragDropPayload(). These enable wiring connections between node pins with mouse drags, using a payload system that carries integer identifiers between source and target.

Step-by-Step Implementation Workflow

Constructing a functional node editor requires coordinating several ImGui systems within your main loop.

Initialize the Canvas

Begin by creating a window and obtaining its draw list. All subsequent rendering—both node backgrounds and connection lines—targets this ImDrawList instance:

ImGui::Begin("Node Editor");
ImDrawList* draw = ImGui::GetWindowDrawList();

Render Node Containers

Iterate over your node data structure and position each node using screen coordinates. Use ImGui::SetCursorScreenPos() followed by BeginGroup() to establish the node’s origin, then draw background shapes and content widgets:

ImGui::SetCursorScreenPos(node.pos);
ImGui::BeginGroup();
draw->AddRectFilled(node.pos, node.pos + node.size, bg_color);
ImGui::Text("Node Content");
ImGui::EndGroup();

Handle Mouse Interactions

Detect dragging using ImGui::IsItemActive() and ImGui::IsMouseDragging(). When active, modify the node’s position by ImGui::GetIO().MouseDelta to implement movement. Hover states are queried via ImGui::IsItemHovered().

Draw Connections

After rendering all nodes to ensure correct z-ordering, iterate your link list. Calculate pin positions relative to each node’s screen rectangle and submit Bézier curves to the draw list:

draw->AddBezierCurve(p1, p1 + ImVec2(40,0), p2 - ImVec2(40,0), p2,
                     ImGui::GetColorU32(ImGuiCol_PlotLines), 3.0f);

Because the draw list batches primitives, these lines render efficiently alongside UI widgets.

Complete Minimal Example

The following self-contained example demonstrates a two-node editor with draggable nodes and connectable pins. It assumes an OpenGL 3 backend is initialized, though the ImGui-specific code works with any backend (Vulkan, DirectX, etc.):

struct Node {
    int   id;
    ImVec2 pos;          // top-left corner in screen space
    ImVec2 size;         // node size
    const char* name;
    bool  selected = false;
};

static std::vector<Node> nodes = {
    {0, ImVec2(100, 100), ImVec2(150, 80), "Node A"},
    {1, ImVec2(400, 200), ImVec2(150, 80), "Node B"},
};

static std::vector<std::pair<int,int>> links; // (srcNodeId, dstNodeId)

void ShowNodeEditor()
{
    ImGui::Begin("Node Editor");
    ImDrawList* draw = ImGui::GetWindowDrawList();

    // ----- 1. Draw connections (behind nodes)
    for (auto& link : links) {
        const Node& src = nodes[link.first];
        const Node& dst = nodes[link.second];
        ImVec2 p1 = src.pos + ImVec2(src.size.x, src.size.y * 0.5f);
        ImVec2 p2 = dst.pos + ImVec2(0, dst.size.y * 0.5f);
        draw->AddBezierCurve(p1, p1 + ImVec2(40,0), p2 - ImVec2(40,0), p2,
                             ImGui::GetColorU32(ImGuiCol_PlotLines), 3.0f);
    }

    // ----- 2. Draw nodes
    for (Node& node : nodes) {
        ImGui::PushID(node.id);
        ImGui::SetCursorScreenPos(node.pos);
        ImGui::BeginGroup();                     // treat the whole node as a group
        ImGui::Button(node.name, node.size);     // simple background + title
        ImGui::Text("Content…");                 // any widgets you like
        ImGui::EndGroup();

        // ----- 3. Drag-move handling
        if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
            node.pos.x += ImGui::GetIO().MouseDelta.x;
            node.pos.y += ImGui::GetIO().MouseDelta.y;
        }

        // ----- 4. Start a drag-source from the right side (output pin)
        ImGui::SetCursorScreenPos(node.pos + ImVec2(node.size.x, node.size.y*0.5f));
        ImGui::InvisibleButton("output", ImVec2(8,8));
        if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
            ImGui::SetDragDropPayload("NODE_LINK", &node.id, sizeof(node.id));
            ImGui::Text("Link");
            ImGui::EndDragDropSource();
        }

        // ----- 5. Accept a drag-drop on the left side (input pin)
        ImGui::SetCursorScreenPos(node.pos + ImVec2(0, node.size.y*0.5f));
        ImGui::InvisibleButton("input", ImVec2(8,8));
        if (ImGui::BeginDragDropTarget()) {
            if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("NODE_LINK")) {
                int srcId = *(int*)payload->Data;
                links.emplace_back(srcId, node.id);
            }
            ImGui::EndDragDropTarget();
        }
        ImGui::PopID();
    }
    ImGui::End();
}

This implementation relies solely on core Dear ImGui functions from imgui.h and imgui_widgets.cpp, requiring no third-party node editor extensions.

Key Repository Files

When customizing your graph visualization, reference these specific files in the ocornut/imgui repository:

Summary

  • Immediate-mode architecture in Dear ImGui lets you define node positions and connections dynamically every frame without persistent widget hierarchies.
  • ImDrawList functions such as AddBezierCurve() and AddRectFilled() provide GPU-accelerated rendering for custom shapes and connection lines.
  • Widget grouping (BeginGroup/EndGroup) and the ID stack (PushID/PopID) enable treating complex nodes as single interactive units.
  • Drag-and-drop payloads facilitate creating graph connections by carrying node identifiers between output and input pins.
  • All functionality resides in the header-only core (imgui*.h/imgui*.cpp) and works across all supported graphics backends.

Frequently Asked Questions

Does Dear ImGui provide a built-in node editor widget?

No. Dear ImGui does not ship with a high-level node editor widget. Instead, the library provides the low-level primitives—ImDrawList, grouping APIs, and drag-and-drop—in imgui.h and imgui_widgets.cpp that allow you to construct custom node editors tailored to your specific data model and visualization requirements.

How does the immediate-mode API affect node editor state management?

Because Dear ImGui rebuilds the UI every frame as implemented in imgui.cpp, you must store node positions and connection graphs in your own data structures (e.g., std::vector<Node>). The library uses the ID stack system from imgui_internal.h to track transient interaction states like selection and hover, but persistent data remains your responsibility.

Can I use these techniques with any graphics API?

Yes. The node editor code uses only Dear ImGui core functions that output to ImDrawList. The vertex buffers generated are backend-agnostic. Whether you use the OpenGL 3 backend at backends/imgui_impl_opengl3.cpp, the Vulkan backend, or DirectX, the node rendering and interaction logic remains identical; only the final upload and draw call implementation differs.

What ensures good performance when rendering many nodes and connections?

Draw list batching. As implemented in imgui_draw.cpp, all AddLine(), AddBezierCurve(), and widget draw commands append to a single ImDrawList that is ultimately rendered in a few GPU draw calls. This keeps CPU overhead low even with complex graph visualizations containing hundreds of nodes and connection lines.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →