How to Add Custom Widgets in Dear ImGui: A Complete Guide to the Core API

Custom widgets in Dear ImGui are constructed using three essential primitives—GetID() for unique identification, ItemAdd() for layout registration, and ButtonBehavior() for input handling—which integrate seamlessly with the library's navigation and rendering pipeline.

Dear ImGui (ocornut/imgui) exposes its internal widget construction API, allowing developers to create custom interactive elements that behave identically to built-in controls. When you add custom widgets in Dear ImGui using these low-level functions, you inherit automatic support for hover detection, focus management, clipping, and keyboard navigation without additional boilerplate.

Core Architecture for Custom Widgets

Every widget in Dear ImGui, whether built-in or custom, follows the same architectural pattern involving identity, registration, interaction, and rendering.

ID Generation is the foundation. Every widget must own a unique ImGuiID, created via GetID() or GetIDWithSeed(). If your widget contains overlapping interactive regions that temporarily share identifiers, wrap those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true) to prevent conflicts. The ID system is defined in imgui.h.

Item Registration occurs through ItemAdd() and ItemSize(). Call ItemSize() first to reserve space in the layout, then ItemAdd(bb, id)—where bb is an ImRect bounding box—to register the widget with the window's draw list and navigation system. This step, implemented in imgui.cpp, makes the widget visible to ImGui's internal state tracking.

Interaction Handling relies on ButtonBehavior(), the low-level engine for hover, held, and pressed states. Located in imgui_widgets.cpp, this function accepts an ImRect, an ImGuiID, and output pointers for hover and held states. Most built-in widgets are thin wrappers around this primitive.

State Management requires KeepAliveID() if you call ButtonBehavior() without a preceding ItemAdd. This ensures the ID remains active for the current frame even when bypassing standard registration, as noted in the KeepAliveID implementation in imgui.cpp.

Navigation Integration happens automatically when using ItemAdd(), but advanced scenarios may require manual key ownership via SetKeyOwner() from imgui_internal.h to handle keyboard shortcuts properly.

Rendering uses the window's draw list (window->DrawList) to push visual geometry. After handling interaction, draw your widget using functions like AddRectFilled() or RenderFrame(), applying style colors based on the hover and active states captured earlier.

The Standard Implementation Pattern

To add custom widgets in Dear ImGui that function as first-class citizens, follow this six-step workflow:

  1. Create a stable ID using ImGuiID id = GetID("MyWidget##unique");

  2. Define the interaction rectangle with ImRect bb = ImRect(pos, pos + size);

  3. Register the item by calling ItemSize(bb) followed by if (!ItemAdd(bb, id)) return false;

  4. Process input via ButtonBehavior(bb, id, &hovered, &held, flags);

  5. Update state based on interaction flags, such as checking hovered && IsMouseClicked(0) for clicks

  6. Render the visual representation using window->DrawList primitives, applying style variations for hover and active states

This sequence guarantees your widget participates in ImGui's focus, navigation, clipping, and automatic state tracking systems.

Common Pitfalls and Solutions

When implementing custom widgets, several specific mistakes can break functionality:

  • Duplicate ID Conflicts: Overlapping rectangles reusing the same ID cause navigation errors. Fix this by using distinct ID seeds or temporarily enabling ImGuiItemFlags_AllowDuplicateId.

  • Missing ItemAdd: Without calling ItemAdd(), your widget bypasses navigation and clipping. If you deliberately skip ItemAdd() for a raw input widget, you must call KeepAliveID(id) after ButtonBehavior().

  • Incorrect Button Flags: Using the wrong ImGuiButtonFlags (such as forgetting ImGuiButtonFlags_NoNavFocus) produces unwanted navigation behavior. Review the flags defined in imgui.h.

  • Invisible Widgets: Widgets that process input but never push vertices to the draw list appear invisible. Always render something after interaction handling.

Complete Code Examples

Binary Toggle Switch

This example demonstrates a complete custom toggle using the core API:

bool MyToggle(const char* label, bool* v, const ImVec2& size = ImVec2(0,0))
{
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    if (window->SkipItems) return false;

    // 1) ID & rectangle
    ImGuiID id = window->GetID(label);
    ImVec2 label_size = ImGui::CalcTextSize(label);
    ImVec2 toggle_size = ImGui::CalcItemSize(size, ImGui::CalcItemWidth(), label_size.y);
    ImRect bb = ImRect(window->DC.CursorPos, window->DC.CursorPos + toggle_size);
    ImGui::ItemSize(bb);
    if (!ImGui::ItemAdd(bb, id)) return false;

    // 2) Interaction
    bool hovered, held;
    ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    if (ImGui::IsItemClicked()) *v = !*v;

    // 3) Rendering
    ImU32 col_bg = *v ? ImGui::GetColorU32(ImGuiCol_Button) : ImGui::GetColorU32(ImGuiCol_ButtonHovered);
    ImU32 col_knob = ImGui::GetColorU32(ImGuiCol_Text);
    window->DrawList->AddRectFilled(bb.Min, bb.Max, col_bg, 4.0f);
    ImVec2 knob_pos = *v ? ImVec2(bb.Max.x - toggle_size.y * 0.5f, bb.Min.y + toggle_size.y * 0.5f)
                         : ImVec2(bb.Min.x + toggle_size.y * 0.5f, bb.Min.y + toggle_size.y * 0.5f);
    window->DrawList->AddCircleFilled(knob_pos, toggle_size.y * 0.4f, col_knob);
    ImGui::RenderTextClipped(bb.Min, bb.Max, label, NULL, &label_size, ImVec2(0.5f,0.5f), NULL);
    return true;
}

Key implementation details: Uses GetID for identity, ItemAdd for registration, ButtonBehavior for state detection, and draw list primitives for the visual track and knob.

Custom Horizontal Slider

This draggable slider implements value manipulation via mouse delta:

bool MyHorizontalSlider(const char* label, float* value, float v_min, float v_max, const ImVec2& size = ImVec2(0,0))
{
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    if (window->SkipItems) return false;

    ImGuiID id = window->GetID(label);
    ImVec2 slider_size = ImGui::CalcItemSize(size, ImGui::CalcItemWidth(), ImGui::GetFrameHeight());
    ImRect bb(window->DC.CursorPos, window->DC.CursorPos + slider_size);
    ImGui::ItemSize(bb);
    if (!ImGui::ItemAdd(bb, id)) return false;

    // Interaction – we only need ButtonBehavior; no separate ItemAdd is required.
    bool hovered, held;
    ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    if (held)
    {
        float mouse_delta = ImGui::GetIO().MouseDelta.x;
        float proportion = mouse_delta / (bb.Max.x - bb.Min.x);
        *value = ImClamp(*value + proportion * (v_max - v_min), v_min, v_max);
    }

    // Rendering
    float t = ImSaturate((*value - v_min) / (v_max - v_min));
    ImU32 col_bg = ImGui::GetColorU32(ImGuiCol_FrameBg);
    ImU32 col_fg = ImGui::GetColorU32(ImGuiCol_SliderGrabActive);
    window->DrawList->AddRectFilled(bb.Min, bb.Max, col_bg, ImGui::GetStyle().FrameRounding);
    ImVec2 grab_bb_min = ImLerp(bb.Min, bb.Max, ImVec2(t, 0.0f));
    ImVec2 grab_bb_max = ImLerp(bb.Min, bb.Max, ImVec2(t, 1.0f));
    window->DrawList->AddRectFilled(grab_bb_min, grab_bb_max, col_fg, ImGui::GetStyle().FrameRounding);
    ImGui::RenderTextClipped(bb.Min, bb.Max, label, NULL, NULL, ImVec2(0.5f,0.5f), NULL);
    return held;
}

Key implementation details: Calculates value changes from MouseDelta while held, uses ImLerp for proportional positioning, and renders the grab bar using the same bounding box processed by ButtonBehavior.

Composite Color Picker Button

This widget combines custom rendering with ImGui's built-in popup functionality:

bool MyColorButton(const char* label, ImVec4* col, ImGuiColorEditFlags flags = 0)
{
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    if (window->SkipItems) return false;

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

    // Interaction – open ImGui's native color picker on click
    bool clicked = ImGui::ButtonBehavior(bb, id, NULL, NULL, ImGuiButtonFlags_None);
    if (clicked && ImGui::BeginPopupContextItem(label))
    {
        ImGui::ColorPicker4("##picker", (float*)col, flags);
        ImGui::EndPopup();
    }

    // Rendering – simple filled rectangle with the supplied color
    ImU32 col_u32 = ImGui::ColorConvertFloat4ToU32(*col);
    window->DrawList->AddRectFilled(bb.Min, bb.Max, col_u32, ImGui::GetStyle().FrameRounding);
    // optional border
    window->DrawList->AddRect(bb.Min, bb.Max, ImGui::GetColorU32(ImGuiCol_Border));
    return clicked;
}

Key implementation details: Uses ButtonBehavior to detect clicks for opening a context popup, demonstrating how custom widgets can leverage existing Dear ImGui editors while maintaining custom visual presentation.

Essential Source Files for Reference

Understanding the following files in the ocornut/imgui repository is crucial for advanced widget development:

  • imgui.h: Contains public API declarations including ID helpers, PushItemFlag, and ImGuiButtonFlags definitions.

  • imgui.cpp: Houses core item registration functions (ItemAdd, ItemSize, KeepAliveID) and rendering utilities like RenderFrame.

  • imgui_widgets.cpp: Provides the ButtonBehavior implementation and serves as the definitive reference for how built-in widgets combine interaction and rendering.

  • imgui_internal.h: Exposes advanced utilities such as SetKeyOwner for keyboard handling and internal flags for specialized behaviors.

  • imgui_demo.cpp: Contains extensive example code demonstrating various custom widget patterns and advanced usage scenarios.

  • backends/: Directory containing backend implementations that illustrate how draw lists are flushed to the GPU, useful when implementing custom rendering beyond standard draw list primitives.

Summary

To successfully add custom widgets in Dear ImGui:

  • Always generate a unique ID using GetID() to prevent state collisions

  • Register with ItemAdd() to enable navigation, clipping, and focus management

  • Use ButtonBehavior() as the universal interaction primitive for hover and click detection

  • Call KeepAliveID() when bypassing ItemAdd for raw input-only widgets

  • Render via window->DrawList using the same bounding box processed by interaction code

  • Reference imgui_widgets.cpp for authoritative patterns on combining behavior and presentation

Frequently Asked Questions

What is the minimum code required for a functional custom widget?

The absolute minimum requires three elements: generate an ID with GetID(), define an ImRect bounding box, and call ButtonBehavior() with that rectangle and ID. However, to function properly within layouts, you should also call ItemSize() before ItemAdd(), and to remain visible, you must render geometry to the draw list using the same bounding box coordinates.

Why does my custom widget not respond to clicks?

The most common cause is missing ItemAdd() registration, which prevents the widget from being processed in the interaction loop. Alternatively, you may have duplicate ID conflicts where another widget is consuming the input, or you might be checking for clicks incorrectly—use the hovered and held outputs from ButtonBehavior() rather than raw mouse position checks to respect ImGui's input stack.

How do I handle keyboard navigation for custom widgets?

When you use ItemAdd() with the default parameters, your widget automatically participates in tab navigation and focus systems. For advanced keyboard handling, such as arrow key navigation within a custom grid, manually call SetKeyOwner() from imgui_internal.h to claim ownership of specific ImGuiKey values, preventing other widgets from consuming those inputs while your widget is focused.

Can I create custom widgets without including imgui_internal.h?

Yes, for basic widgets you only need imgui.h and standard API functions like ButtonBehavior(), ItemAdd(), and GetID(). However, imgui_internal.h is required for advanced features such as manual key ownership (SetKeyOwner), duplicate ID flags, and accessing the internal ImGuiWindow structure directly for specialized draw list operations. Most production custom widgets eventually require at least partial inclusion of the internal API.

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 →