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 the ImDrawList* associated with the current window. Defined in imgui.h around line 1245, this function provides the pointer you feed drawing commands to.

  • ImDrawList class definition – Stores vertex buffers, index buffers, and manages texture and clipping state. Found in imgui_internal.h around line 858, this class declares primitive helpers like AddLine, AddRectFilled, and PathArcTo.

  • ImDrawList implementation – The heavy lifting of buffer allocation, command merging, and clipping occurs in imgui_draw.cpp starting around line 392. This file realizes the geometry pipeline used by all helper methods.

  • Reference implementation – The imgui_demo.cpp file 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:

  1. Determine the widget's screen rectangle – Use ImGui::GetCursorScreenPos() to retrieve the top-left corner, then calculate the bounding box using your desired size.

  2. Reserve interaction space – Call ImGui::InvisibleButton("##widget_id", size) so ImGui tracks hover, click, and drag states for your widget area.

  3. Obtain the draw list – Retrieve the current draw list via ImDrawList* draw = ImGui::GetWindowDrawList();.

  4. Add geometry – Use methods like draw->AddRectFilled, draw->AddCircle, or draw->PathArcTo to render the visual representation within your calculated rectangle.

  5. Query interaction states – After the invisible button call, check ImGui::IsItemHovered(), ImGui::IsItemClicked(), or ImGui::IsItemActive() to drive widget state.

  6. 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:

  • InvisibleButton provides hover and click states without rendering default button graphics.
  • ImDrawList::AddRectFilled draws the background using theme colors (ImGuiCol_Button and ImGuiCol_ButtonActive).
  • GetItemRectMin and GetItemRectMax provide 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 PathArcTo combined with PathStroke to draw a partial circle arc.
  • Horizontal mouse dragging updates the value using ImGui::GetIO().MouseDelta.
  • Colors are pulled from ImGuiCol_SliderGrab and 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 values array.
  • CalcItemWidth ensures the bar matches standard ImGui item sizing when no explicit size is provided.
  • Hover state draws an additional highlight rectangle using ImGuiCol_Text for 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() and GetItemRectMin() 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 standard ImGuiCol_* 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 AddRectFilled and AddCircle for basic shapes, or PathArcTo and PathStroke for 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:

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 →