How Dear ImGui's ID Stack System Works: A Complete Technical Guide

TLDR: Dear ImGui generates unique widget identifiers by hashing a stack of contextual values stored in ImGuiContext, allowing multiple identical labels to coexist without state collisions.

Dear ImGui's immediate mode architecture requires a robust mechanism to track widget state—such as focus, hover, and activation—across frames. The ID stack system solves this by computing 32-bit hashes on-the-fly from a hierarchy of values rather than storing explicit identifiers. Understanding how PushID and PopID manipulate this stack is essential for debugging collisions and managing complex layouts in the ocornut/imgui codebase.

Why Unique IDs Matter

Every interactive widget requires a unique identifier to persist state between frames. Rather than expecting users to manually generate UUIDs, the library computes these IDs deterministically by hashing the current contents of an internal stack maintained within ImGuiContext. This approach allows sibling widgets to share identical labels—such as multiple "Delete" buttons in a list—without conflicting state.

The ID Stack Architecture

The ID stack lives inside the ImGuiContext structure and serves as a hierarchy of scope identifiers. As described in the ID STACK section of imgui.cpp, the system automatically pushes context when entering new scopes and hashes all stack elements to produce the final widget ID.

Automatic Scope Management

Container functions like Begin() and TreeNode() implicitly call PushID() when entering a scope and PopID() when exiting. This automatic management ensures that windows, tree nodes, and other containers create distinct namespaces for their children without manual intervention. The stack is cleared incrementally, with each PopID() removing the most recently pushed value.

Pushing Values Onto the Stack

Users can manually push identifiers using three distinct overloads declared in imgui.h and implemented in imgui.cpp around lines 9355‑9379. Each overload adds a specific data type to the stack, which participates in the final hash calculation.

String-Based Identifiers

Use ImGui::PushID(const char* str_id) or the ranged variant PushID(const char* str_begin, const char* str_end) to push a string segment onto the stack. This is the most common approach for distinguishing between sibling elements.

Pointer-Based Identifiers

For objects with stable memory addresses, ImGui::PushID(const void* ptr_id) pushes a pointer value directly. This is ideal when iterating over actual object instances where the pointer serves as a unique key.

Integer-Based Identifiers

When iterating loops or handling indexed data, ImGui::PushID(int int_id) pushes an integer value. This overload provides a convenient way to disambiguate array elements without string conversions.

Label Syntax and Implicit Hashing

The final ID computation incorporates the widget's label string, which supports special syntax for advanced use cases:

  • ## hidden suffix: Text following ## participates in the hash but is not rendered, allowing identical visible labels with distinct IDs.
  • ### rename operator: Text before ### is excluded from the hash, while text after becomes the ID base. This allows dynamic visible text—like changing counters—while maintaining stable identifiers.

According to the FAQ documentation in docs/FAQ.md, these operators provide fine-grained control over what contributes to the hash without affecting the user interface.

How the Final ID Is Computed

Internally, Dear ImGui calls GetID() (see ImGui::GetID in the ID STACK section of imgui.cpp) to generate the final 32-bit hash. This function iterates over every element currently on the stack—strings, pointers, and integers—mixing each into a cumulative hash using the same algorithm as string hashing. The resulting value is stored in the widget's ID field and used for all state look-ups in ImGuiContext.

Debugging ID Collisions

When widgets exhibit unexpected focus behavior or assertion failures indicate duplicate IDs, use the ID Stack Tool available in imgui_demo.cpp. Call ImGui::ShowIDStackToolWindow() or navigate to Demo > Tools > ID Stack Tool in the demo window. This tool displays every level of the current stack with intermediate hash values, making it trivial to identify where collisions occur.

Code Examples

The following examples demonstrate the three PushID overloads and label syntax tricks as implemented in the ocornut/imgui source:

// Example 1: Integer-based PushID in a loop
ImGui::Begin("MyWindow");
for (int i = 0; i < 3; ++i) {
    ImGui::PushID(i);                // Stack: ["MyWindow", i]
    ImGui::Button("Click");          // ID = hash("MyWindow", i, "Click")
    ImGui::PopID();
}
ImGui::End();

// Example 2: Pointer-based PushID for object stability
MyObject* obj = GetSelectedObject();
ImGui::PushID(obj);                  // Stack: ["MyWindow", obj]
ImGui::Checkbox("Enabled", &obj->enabled);
ImGui::PopID();

// Example 3: Hidden suffix with ##
ImGui::Button("Play##song42");       // Stack: ["MyWindow"]; ID = hash("MyWindow", "Play##song42")

// Example 4: Dynamic label with constant ID using ###
float fps = ImGui::GetIO().Framerate;
char buf[64];
sprintf(buf, "FPS: %.1f###FPSLabel", fps);
ImGui::Text(buf);                    // Visible text changes, ID stays hash("MyWindow", "FPSLabel")

Summary

  • The ID stack lives in ImGuiContext and is manipulated via PushID() and PopID() defined in imgui.cpp (lines 9355‑9379) and declared in imgui.h.
  • Container functions automatically manage stack scope, pushing at entry and popping at exit.
  • Three overloads accept strings, pointers, and integers to disambiguate sibling widgets.
  • The final ID is a 32-bit hash computed by GetID() that combines all stack elements with the widget label using the same algorithm as string hashing.
  • Label syntax using ## and ### allows hidden hash components or stable IDs with changing visible text.
  • Use ShowIDStackToolWindow() from imgui_demo.cpp to visualize the current stack and debug collisions.

Frequently Asked Questions

What causes ID collisions in Dear ImGui?

ID collisions occur when two interactive widgets generate identical 32-bit hashes, causing Dear ImGui to treat them as the same widget. This happens when siblings share the same label without distinct stack context, or when manual PushID calls are mismatched with PopID. The assertion error will typically reference the conflicting label and can be diagnosed using the ID Stack Tool.

When should I use PushID with a pointer versus an integer?

Use PushID(const void* ptr_id) when iterating over stable object addresses where the pointer itself serves as a unique entity identifier—common when rendering lists of C++ objects. Use PushID(int int_id) for indexed loops or array positions where the integer index is the distinguishing factor, as it avoids pointer indirection and is slightly more efficient according to the implementations in imgui.cpp.

How do I debug duplicate ID errors in ImGui?

Navigate to Demo > Tools > ID Stack Tool in the demo interface or programmatically call ImGui::ShowIDStackToolWindow() as implemented in imgui_demo.cpp. This window displays every level of the current ID stack with the resulting hash values at each scope. By inspecting the stack while the duplicate ID warning is active, you can identify which parent scope is missing a PushID or where two widgets inadvertently share the same hash components.

What is the difference between ## and ### in ImGui labels?

The ## operator marks a hidden suffix—text after ## is not rendered but participates in the ID hash, allowing buttons with identical visible text to have unique IDs. The ### operator excludes everything before it from the hash and uses only the text after as the identifier base, enabling you to change the visible label dynamically while keeping the underlying ID stable for state persistence, as documented in the Dear ImGui FAQ.

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 →