How to Create Entirely New Custom Widget Types from Scratch in Dear ImGui

Creating custom widgets in Dear ImGui requires three fundamental steps: reserving layout space and registering an ID with ItemSize() and ItemAdd(), handling interaction states via ButtonBehavior(), and drawing primitives through the window's ImDrawList.

Dear ImGui (ocornut/imgui) implements all UI elements—from simple buttons to complex color pickers—using a consistent immediate-mode architecture rather than hard-coded widget classes. This design allows developers to create entirely new custom widget types from scratch by replicating the same internal pattern used by the built-in components. By manipulating the draw list and layout system directly, custom widgets inherit automatic clipping, navigation support, and theme integration.

The Three-Step Widget Architecture

Every widget in imgui_widgets.cpp follows an identical lifecycle. Mastering these three phases allows you to construct any interactive element.

Step 1: Layout Reservation and ID Generation

First, your widget must declare its screen space and obtain a unique identifier. As implemented in ImGui::ButtonEx at lines 86–104 of imgui_widgets.cpp, this involves:

  1. Calling window->GetID(label) to generate an ImGuiID from the current ID stack.
  2. Calculating the bounding box (ImRect bb) based on window->DC.CursorPos and desired dimensions.
  3. Invoking ItemSize(bb) to advance the cursor and reserve vertical space.
  4. Calling ItemAdd(bb, id) to register the item with ImGui's layout and clipping system.

If ItemAdd returns false (indicating the widget is clipped or the window is collapsed), you should early-out to save processing time.

Step 2: Interaction Handling with ButtonBehavior

Once registered, the widget must respond to input. Rather than handling mouse coordinates manually, call ButtonBehavior() as seen at lines 5450–5460 in imgui_widgets.cpp:

bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);

This helper automatically manages mouse hover detection, click states, keyboard/gamepad navigation, and overlapping window checks. It respects ImGuiButtonFlags such as ImGuiButtonFlags_PressedOnClickRelease or ImGuiButtonFlags_AllowOverlap, ensuring your custom widget behaves like native ImGui elements.

Step 3: Rendering via ImDrawList

Finally, draw the visual representation using the window's draw list. Access it via ImGui::GetWindowDrawList() and issue primitive commands like AddRectFilled(), AddCircle(), or AddText(). The demo example ShowExampleAppCustomRendering at lines 10248–10266 in imgui_demo.cpp demonstrates how to push custom geometry inside a widget-like function while respecting the current transform and clipping region.

Complete Example: Building a Custom Toggle Switch

The following implementation creates a fully functional toggle switch widget that follows ImGui's architectural conventions:

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

    ImGuiContext& g = *GImGui;
    const ImGuiID id = window->GetID(label);
    const ImVec2 size(40.0f, 20.0f);
    const ImVec2 pos = window->DC.CursorPos;
    const ImRect bb(pos, pos + size);

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

    bool hovered, held;
    bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    
    if (pressed)
        *v = !*v;

    ImDrawList* draw = ImGui::GetWindowDrawList();
    ImU32 col_bg = ImGui::GetColorU32(*v ? ImGuiCol_ButtonActive : ImGuiCol_Button);
    ImU32 col_knob = ImGui::GetColorU32(ImGuiCol_FrameBg);
    
    const float radius = size.y * 0.5f - 2.0f;
    const ImVec2 knob_center = *v 
        ? ImVec2(pos.x + size.x - radius - 2.0f, pos.y + size.y * 0.5f)
        : ImVec2(pos.x + radius + 2.0f, pos.y + size.y * 0.5f);

    draw->AddRectFilled(bb.Min, bb.Max, col_bg, size.y * 0.5f);
    draw->AddCircleFilled(knob_center, radius, col_knob);

    if (label[0])
    {
        ImVec2 label_size = ImGui::CalcTextSize(label);
        ImVec2 label_pos = ImVec2(bb.Max.x + g.Style.ItemInnerSpacing.x,
                                 bb.Min.y + (size.y - label_size.y) * 0.5f);
        ImGui::RenderText(label_pos, label);
    }

    return pressed;
}

Key implementation details:

  • Early-out optimization: Checks window->SkipItems to respect collapsed windows.
  • ID uniqueness: Uses window->GetID() to prevent collisions when the widget appears in loops.
  • State visualization: Leverages hovered and held (returned by reference from ButtonBehavior) to adjust colors for visual feedback if desired.

Advanced Interaction: Drag-and-Drop Support

Custom widgets can leverage ImGui's advanced features like drag-and-drop targets and sources. This color button example accepts color payloads while maintaining the core three-step structure:

bool MyColorButton(const char* id_str, ImU32* color)
{
    ImGuiWindow* window = ImGui::GetCurrentWindow();
    if (window->SkipItems)
        return false;

    const ImVec2 size(36.0f, 36.0f);
    const ImVec2 pos = window->DC.CursorPos;
    const ImRect bb(pos, pos + size);
    const ImGuiID id = window->GetID(id_str);
    
    ImGui::ItemSize(size);
    if (!ImGui::ItemAdd(bb, id))
        return false;

    bool hovered, held;
    ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_PressedOnClickRelease);

    if (held && ImGui::BeginDragDropSource(ImGuiDragDropFlags_None))
    {
        ImGui::SetDragDropPayload("COLOR", color, sizeof(ImU32));
        ImGui::Text("Color: 0x%08X", *color);
        ImGui::EndDragDropSource();
    }

    if (ImGui::BeginDragDropTarget())
    {
        if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR"))
            memcpy(color, payload->Data, sizeof(ImU32));
        ImGui::EndDragDropTarget();
    }

    ImDrawList* draw = ImGui::GetWindowDrawList();
    draw->AddRectFilled(bb.Min, bb.Max, *color, 4.0f);
    draw->AddRect(bb.Min, bb.Max, ImGui::GetColorU32(hovered ? ImGuiCol_BorderShadow : ImGuiCol_Border), 4.0f);

    return held;
}

Essential Source Files for Reference

When creating entirely new custom widget types from scratch, consult these files in the ocornut/imgui repository:

  • imgui_widgets.cpp: Contains reference implementations of standard widgets. Examine ButtonEx (lines 86–104) for layout/ID patterns and ButtonBehavior (lines 5450–5460) for interaction logic.
  • imgui_internal.h: Declares internal helpers like ItemAdd(), ItemSize(), and navigation utilities required for advanced widgets.
  • imgui_demo.cpp: Review ShowExampleAppCustomRendering (lines 10248–10266) for examples of low-level ImDrawList usage within widget contexts.
  • imgui_draw.cpp: Implements drawing primitives (AddRectFilled, AddCircle, etc.) used in the rendering phase.
  • imgui.h: Provides the public API and style color enums (ImGuiCol_Button, ImGuiCol_FrameBg) for consistent theming.

Summary

  • Layout and Identity: Always call ItemSize() and ItemAdd() to register your widget's bounding box and ID, as demonstrated in imgui_widgets.cpp.
  • Interaction: Use ButtonBehavior() to handle mouse, keyboard, and gamepad input automatically without manual coordinate checks.
  • Rendering: Draw via ImDrawList primitives obtained from GetWindowDrawList(), ensuring automatic clipping and transform application.
  • Integration: Custom widgets respect PushID()/PopID() scopes, style colors, and window clipping when following the standard three-step pattern.

Frequently Asked Questions

How do I handle keyboard navigation in a custom ImGui widget?

Keyboard and gamepad navigation are automatically handled when you use ButtonBehavior() or similar interaction helpers from imgui_internal.h. If you need custom navigation logic, check the return value of ItemAdd() for navigation highlight requests and call SetItemDefaultFocus() when appropriate.

Can I create a custom widget that spans multiple frames or has internal state?

Yes, though ImGui is immediate-mode, you can persist state using GetStateStorage() or by allocating custom data via GetID() keys. For multi-frame animations or transitions, store state in your application code and pass it as parameters to your widget function each frame.

Why does my custom widget not clip when the window is scrolled?

Ensure you call ItemAdd() before rendering and verify that your drawing commands use GetWindowDrawList() rather than GetForegroundDrawList() (unless you specifically need overlay rendering). The ItemAdd() function registers your bounding box with the current window's clip rect; anything drawn outside bb will be culled automatically.

What is the difference between ButtonBehavior and InvisibleButton?

InvisibleButton found in imgui_widgets.cpp is a convenience wrapper that creates a hit-region without rendering. ButtonBehavior is the lower-level internal function that handles all input logic. Use InvisibleButton for simple hit-tests, but call ButtonBehavior directly when building custom widgets to gain fine-grained control over hover states, button flags, and return values.

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 →