How to Create Custom Widgets in Dear ImGui Using the Drawing API
You create custom widgets in Dear ImGui by allocating an interaction rectangle with InvisibleButton(), querying input state with IsItemHovered() and ButtonBehavior(), then issuing draw commands to an ImDrawList retrieved via GetWindowDrawList().
Dear ImGui's immediate-mode architecture allows you to extend the library beyond its built-in controls by drawing directly into the vertex buffers. This guide shows you how to build interactive UI components in the ocornut/imgui repository by combining the internal input handling API with low-level drawing primitives defined in imgui_draw.cpp and declared in imgui.h.
Understanding the ImDrawList Architecture
Every window in Dear ImGui maintains its own ImDrawList buffer accessed via ImGui::GetWindowDrawList(). There are also global background and foreground draw lists available through ImGui::GetBackgroundDrawList() and ImGui::GetForegroundDrawList() for rendering outside window boundaries.
The ImDrawList structure (defined in [imgui.h around lines 3309–3335](https://github.com/ocornut/imgui/blob/master/imgui.h#L3309)) accumulates vertex and index data that the backend uploads to the GPU each frame. Because Dear ImGui operates in immediate mode, your custom widget must redraw its geometry every frame—there is no persistent widget object or retained geometry.
Key implementation files to reference:
imgui_draw.cpp– Contains implementations of allAddLine(),AddRect(),AddCircleFilled(), and other primitives.imgui_internal.h– ExposesItemAdd(),ButtonBehavior(), and other internal layout helpers.imgui.cpp– Houses core window handling and the draw list management system.
The Six-Step Pattern for Custom Widgets
All custom widgets follow a consistent pattern that separates interaction logic from rendering.
1. Reserve Screen Space
Allocate a rectangle and register it with the layout system using ImGui::InvisibleButton() or the low-level ItemAdd() API. This creates a hit-testable region and generates a unique identifier.
const ImVec2 size = ImVec2(100, 30);
ImGui::InvisibleButton("my_widget", size);
// Or manually:
// ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
// ImGui::ItemSize(bb);
// if (!ImGui::ItemAdd(bb, id)) return false;
2. Capture Input State
Query interaction flags immediately after reserving space. Use IsItemHovered() for mouse overlap and ButtonBehavior() for click detection.
bool hovered = ImGui::IsItemHovered();
bool active = ImGui::IsItemActive();
bool clicked = ImGui::IsMouseClicked(0);
3. Retrieve the Draw List
Get the current window's draw list to issue rendering commands. For overlay elements spanning multiple windows, use GetForegroundDrawList().
ImDrawList* draw_list = ImGui::GetWindowDrawList();
// ImDrawList* fg_list = ImGui::GetForegroundDrawList();
4. Compute Geometry
Convert from ImGui coordinates to pixel positions using GetItemRectMin() and GetItemRectMax().
ImVec2 p_min = ImGui::GetItemRectMin();
ImVec2 p_max = ImGui::GetItemRectMax();
ImVec2 center = (p_min + p_max) * 0.5f;
5. Issue Draw Commands
Call ImDrawList methods to render shapes. Use AddRectFilled() for backgrounds, AddCircle() for rounded elements, and AddText() for labels.
draw_list->AddRectFilled(p_min, p_max, IM_COL32(100, 100, 100, 255));
draw_list->AddRect(p_min, p_max, IM_COL32(255, 255, 255, 255));
6. Return Logical State
Return a boolean or value representing the widget's state to the caller.
return clicked; // or return *v for toggles
Practical Implementation: Custom Toggle Button
This complete example implements a circular toggle button using the internal API for proper layout integration and interaction handling.
bool MyToggle(const char* label, bool* v, const ImVec2& size = ImVec2(0, 0))
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (window->SkipItems) return false;
ImGuiContext& g = *GImGui;
const ImGuiID id = window->GetID(label);
const ImVec2 label_size = ImGui::CalcTextSize(label);
const ImVec2 final_size = ImGui::CalcItemSize(size,
label_size.x + g.Style.FramePadding.x * 2,
label_size.y + g.Style.FramePadding.y * 2);
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + final_size);
ImGui::ItemSize(bb, g.Style.FramePadding.y);
if (!ImGui::ItemAdd(bb, id)) return false;
bool hovered = ImGui::ItemHoverable(bb, id);
bool pressed = ImGui::ButtonBehavior(bb, id, nullptr, nullptr, ImGuiButtonFlags_None);
if (pressed) *v = !*v;
ImDrawList* draw = ImGui::GetWindowDrawList();
ImVec2 center = bb.GetCenter();
float radius = ImMin(bb.GetWidth(), bb.GetHeight()) * 0.5f;
ImU32 bg_col = hovered ? ImGui::GetColorU32(ImGuiCol_ButtonHovered)
: ImGui::GetColorU32(ImGuiCol_Button);
draw->AddCircleFilled(center, radius, bg_col, 32);
if (*v)
{
ImU32 check_col = ImGui::GetColorU32(ImGuiCol_Text);
draw->AddLine(center + ImVec2(-radius * 0.4f, 0),
center + ImVec2(0, radius * 0.4f), check_col, 2.0f);
draw->AddLine(center + ImVec2(0, radius * 0.4f),
center + ImVec2(radius * 0.4f, -radius * 0.4f), check_col, 2.0f);
}
if (label_size.x > 0.0f)
ImGui::RenderText(bb.Max + ImVec2(g.Style.ItemSpacing.x, 0.0f), label);
return pressed;
}
The function uses ItemAdd() for proper clipping and navigation support, ItemHoverable() for hover detection, and ButtonBehavior() to handle mouse clicks according to ImGui's standard button rules.
Practical Implementation: Custom Graph Widget
This example renders a line graph using AddLine() to plot values within a defined canvas area.
void PlotCustomGraph(const char* label, const float* values, int values_count,
const ImVec2& size = ImVec2(0, 0))
{
ImGui::BeginGroup();
ImGui::PushID(label);
ImGui::InvisibleButton("canvas", size);
ImVec2 p0 = ImGui::GetItemRectMin();
ImVec2 p1 = ImGui::GetItemRectMax();
ImDrawList* draw = ImGui::GetWindowDrawList();
draw->AddRectFilled(p0, p1, ImGui::GetColorU32(ImGuiCol_FrameBg));
draw->AddLine(ImVec2(p0.x, p1.y), ImVec2(p1.x, p1.y),
ImGui::GetColorU32(ImGuiCol_Text));
draw->AddLine(ImVec2(p0.x, p0.y), ImVec2(p0.x, p1.y),
ImGui::GetColorU32(ImGuiCol_Text));
if (values_count > 1)
{
float inv_count = 1.0f / (float)(values_count - 1);
ImVec2 prev = ImVec2(p0.x, p1.y - values[0] * (p1.y - p0.y));
for (int i = 1; i < values_count; ++i)
{
ImVec2 cur = ImVec2(
p0.x + (p1.x - p0.x) * i * inv_count,
p1.y - values[i] * (p1.y - p0.y)
);
draw->AddLine(prev, cur, ImGui::GetColorU32(ImGuiCol_PlotLines));
prev = cur;
}
}
ImGui::PopID();
ImGui::EndGroup();
}
Handling Clipping and Coordinate Spaces
Restrict drawing to specific areas using ImGui::PushClipRect() and ImGui::PopClipRect(). This is essential when implementing scrollable custom widgets or nested canvases.
ImGui::PushClipRect(p_min, p_max, true); // intersect_with_current_clip=true
draw_list->AddCircle(center, radius, col);
ImGui::PopClipRect();
When drawing outside the current window bounds—such as for tooltips, drag previews, or global overlays—use the foreground draw list obtained from ImGui::GetForegroundDrawList(). This list renders on top of all windows and bypasses the current window's clipping rectangle.
Summary
- Reserve space with
InvisibleButton()orItemAdd()to create an interaction rectangle and obtain an ID. - Query input using
IsItemHovered(),IsItemActive(), orButtonBehavior()immediately after reserving space. - Retrieve the draw list via
GetWindowDrawList()for window-local content, orGetForegroundDrawList()for global overlays. - Draw every frame because Dear ImGui is immediate-mode; geometry persists only until the next frame.
- Reference
imgui_draw.cppfor primitive implementations andimgui.hforImDrawListmethod signatures.
Frequently Asked Questions
What is the difference between GetWindowDrawList and GetBackgroundDrawList?
GetWindowDrawList() returns the draw list for the current window, automatically clipped to that window's boundaries and transformed by its scroll position. GetBackgroundDrawList() and GetForegroundDrawList() return global draw lists that render behind or in front of all windows, using screen-space coordinates regardless of the current window context. Use global lists for elements that must persist across window boundaries or render outside the current window's clipped region.
How do I handle mouse interaction in a custom widget?
First, reserve space with InvisibleButton() to generate a hit rectangle and ID. Then check IsItemHovered() to detect mouse overlap and IsItemClicked() or ButtonBehavior() to handle clicking. For complex widgets requiring drag operations, use ButtonBehavior() with flags like ImGuiButtonFlags_MouseButtonLeft and query IsItemActive() during subsequent frames to track ongoing interaction.
Where are the ImDrawList primitives implemented?
All drawing methods—AddLine(), AddRect(), AddCircleFilled(), AddText(), etc.—are implemented in imgui_draw.cpp. The structure definition and public API declarations reside in imgui.h around lines 3309–3335, while advanced internal helpers are found in imgui_internal.h.
Do custom widgets need to cache their geometry between frames?
No. Dear ImGui is an immediate-mode library, meaning you redraw your widget's geometry every frame by issuing new commands to the ImDrawList. The library rebuilds the vertex buffers from scratch each frame, so there is no benefit to caching previous draw commands. Focus on optimizing the CPU-side geometry calculation rather than attempting to retain GPU buffers.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →