How to Create Custom Widgets Using the ImDrawList Low-Level API in Dear ImGui
To create custom widgets in Dear ImGui, combine ImDrawList primitives (lines, rectangles, arcs) with InvisibleButton for input handling, using screen-space coordinates relative to the current window's draw list.
Dear ImGui's ImDrawList API provides direct access to the vertex buffer layer, enabling developers to create custom widgets that integrate seamlessly with the rest of the UI. By leveraging the low-level drawing commands implemented in imgui_draw.cpp alongside standard input helpers, you can build bespoke interactive elements—from radial knobs to multi-segment progress bars—while maintaining full compatibility with ImGui's clipping and styling systems.
Understanding the ImDrawList Architecture
The ImDrawList class serves as the core rendering layer for Dear ImGui, managing vertex and index buffers while handling clipping and draw-command state. Unlike high-level widget functions, this low-level API exposes raw geometry commands that write directly into the window's render buffer.
Core Components and Source Files
-
ImGui::GetWindowDrawList()– Obtains theImDrawList*associated with the current window. Defined inimgui.haround line 1245, this function provides the pointer you feed drawing commands to. -
ImDrawListclass definition – Stores vertex buffers, index buffers, and manages texture and clipping state. Found inimgui_internal.haround line 858, this class declares primitive helpers likeAddLine,AddRectFilled, andPathArcTo. -
ImDrawListimplementation – The heavy lifting of buffer allocation, command merging, and clipping occurs inimgui_draw.cppstarting around line 392. This file realizes the geometry pipeline used by all helper methods. -
Reference implementation – The
imgui_demo.cppfile contains working examples of custom rendering in the "Custom rendering" section around line 10171, demonstrating the pattern of combining draw lists with invisible buttons.
The Six-Step Workflow for Custom Widgets
Creating functional custom widgets requires coupling visual output with input handling. Follow this standardized pattern:
-
Determine the widget's screen rectangle – Use
ImGui::GetCursorScreenPos()to retrieve the top-left corner, then calculate the bounding box using your desired size. -
Reserve interaction space – Call
ImGui::InvisibleButton("##widget_id", size)so ImGui tracks hover, click, and drag states for your widget area. -
Obtain the draw list – Retrieve the current draw list via
ImDrawList* draw = ImGui::GetWindowDrawList();. -
Add geometry – Use methods like
draw->AddRectFilled,draw->AddCircle, ordraw->PathArcToto render the visual representation within your calculated rectangle. -
Query interaction states – After the invisible button call, check
ImGui::IsItemHovered(),ImGui::IsItemClicked(), orImGui::IsItemActive()to drive widget state. -
Optional: animate – Store persistent state in static variables or structs, updating values each frame before issuing draw commands.
Because ImDrawList operates in screen space and does not perform layout beyond the window's clip rectangle, all coordinates must be absolute positions. The system automatically respects the current clip rectangle when using GetWindowDrawList(), ensuring your widget renders only within visible window bounds.
Practical Implementation Examples
The following self-contained snippets demonstrate the complete pattern of draw-list rendering coupled with input handling. Each example can be integrated directly into any ImGui-based project.
Custom Toggle Button with Check Mark
This example creates a square toggle button that draws a check mark when active, using standard ImGui theme colors for consistency.
bool MyToggleButton(const char* label, bool* v, const ImVec2& size = ImVec2(0,0))
{
// 1️⃣ Layout – let ImGui allocate space
ImGui::PushID(label);
ImGui::BeginGroup();
ImGui::Dummy(size == ImVec2(0,0) ? ImVec2(20,20) : size);
ImGui::SameLine(0,0);
// 2️⃣ Interaction – invisible button covers the same rectangle
bool clicked = ImGui::InvisibleButton("##toggle", ImGui::GetItemRectSize());
// 3️⃣ State handling
if (clicked && ImGui::IsMouseClicked(0))
*v = !*v;
// 4️⃣ Rendering
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetItemRectMin();
ImVec2 q = ImGui::GetItemRectMax();
ImU32 col_bg = ImGui::GetColorU32((*v) ? ImGuiCol_ButtonActive : ImGuiCol_Button);
dl->AddRectFilled(p, q, col_bg, 4.0f);
if (*v)
{
// Draw check mark
dl->AddLine(ImVec2(p.x+4, p.y+4), ImVec2(q.x-4, q.y-4), ImGui::GetColorU32(ImGuiCol_Text), 2.0f);
dl->AddLine(ImVec2(p.x+4, q.y-4), ImVec2(q.x-4, p.y+4), ImGui::GetColorU32(ImGuiCol_Text), 2.0f);
}
// 5️⃣ Optional label
ImGui::SameLine();
ImGui::TextUnformatted(label);
ImGui::EndGroup();
ImGui::PopID();
return clicked;
}
Key implementation details:
InvisibleButtonprovides hover and click states without rendering default button graphics.ImDrawList::AddRectFilleddraws the background using theme colors (ImGuiCol_ButtonandImGuiCol_ButtonActive).GetItemRectMinandGetItemRectMaxprovide screen-space coordinates for the drawn geometry.
Radial Knob Control Using Arc Primitives
This circular knob widget demonstrates path-based rendering and drag interaction for float value manipulation.
float MyKnob(const char* label, float value, float min, float max, float radius = 30.0f)
{
ImGui::PushID(label);
ImVec2 center = ImGui::GetCursorScreenPos() + ImVec2(radius, radius);
ImGui::InvisibleButton("##knob", ImVec2(radius*2, radius*2));
bool hovered = ImGui::IsItemHovered();
bool active = ImGui::IsItemActive();
// Drag handling (horizontal drag → value change)
if (active && ImGui::IsMouseDragging(0))
{
float delta = ImGui::GetIO().MouseDelta.x * (max-min) * 0.01f;
value = ImClamp(value + delta, min, max);
}
// Compute angle (0 = right, clockwise)
float t = (value - min) / (max - min);
float angle = ImLerp(-IM_PI * 0.75f, IM_PI * 0.75f, t);
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 col_bg = ImGui::GetColorU32(ImGuiCol_FrameBg);
ImU32 col_fg = ImGui::GetColorU32(ImGuiCol_SliderGrab);
ImU32 col_hl = ImGui::GetColorU32(ImGuiCol_SliderGrabActive);
// Background circle
dl->AddCircleFilled(center, radius, col_bg, 32);
// Arc representing current value
dl->PathArcTo(center, radius-4, -IM_PI*0.75f, angle, 32);
dl->PathStroke(col_fg, false, 6.0f);
// Optional highlight when hovered
if (hovered)
dl->AddCircle(center, radius, col_hl, 32, 2.0f);
ImGui::SameLine(0, radius*2 + ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Text("%s: %.2f", label, value);
ImGui::PopID();
return value;
}
Key implementation details:
- The knob uses
PathArcTocombined withPathStroketo draw a partial circle arc. - Horizontal mouse dragging updates the value using
ImGui::GetIO().MouseDelta. - Colors are pulled from
ImGuiCol_SliderGraband related style variables to ensure theme consistency.
Multi-Segment Progress Bar
This widget splits a progress bar into colored segments, useful for displaying composite data like memory usage or CPU core utilization.
float MyMultiBar(const char* label, const float* values, const ImU32* colors, int count, const ImVec2& size = ImVec2(0,0))
{
ImGui::PushID(label);
ImVec2 barSize = size;
if (barSize.x == 0) barSize.x = ImGui::CalcItemWidth();
if (barSize.y == 0) barSize.y = ImGui::GetTextLineHeight();
ImGui::InvisibleButton("##multibar", barSize);
bool hovered = ImGui::IsItemHovered();
ImVec2 pos = ImGui::GetItemRectMin();
ImDrawList* dl = ImGui::GetWindowDrawList();
float cum = 0.0f;
for (int i = 0; i < count; ++i)
{
float w = barSize.x * values[i];
ImVec2 a = ImVec2(pos.x + cum, pos.y);
ImVec2 b = ImVec2(a.x + w, pos.y + barSize.y);
dl->AddRectFilled(a, b, colors[i]);
cum += w;
}
// Border
dl->AddRect(pos, pos + barSize, ImGui::GetColorU32(ImGuiCol_Border));
if (hovered)
dl->AddRect(pos, pos + barSize, ImGui::GetColorU32(ImGuiCol_Text), 0.0f, ImDrawFlags_None, 2.0f);
ImGui::SameLine();
ImGui::TextUnformatted(label);
ImGui::PopID();
return cum / barSize.x;
}
Key implementation details:
- The widget calculates segment widths proportionally using the input
valuesarray. CalcItemWidthensures the bar matches standard ImGui item sizing when no explicit size is provided.- Hover state draws an additional highlight rectangle using
ImGuiCol_Textfor visibility.
Source File Reference Guide
When extending these patterns, consult the following source files in the ocornut/imgui repository:
| File | Contents | Location |
|---|---|---|
imgui.h |
Public API declarations including ImGui::GetWindowDrawList() and InvisibleButton |
Line 1245 (approx.) |
imgui_internal.h |
ImDrawList class definition, helper flags, and inline utilities |
Line 858 (approx.) |
imgui_draw.cpp |
Full implementation of ImDrawList methods including AddRectFilled, PathArcTo, and PathStroke |
Line 392 (approx.) |
imgui_demo.cpp |
Complete working examples under "Custom rendering" section | Line 10171 (approx.) |
These files describe the complete pipeline from high-level API calls down to vertex buffer submission, providing the reference needed to implement any custom geometry.
Summary
- ImDrawList is screen-space – All coordinates are absolute screen positions; use
GetCursorScreenPos()andGetItemRectMin()to obtain base positions. - InvisibleButton bridges input and output – This function reserves layout space and tracks interaction states without rendering default widget graphics.
- Theme consistency comes from style variables – Use
ImGui::GetColorU32()with standardImGuiCol_*constants to respect user themes. - Clipping is automatic – The draw list automatically respects window clip rectangles, but you must handle layout positioning manually.
- Primitives range from simple to complex – Use
AddRectFilledandAddCirclefor basic shapes, orPathArcToandPathStrokefor custom curves.
Frequently Asked Questions
What is ImDrawList and how does it differ from ImGui's high-level API?
ImDrawList is the vertex buffer abstraction layer that sits below Dear ImGui's widget API. While high-level functions like Button() or SliderFloat() handle layout, styling, and rendering internally, ImDrawList exposes raw commands such as AddLine and AddRectFilled that write directly to the GPU-bound vertex buffers. This low-level access allows you to create visual elements that don't exist in the standard widget set, but requires you to manually handle positioning and interaction.
How do I handle input detection for custom widgets?
Always call ImGui::InvisibleButton() immediately after determining your widget's screen rectangle. This function creates an invisible hit area that integrates with ImGui's input system, enabling you to query states using IsItemHovered(), IsItemClicked(), and IsItemActive(). Place your drawing code after the invisible button call so that geometry renders on top of the interaction layer.
Why must I use InvisibleButton instead of Button for custom rendering?
Standard Button() calls render their own background, borders, and text using the active style, which would conflict with your custom draw-list geometry. InvisibleButton() performs the same layout allocation and input tracking without emitting any draw commands, giving you a clean canvas to render upon while maintaining full interaction capability.
How do I ensure my custom widgets respect ImGui's theming and styling?
Query color values using ImGui::GetColorU32(ImGuiCol_*) rather than hardcoding RGB values. For sizing, use CalcItemWidth() and GetTextLineHeight() to match standard widget dimensions. Retrieve padding and spacing values from ImGui::GetStyle() when calculating layouts, ensuring your custom widgets adapt automatically to style changes and DPI scaling.
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 →