How to Implement Custom Widget Types by Extending Dear ImGui

Dear ImGui is designed to be extensible without touching the core source files; you implement custom widget types by generating unique IDs with GetID(), handling interaction via ButtonBehavior(), registering the item with ItemAdd(), and rendering through the window's draw list.

Dear ImGui (ocornut/imgui) provides an immediate-mode GUI architecture that allows developers to create custom widgets without modifying the library's internals. By following the same patterns found in imgui_widgets.cpp, you can add specialized controls that integrate seamlessly with ImGui's navigation, styling, and input handling systems.

The Anatomy of a Custom Widget

Creating a custom widget involves six distinct steps that mirror the internal implementation of built-in controls like Button() and Checkbox().

Generate a Unique ID

Every widget requires a unique identifier to handle interaction state, navigation, and the ID stack. Use ImGui::GetID() or the PushID()/PopID() helpers to generate this identifier. In imgui.cpp at approximately line 1512, window->GetID(label) hashes the string with the current ID stack seed to produce a unique ImGuiID.

Define the Interaction Rectangle

Calculate the widget's bounding box (ImRect) based on content size and frame padding. Call ImGui::ItemSize() to advance the cursor and ImGui::ItemAdd() (defined in imgui_internal.h at line 3616) to register the item with the UI system. Registration enables automatic clipping, navigation, and tooltip support.

Handle Interaction with ButtonBehavior

Call ImGui::ButtonBehavior() (located in imgui_internal.h at line 3766) to process mouse input, hover states, focus, and navigation. This helper encapsulates the complex state machine for button logic, including repeat and double-click handling.

The function signature is:

bool ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags = 0);

Render the Visual Appearance

Use ImGui::GetWindowDrawList() to access low-level drawing functions like AddRectFilled(), AddText(), and RenderFrame(). These ensure your widget respects the current color theme and style settings.

Return Interaction Results

Return a boolean (or custom value) indicating whether the widget was activated, following the convention of built-in functions.

Complete Implementation: Custom Toggle Button

Below is a complete, compilable example that creates a colorful toggle button. Place this implementation in a separate .cpp file to avoid modifying core ImGui sources.

// MyToggleButton.h
#pragma once
#include "imgui.h"

namespace ImGui
{
    // Returns true when the toggle changes state.
    bool MyToggleButton(const char* label, bool* v);
}
// MyToggleButton.cpp
#include "MyToggleButton.h"
#include "imgui_internal.h"   // Required for ButtonBehavior, ItemAdd, etc.

namespace ImGui
{
    bool MyToggleButton(const char* label, bool* v)
    {
        // 1. Generate ID and check visibility
        ImGuiWindow* window = GetCurrentWindow();
        if (window->SkipItems)
            return false;
        
        ImGuiID id = window->GetID(label);

        // 2. Define bounding box
        const ImVec2 label_size = CalcTextSize(label, nullptr, true);
        const ImVec2 padding = GetStyle().FramePadding;
        const ImRect bb(window->DC.CursorPos, 
                        window->DC.CursorPos + ImVec2(padding.x * 2 + label_size.x,
                                                      padding.y * 2 + label_size.y));
        
        ItemSize(bb, GetStyle().FramePadding.y);
        if (!ItemAdd(bb, id))
            return false;  // Clipped

        // 3. Interaction handling
        bool hovered, held;
        bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
        
        if (pressed)
            *v = !*v;

        // 4. Rendering
        const ImU32 col_bg = *v ? GetColorU32(ImGuiCol_ButtonActive) 
                                : GetColorU32(ImGuiCol_Button);
        const ImU32 col_border = hovered ? GetColorU32(ImGuiCol_BorderShadow) 
                                         : GetColorU32(ImGuiCol_Border);
        
        RenderFrame(bb.Min, bb.Max, col_bg, true, GetStyle().FrameRounding);
        window->DrawList->AddRect(bb.Min, bb.Max, col_border, GetStyle().FrameRounding);
        RenderTextClipped(bb.Min + padding, bb.Max - padding, label, nullptr, 
                         &label_size, ImVec2(0.5f, 0.5f), &bb);
        
        return pressed;
    }
}

This implementation automatically integrates with ImGui's ID stack, allowing multiple toggles with identical labels to coexist when wrapped in PushID()/PopID() calls.

Advanced Custom Widget Examples

Color Picker Mini-Widget

This widget displays a color square that opens the built-in color picker when clicked. It demonstrates how to trigger popups from custom widgets.

bool MyColorPicker(const char* label, ImVec4* col)
{
    ImGuiWindow* window = GetCurrentWindow();
    if (window->SkipItems) 
        return false;

    ImGuiID id = window->GetID(label);
    const float size = GetTextLineHeight() * 2.0f;
    const ImRect bb(window->DC.CursorPos,
                    window->DC.CursorPos + ImVec2(size, size));
    
    ItemSize(bb);
    if (!ItemAdd(bb, id)) 
        return false;

    // Interaction
    bool hovered, held;
    bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    
    if (pressed)
        OpenPopup(label);

    // Rendering
    RenderFrame(bb.Min, bb.Max, GetColorU32(*col), true, 0.0f);
    RenderFrameBorder(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f);

    // Popup content
    bool changed = false;
    if (BeginPopup(label))
    {
        changed = ColorPicker4("##picker", (float*)col, ImGuiColorEditFlags_NoLabel);
        EndPopup();
    }
    
    return changed;
}

Circular Knob Slider

This example creates a radial slider control that updates based on mouse angle while held, showcasing custom geometry processing.

#include <cmath>

bool MyKnob(const char* label, float* v, float v_min, float v_max, float radius = 30.0f)
{
    ImGuiWindow* window = GetCurrentWindow();
    if (window->SkipItems) 
        return false;

    ImGuiID id = window->GetID(label);
    const ImVec2 center = window->DC.CursorPos + ImVec2(radius, radius);
    const ImRect bb(center - ImVec2(radius, radius), 
                    center + ImVec2(radius, radius));
    
    ItemSize(bb);
    if (!ItemAdd(bb, id)) 
        return false;

    // Interaction
    bool hovered, held;
    bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
    
    if (held)
    {
        ImVec2 mp = GetIO().MousePos - center;
        float angle = atan2f(mp.y, mp.x);
        float t = (angle + IM_PI) / (2.0f * IM_PI);
        *v = ImLerp(v_min, v_max, t);
        MarkItemEdited(id);
    }

    // Rendering
    ImU32 col_bg = GetColorU32(held ? ImGuiCol_FrameBgActive : ImGuiCol_FrameBg);
    RenderFrame(bb.Min, bb.Max, col_bg, true, radius);
    
    // Indicator line
    float angle = ImLerp(-IM_PI, IM_PI, (*v - v_min) / (v_max - v_min));
    ImVec2 end = center + ImVec2(cosf(angle), sinf(angle)) * radius * 0.8f;
    window->DrawList->AddLine(center, end, GetColorU32(ImGuiCol_SliderGrab), 4.0f);
    
    return held;
}

Key Source Files for Custom Widget Development

When extending ImGui, reference these files to understand the internal architecture:

  • imgui.h: Public API definitions. All custom widgets must include this header for ImGui:: namespace functions and types.
  • imgui_internal.h: Contains ButtonBehavior() (line 3766), ItemAdd() (line 3616), and SetItemKeyOwner() (around line 3602) required for interaction handling and item registration.
  • imgui_widgets.cpp: Reference implementations of all built-in widgets (Button, SliderFloat, Checkbox, etc.) demonstrating proper ID handling, interaction patterns, and rendering techniques.
  • imgui.cpp: Core implementation including RenderFrame(), GetID() generation (around line 1512), and the main UI loop logic.

Summary

  • Never modify core files: Place custom widget implementations in separate .cpp/.h files that include imgui.h and imgui_internal.h.
  • Follow the four-step pattern: Generate ID → Define bounds → Call ButtonBehavior() → Render with draw lists.
  • Use internal helpers: ButtonBehavior() in imgui_internal.h provides production-ready input handling including navigation, focus, and repeat logic.
  • Register items properly: ItemAdd() enables your widget to participate in ImGui's layout, clipping, and navigation systems.
  • Leverage the ID stack: GetID() automatically respects PushID() contexts, allowing safe widget reuse.

Frequently Asked Questions

Do I need to modify imgui.cpp to add custom widgets?

No. Dear ImGui is explicitly designed for extension without core modifications. Create a new source file (e.g., my_widgets.cpp), include imgui.h and imgui_internal.h, and implement your widget in the ImGui namespace. This ensures compatibility with future ImGui updates.

Why must I include imgui_internal.h for custom widgets?

While basic usage only requires imgui.h, imgui_internal.h exposes essential building blocks like ButtonBehavior(), ItemAdd(), and SetItemKeyOwner() that handle the complex logic of mouse interaction, keyboard navigation, and item registration. These functions are stable enough for widget development but are kept internal to preserve API cleanliness.

How do I handle keyboard input in a custom widget?

After calling ItemAdd(), use SetItemKeyOwner() (available in imgui_internal.h around line 3602) to claim ownership of specific keys. Check ImGui::IsKeyDown() or ImGui::IsKeyPressed() within your widget logic to respond to keyboard events while maintaining focus compatibility.

Can I store state inside a custom widget function?

For state that persists between frames (like animation values or edit buffers), avoid static variables inside the function. Instead, accept a pointer to a user-provided state structure as a parameter, similar to how ImGui::BeginCombo() works. This allows multiple instances of the widget to maintain independent states.

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 →