# How to Create Custom Widgets Using the ImDrawList Low-Level API in Dear ImGui

> Learn to build custom widgets in Dear ImGui by leveraging the ImDrawList API with InvisibleButton for input. Master low-level drawing for unique UI elements.

- Repository: [omar/imgui](https://github.com/ocornut/imgui)
- Tags: how-to-guide
- Published: 2026-07-15

---

**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`](https://github.com/ocornut/imgui/blob/main/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`](https://github.com/ocornut/imgui/blob/main/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`](https://github.com/ocornut/imgui/blob/main/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`](https://github.com/ocornut/imgui/blob/main/imgui_draw.cpp) starting around line 392. This file realizes the geometry pipeline used by all helper methods.

- **Reference implementation** – The [`imgui_demo.cpp`](https://github.com/ocornut/imgui/blob/main/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.

```cpp
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.

```cpp
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.

```cpp
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`](https://github.com/ocornut/imgui/blob/main/imgui.h) | Public API declarations including `ImGui::GetWindowDrawList()` and `InvisibleButton` | Line 1245 (approx.) |
| [`imgui_internal.h`](https://github.com/ocornut/imgui/blob/main/imgui_internal.h) | `ImDrawList` class definition, helper flags, and inline utilities | Line 858 (approx.) |
| [`imgui_draw.cpp`](https://github.com/ocornut/imgui/blob/main/imgui_draw.cpp) | Full implementation of `ImDrawList` methods including `AddRectFilled`, `PathArcTo`, and `PathStroke` | Line 392 (approx.) |
| [`imgui_demo.cpp`](https://github.com/ocornut/imgui/blob/main/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.