Understanding the ImGui ID Stack System for Resolving Widget ID Collisions

ImGui resolves widget ID collisions by hashing each widget's label against a hierarchical ID stack manipulated via PushID and PopID, allowing identical labels to coexist in different scopes without global uniqueness requirements.

The Dear ImGui library (ocornut/imgui) uses a sophisticated ID stack system to manage widget identifiers dynamically. When multiple widgets share the same visible label, the ID stack prevents collisions by incorporating scope-specific context into the final hash calculation. Understanding this mechanism is essential for building robust user interfaces with dynamically generated or nested components.

How the ImGui ID Stack Works

Every widget in Dear ImGui receives a unique ImGuiID computed by hashing its label against the current ID stack. This stack-based approach eliminates collisions when identical labels appear in different contexts, such as inside loops or nested UI components.

Base ID Generation via GetID

All widget creation eventually calls GetID(label), with the core implementation residing in imgui.cpp at lines 9357-9381. This function hashes the supplied string with the contents of g.CurrentWindow->IDStack, an ImVector<ImGuiID> that stores the current hierarchical context. The hashing is cumulative: each level of the stack contributes to the final ID, ensuring that the same label produces different hashes under different stack configurations.

PushID and PopID Operations

The public API provides three overloads of PushID to inject scope identifiers, declared in imgui.h at lines 605-608:

void PushID(const char* str_id);      // Hash a string identifier
void PushID(const void* ptr_id);      // Hash a pointer address
void PushID(int int_id);              // Hash an integer value

Each variant hashes its input before pushing it onto IDStack, guaranteeing uniform distribution regardless of input type. PopID removes the top entry, restoring the previous scope. These operations are O(1) and introduce negligible overhead.

PushOverrideID for Pre-Computed Identifiers

For advanced scenarios where you already possess a hashed ImGuiID, the internal API offers PushOverrideID, declared in imgui_internal.h at line 3471. This function pushes the raw ID directly without additional hashing, useful when integrating with external resource managers or custom hashing schemes.

Source Code Implementation of the ID Stack System

The mechanics reside in specific locations within the ocornut/imgui repository. In imgui.cpp (lines 9357-9381), the PushID implementations hash their inputs using ImHashStr or bitwise operations before appending to g.CurrentWindow->IDStack. The stack itself is a simple ImVector that grows and shrinks with push/pop operations.

The demo code in imgui_demo.cpp (lines 961, 1278, 1318, and others) illustrates typical usage patterns in loops, tree nodes, and table rows.

Code Examples for Preventing Widget ID Collisions

Disambiguating Widgets in Loops with PushID

When generating widgets inside loops, identical labels cause collision without scope isolation. Push the loop index to create distinct namespaces:

for (int i = 0; i < 5; ++i) {
    ImGui::PushID(i);                 // Each iteration receives unique stack context
    if (ImGui::Button("Delete")) {    // Same visible label, different ID
        // handle deletion of item i
    }
    ImGui::PopID();
}

Each button receives a distinct final ID because the hash incorporates the integer i from the stack.

Using Pointer Values as Unique Identifiers

For collections of objects, pushing pointer addresses guarantees uniqueness even with identical string content:

struct Item { const char* name; };
Item* items[10];
for (int n = 0; n < 10; ++n) {
    ImGui::PushID(items[n]);           // Hash the pointer address
    ImGui::Text("%s", items[n]->name);
    ImGui::PopID();
}

This technique leverages memory addresses as entropy, preventing collisions when name strings repeat across items.

Advanced Usage with PushOverrideID

When working with pre-computed IDs from external systems, avoid double-hashing:

ImGuiID custom_id = ImHashStr("MySpecialGroup", 0, seed);
ImGui::PushOverrideID(custom_id);      // Push raw ID without re-hashing
// ... create widgets ...
ImGui::PopID();

This pattern appears in imgui_internal.h and is essential when mixing ImGui with custom ID allocation schemes.

Combining PushID with the ## Suffix Syntax

ImGui supports inline unique identifiers via the ## suffix hidden from display. Combine both techniques for robust scoping:

ImGui::PushID(42);
ImGui::Button("Save##file");   // ##file adds hash material
ImGui::PopID();

Both the stack and the suffix contribute to the final hash, though PushID generally provides cleaner block-scope management than multiple ## annotations.

Summary

  • Hierarchical scoping: The ID stack creates private namespaces via PushID and PopID, allowing widget labels to repeat safely across different contexts.
  • Flexible input types: PushID accepts strings, pointers, and integers, hashing all inputs uniformly before storage in the IDStack.
  • Zero-cost abstraction: Stack operations use a simple ImVector with O(1) complexity, adding negligible runtime overhead to widget generation.
  • Override capability: PushOverrideID (in imgui_internal.h) supports pre-hashed identifiers for advanced integration scenarios.
  • Collision immunity: Proper use of the ID stack prevents focus conflicts, state corruption, and activation errors in dynamic, recursively generated UIs.

Frequently Asked Questions

What causes widget ID collisions in ImGui?

Widget ID collisions occur when two active widgets generate identical ImGuiID values, typically because they share the same label string without distinct stack contexts. This causes ImGui to treat them as the same interactive element, resulting in shared hover states, simultaneous activation, and incorrect focus behavior.

How does PushID prevent ID collisions in loops?

PushID injects scope-specific entropy (such as a loop index or array pointer) into the ID stack before widget creation. Since GetID hashes the widget label against the entire stack contents, each iteration produces a unique final identifier despite identical labels, effectively isolating each widget's state.

What is the difference between PushID and PushOverrideID?

PushID hashes its input (string, pointer, or integer) before pushing onto the stack, suitable for raw data sources. PushOverrideID, defined in imgui_internal.h at line 3471, accepts a pre-computed ImGuiID and pushes it directly without additional hashing, designed for scenarios where you manage identifier generation externally or need to avoid double-hashing.

When should I use the ## suffix versus PushID?

The ## suffix provides quick inline disambiguation for single widgets, while PushID offers cleaner, block-scoped namespace management for groups of widgets. Use ## for occasional uniqueness tweaks and PushID for systematic isolation in loops, recursive functions, or reusable UI components.

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 →