How to Create Custom Widgets Using `imgui_internal.h`: The Complete Guide

The imgui_internal.h header exposes the low-level item lifecycle system and interaction helpers required to build custom Dear ImGui widgets that fully integrate with navigation, focus, and styling.

Dear ImGui maintains a strict separation between its stable public API (imgui.h) and the low-level internal machinery housed in imgui_internal.h. While the public API provides high-level convenience functions, the internal API grants direct access to the item registration system, behavior helpers, and drawing primitives that the library uses to implement every built-in widget. Mastering these internal hooks allows you to construct bespoke controls that behave identically to native ImGui components.

The Internal API Architecture

Creating a custom widget requires orchestrating four core internal systems. Unlike the public API, which handles these steps automatically, the internal API requires you to explicitly manage the widget's identity, boundaries, interaction state, and rendering.

1. Generate a Stable ID

Every widget needs a unique ImGuiID to track state across frames. Use ImGui::GetID(label) or the lower-level ImHashStr to generate this identifier based on string labels or pointer values.

2. Define the Bounding Box

Construct an ImRect that encapsulates the widget's screen position and dimensions. This rectangle drives clipping, hit-testing, and navigation focus calculations.

3. Register with ItemAdd

Call ItemAdd (imgui_internal.h line 3478, implemented in imgui.cpp) to register the widget with Dear ImGui's current window. This function:

  • Stores the item's data in g.LastItemData
  • Applies clipping so the widget hides when outside the window bounds
  • Registers the item for navigation and focus handling
bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL, ImGuiItemFlags extra_flags = 0);

4. Handle Interaction with Behavior Helpers

Use dedicated behavior functions to compute hover, active, and pressed states. ButtonBehavior (declared around line 3766 in imgui_internal.h) processes mouse and keyboard input, updates navigation flags, and manages the g.NavId and NavActivateId states. Alternative helpers include SliderBehavior and ComboBehavior located in imgui_widgets.cpp.

5. Render via Draw Lists

Retrieve the current window's draw list using ImGui::GetWindowDrawList() and issue primitive commands (AddRectFilled, AddText, AddCircleFilled, etc.). Always pull colors from ImGui::GetStyle().Colors to respect the active theme.

Step-by-Step Implementation Guide

Creating IDs and Bounding Boxes

Begin by checking if the window is skipping items (optimization for clipped content), then calculate your widget's rectangle:

ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems) return false;

ImGuiID id = ImGui::GetID("MyCustomWidget");
ImVec2 pos = window->DC.CursorPos;
ImVec2 size = ImGui::CalcItemSize(ImVec2(0,0), 100.0f, ImGui::GetFrameHeight());
ImRect bb(pos, pos + size);

Registering the Item

The ItemAdd call is mandatory for navigation and focus support. Without this, your widget will not respond to tab ordering or gamepad input:

if (!ImGui::ItemAdd(bb, id)) return false;

For widgets that don't render every frame (such as drag-and-drop proxies), call KeepAliveID(id) to prevent the ID from being recycled by Dear ImGui's hash table.

Processing Input States

ButtonBehavior is the workhorse for clickable widgets. It outputs hover and held states while internally handling navigation activation:

bool hovered = false, held = false;
ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
bool clicked = held && ImGui::IsMouseReleased(ImGuiMouseButton_Left);

For value-based controls, SliderBehavior (defined in imgui_widgets.cpp) provides拖拽 logic, clamping, and keyboard manipulation:

float new_value = *value;
bool hovered = false;
ImGui::SliderBehavior(bb, id, &new_value, v_min, v_max, ImGuiSliderFlags_None, &hovered);

Complete Code Examples

Example 1: Custom Toggle Button

This implementation demonstrates the full lifecycle: ID generation, item registration, behavior handling, and custom rendering with shape primitives.

bool CustomToggle(const char* label, bool* v)
{
    // Step 1: ID and window context
    ImGuiID id = ImGui::GetID(label);
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    if (window->SkipItems) return false;

    // Step 2: Bounding box calculation
    ImVec2 pos = window->DC.CursorPos;
    ImVec2 size = ImGui::CalcItemSize(ImVec2(0,0), ImGui::GetFontSize() * 2.0f, ImGui::GetFontSize() * 1.2f);
    ImRect bb(pos, pos + size);

    // Step 3: Register item for navigation/clipping
    ImGui::ItemAdd(bb, id);

    // Step 4: Interaction handling
    bool hovered = false, held = false;
    ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
    
    // Step 5: State mutation
    bool pressed = held && ImGui::IsMouseReleased(ImGuiMouseButton_Left);
    if (pressed) *v = !*v;

    // Step 6: Rendering
    ImU32 col_bg = ImGui::GetColorU32(*v ? ImGuiCol_ButtonActive : 
                                        held ? ImGuiCol_ButtonActive : 
                                        hovered ? ImGuiCol_ButtonHovered : 
                                        ImGuiCol_Button);
    
    ImDrawList* draw_list = ImGui::GetWindowDrawList();
    draw_list->AddRectFilled(bb.Min, bb.Max, col_bg, window->WindowRounding);
    
    // Draw indicator circle
    float pad = bb.GetHeight() * 0.2f;
    ImVec2 circle_pos = *v ? ImVec2(bb.Max.x - pad - (bb.GetHeight() * 0.3f), bb.GetCenter().y) 
                           : ImVec2(bb.Min.x + pad + (bb.GetHeight() * 0.3f), bb.GetCenter().y);
    draw_list->AddCircleFilled(circle_pos, bb.GetHeight() * 0.3f, IM_COL32(255, 255, 255, 255));

    return pressed;
}

Example 2: Custom Slider with Rail Rendering

This example leverages SliderBehavior for complex interaction logic while providing completely custom visuals.

bool CustomSliderFloat(const char* label, float* value, float v_min, float v_max)
{
    ImGuiID id = ImGui::GetID(label);
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    if (window->SkipItems) return false;

    ImVec2 size = ImGui::CalcItemSize(ImVec2(0,0), 200.0f, ImGui::GetFrameHeight());
    ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
    
    if (!ImGui::ItemAdd(bb, id)) return false;

    // Use internal slider behavior for drag/keyboard logic
    bool hovered = false;
    float temp_val = *value;
    ImGui::SliderBehavior(bb, id, ImGuiDataType_Float, &temp_val, &v_min, &v_max, "", 0, &hovered);
    
    if (temp_val != *value) *value = temp_val;

    // Custom rendering: horizontal rail
    ImDrawList* draw_list = ImGui::GetWindowDrawList();
    ImRect rail_bb(bb.Min + ImVec2(0, bb.GetHeight() * 0.4f), 
                   bb.Max - ImVec2(0, bb.GetHeight() * 0.4f));
    draw_list->AddRectFilled(rail_bb.Min, rail_bb.Max, 
                            ImGui::GetColorU32(ImGuiCol_FrameBg), 4.0f);

    // Custom rendering: position indicator
    float t = (*value - v_min) / (v_max - v_min);
    float grab_x = ImLerp(rail_bb.Min.x, rail_bb.Max.x, t);
    ImVec2 grab_center(grab_x, rail_bb.GetCenter().y);
    draw_list->AddCircleFilled(grab_center, bb.GetHeight() * 0.4f, 
                              ImGui::GetColorU32(hovered ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab));

    // Label rendering
    ImGui::RenderTextClipped(bb.Min, bb.Max, label, NULL, NULL, ImVec2(0.5f, 0.5f));

    return hovered;
}

Key Source Files to Reference

Understanding the internal implementation requires studying these specific files in the ocornut/imgui repository:

  • imgui_internal.h – Declares ItemAdd, ButtonBehavior, KeepAliveID, and the navigation flag constants (e.g., ImGuiButtonFlags_NoNavFocus).
  • imgui.cpp – Contains the implementations of ItemAdd and the core item lifecycle logic that manages g.LastItemData and clipping.
  • imgui_widgets.cpp – Houses the concrete behavior implementations including SliderBehavior, ComboBehavior, and the detailed logic for ButtonBehavior.
  • imgui_draw.cpp – Provides the ImDrawList primitives (AddRect, AddText, etc.) used for custom rendering.
  • imgui_demo.cpp – Contains practical demonstrations of advanced rendering techniques and examples of how the internal API constructs complex widgets.

Summary

  • The internal API in imgui_internal.h exposes the building blocks used by Dear ImGui's own widget suite, enabling fully native custom controls.
  • ItemAdd is mandatory for navigation, focus, and clipping support; it registers your widget's bounding box and ID with the current window context.
  • ButtonBehavior and SliderBehavior handle complex input processing, navigation activation, and state management automatically.
  • KeepAliveID prevents ID collision for widgets that skip frames or exist only during specific interaction states.
  • Custom widgets should draw using ImDrawList primitives and reference ImGui::GetStyle().Colors to maintain visual consistency with the user's theme.

Frequently Asked Questions

When should I use imgui_internal.h instead of the public API?

Use the internal API when you need custom geometry or specialized interaction patterns that the standard Button(), Slider(), or InputText() functions cannot accommodate. The internal API is essential for widgets requiring non-rectangular hit areas, composite controls, or unique rendering pipelines. Note that because these symbols are not guaranteed stable across versions, you should pin your project to a specific Dear ImGui version when using internals.

What is the difference between ItemAdd and ButtonBehavior?

ItemAdd registers the widget's existence with Dear ImGui's window and navigation systems, handling clipping and focus allocation. ButtonBehavior processes input events (mouse clicks, keyboard navigation, gamepad) and calculates hover/active states. You must call ItemAdd before ButtonBehavior to establish the widget's identity and bounding box, but ButtonBehavior is only required if your widget needs click or hover detection.

Do custom widgets built with the internal API support gamepad navigation?

Yes. Because ItemAdd populates the internal LastItemData structure and ButtonBehavior respects the NavId system, widgets built using these primitives automatically participate in Dear ImGui's keyboard and gamepad navigation. The behavior functions internally set g.NavActivateId and handle ImGuiButtonFlags_NoNavFocus flags, ensuring your custom widget responds identically to built-in controls when using tab navigation or directional pads.

How do I handle text input inside a custom widget?

For widgets requiring text entry, use InputTextEx (declared in imgui_internal.h and implemented in imgui_widgets.cpp) rather than building raw input handling. This internal function exposes the full text editing state machine, undo/redo buffers, and callback system while allowing you to control the rendering. Alternatively, call ImGui::InputText() publicly and overlay custom graphics using GetWindowDrawList() if you only need visual customization rather than behavioral changes.

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 →