How to Extend Dear ImGui with Custom Widgets: A Complete Guide to the Internal API

To extend Dear ImGui with custom widgets, you generate a unique ID with GetID(), register the bounding box with ItemAdd(), handle interactions via ButtonBehavior(), and render using ImDrawList primitives—all without modifying the core library.

Dear ImGui's immediate-mode architecture in the ocornut/imgui repository separates layout/interaction from rendering, allowing you to create sophisticated custom controls by composing a small set of internal API functions. By following the same pattern used by built-in buttons and sliders, you can add custom drawing that respects clipping rectangles, style colors, and navigation focus.

The Six Essential Steps for Custom Widgets

Every widget in Dear ImGui follows a strict lifecycle. When building custom widgets in your own codebase, implement these six sequential operations:

1. Generate a Stable ID

Before processing or drawing, you must generate a unique identifier so ImGui can track state (hover, active, focus) across frames. Call window->GetID(label) or use ImGui::GetID() directly. According to the source in imgui.cpp, this hashes the ID stack to create a stable 32-bit identifier.

2. Define the Geometry with ItemAdd

Submit the widget’s bounding rectangle (ImRect) to the layout system using ItemAdd(bb, id) defined in imgui.cpp around line 11455. This function registers the rectangle for clipping, hit-testing, and navigation focus. Always call ItemSize(bb) immediately before ItemAdd() to advance the cursor position.

3. Handle Input via ButtonBehavior

Process mouse and keyboard navigation using ButtonBehavior(bb, id, &hovered, &held, flags) from imgui_widgets.cpp (line 545). This implements the generic "mouse-over / mouse-pressed" state machine used by most built-in widgets, handling edge cases like repeat rates and navigation activation automatically.

4. Maintain ID Lifetime with KeepAliveID

If you create a widget that bypasses ItemAdd()—for example, issuing raw draw calls that must still react to IsItemHovered() later—you must call KeepAliveID(id) from imgui.cpp (line 5668). Most custom widgets follow the standard ItemAdd path, so this step is rarely required.

5. Render to ImDrawList

Issue draw commands through the immediate-mode draw list using ImGui::GetWindowDrawList()->Add... primitives. The ImDrawList API (defined in imgui_draw.cpp) provides rectangles, lines, circles, and text that integrate seamlessly with the current theme and clipping.

6. Wrap in a Public Function

Expose your widget as a standard C++ function matching ImGui’s naming conventions (e.g., MyToggle(const char* label, bool* v)). Place this function in your own source files—no modifications to the library are necessary.

Complete Implementation: Custom Toggle Switch

The following implementation demonstrates the complete workflow, creating a functional toggle switch that uses ButtonBehavior, ItemAdd, and ImDrawList:

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

    // 1. Generate unique ID
    const ImGuiID id = window->GetID(label);

    // 2. Define geometry
    const ImVec2 widgetSize = ImVec2(30, 18);
    const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + widgetSize);
    ImGui::ItemSize(bb);
    if (!ImGui::ItemAdd(bb, id))
        return false;

    // 3. Handle input
    bool hovered, held;
    ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    if (held && ImGui::IsMouseClicked(0))
        *v = !*v;

    // 4. Render via ImDrawList
    ImU32 col_bg = ImGui::GetColorU32(*v ? ImGuiCol_ButtonActive : ImGuiCol_Button);
    ImU32 col_knob = ImGui::GetColorU32(ImGuiCol_Text);
    ImDrawList* draw = ImGui::GetWindowDrawList();
    
    draw->AddRectFilled(bb.Min, bb.Max, col_bg, 4.0f);
    ImVec2 knobPos = *v ? bb.Max - ImVec2(4, 4) : bb.Min + ImVec2(4, 4);
    draw->AddCircleFilled(knobPos, 6.0f, col_knob);

    // Optional label rendering
    if (hovered && ImGui::IsItemHovered())
        ImGui::SetTooltip("%s", label);

    return true;
}

This pattern—GetID → ItemSize → ItemAdd → ButtonBehavior → Render—mirrors the implementation of native widgets in imgui_widgets.cpp around line 541.

Advanced Example: Custom Color Picker

For widgets requiring complex hit-testing, compute interaction manually while still using ButtonBehavior for the base interaction state:

bool ColorPicker(const char* label, ImVec4* col)
{
    ImGuiWindow* win = ImGui::GetCurrentWindow();
    if (win->SkipItems)
        return false;

    const ImGuiID id = win->GetID(label);
    const ImVec2 size = ImVec2(200, 200);
    const ImRect bb(win->DC.CursorPos, win->DC.CursorPos + size);
    
    ImGui::ItemSize(bb);
    if (!ImGui::ItemAdd(bb, id))
        return false;

    // Handle interaction
    bool hovered, held;
    ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    
    if (held && ImGui::GetIO().MouseClicked[0])
    {
        ImVec2 mouse = ImGui::GetIO().MousePos - bb.Min;
        mouse.x = ImClamp(mouse.x / size.x, 0.0f, 1.0f);
        mouse.y = ImClamp(mouse.y / size.y, 0.0f, 1.0f);
        
        // Convert normalized coordinates to RGB
        ImGui::ColorConvertHSVtoRGB(mouse.x, 1.0f - mouse.y, col->w, 
                                    col->x, col->y, col->z);
    }

    // Render gradient background
    ImDrawList* draw = ImGui::GetWindowDrawList();
    for (int i = 0; i < 255; ++i)
    {
        float t = i / 255.0f;
        ImU32 color = ImGui::ColorConvertFloat4ToU32(
            ImVec4(t, 1.0f - t, col->z, 1.0f));
        draw->AddLine(bb.Min + ImVec2(t * size.x, 0), 
                      bb.Min + ImVec2(t * size.x, size.y), color);
    }

    // Selection marker
    ImVec2 marker = bb.Min + ImVec2(col->x * size.x, (1.0f - col->y) * size.y);
    draw->AddCircleFilled(marker, 5.0f, ImGui::GetColorU32(ImGuiCol_CheckMark));

    return held;
}

Testing Your Widget in the Demo

To verify your custom widget integrates correctly with clipping and focus systems, add it to imgui_demo.cpp inside the ShowDemoWindow() function:

if (ImGui::CollapsingHeader("Custom Widgets"))
{
    static bool toggle = false;
    MyToggle("Example Toggle", &toggle);

    static ImVec4 color = ImVec4(0.4f, 0.6f, 0.9f, 1.0f);
    ColorPicker("Custom Picker", &color);
}

The demo file already contains reference implementations of complex widgets, making it an ideal environment for testing custom interactions without writing a separate application.

Key Source Files for Widget Development

Understanding these core files helps you trace the data flow from ID generation to rendering:

  • imgui.cpp – Contains ItemAdd (line 11455), KeepAliveID (line 5668), and the navigation handling logic.
  • imgui_widgets.cpp – Implements built-in widgets and exposes ButtonBehavior (line 545) for reuse in custom controls.
  • imgui_draw.cpp – Defines ImDrawList and all immediate-mode rendering primitives.
  • imgui_demo.cpp – Reference implementations and test cases; add your widgets here to experiment.

Summary

  • Generate IDs using window->GetID() to maintain state across frames.
  • Register geometry with ItemSize() followed by ItemAdd() to participate in layout and clipping.
  • Handle interaction through ButtonBehavior() to reuse ImGui’s robust input state machine.
  • Render using ImDrawList primitives for automatic style and clipping integration.
  • Place code in your own source files—modifying ocornut/imgui is unnecessary for custom widgets.
  • Reference imgui_widgets.cpp line 541 for the canonical button implementation pattern.

Frequently Asked Questions

Do I need to modify the Dear ImGui source code to create custom widgets?

No. The most common approach is user-side extension—simply adding a C++ function like MyToggle() to your own source files. You only need to modify the core library if you require deep integration with the navigation focus stack or need to add new data types to ImGuiDataType.

What is the difference between ItemSize and ItemAdd?

ItemSize() advances the cursor position and calculates layout bounds, while ItemAdd() (defined in imgui.cpp) actually registers the item with the window’s item list, enabling clipping, hit-testing, and ID tracking. You must call both for every interactive widget.

How do I handle keyboard navigation in custom widgets?

Calling ButtonBehavior() automatically integrates with ImGui’s navigation system. When users navigate with keyboard/gamepad, ImGui will trigger the held output parameter and activate the widget appropriately. For custom navigation behaviors, query ImGui::IsItemFocused() after calling ItemAdd().

Why would I need KeepAliveID?

KeepAliveID() (from imgui.cpp line 5668) is required only when you create a widget that issues raw draw calls without calling ItemAdd(). It prevents the widget’s ID from being garbage-collected when the item is clipped or skipped. Most custom widgets use the standard ItemAdd() path and never need this function.

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 →