How to Use ImGuiListClipper for Efficient List Rendering in Dear ImGui

ImGuiListClipper is a helper class that renders only visible items in scrolling regions, reducing O(N) iteration costs to O(V) for visible rows only.

The ImGuiListClipper class in the ocornut/imgui repository solves the performance bottleneck of rendering large datasets by skipping invisible items entirely. When displaying thousands or millions of rows, iterating through every entry to call ImGui functions creates unnecessary CPU overhead. This clipper leverages internal visibility calculations to process only the subset of items that intersect the current viewport.

Core Architecture and Source Files

The public API resides in imgui.h at lines 2894–2921, where the ImGuiListClipper struct declares the constructor, Begin(), Step(), End() methods, and the DisplayStart/DisplayEnd indices. Internal bookkeeping structures live in imgui_internal.h:

  • ImGuiListClipperRange (lines 1689–1699): Stores clipping ranges and seek offsets
  • ImGuiListClipperData (lines 1702–1712): Maintains step counters and range buffers

The algorithm implementation appears in imgui.cpp, specifically within the ImGuiListClipper::Begin, Step, and End method definitions.

How ImGuiListClipper Works Internally

The clipper operates through a three-phase state machine that caches visibility results across frames:

  1. Begin – Initialize with items_count (total rows) and optional items_height. Pass INT_MAX if the count is unknown.
  2. Step – Advance internal state and populate DisplayStart and DisplayEnd with the indices of rows intersecting the visible scrolling region. Returns true while work remains.
  3. End – Reset internal buffers, allowing clipper reuse. Called automatically by the destructor.

The clipper handles variable item heights by refining estimates after the first frame renders, and automatically synchronizes with table row counters unless ImGuiListClipperFlags_NoSetTableRowCounters is specified.

Basic Usage Pattern

Wrap your rendering loop inside the clipper's Step() iteration:

ImGuiListClipper clipper;
clipper.Begin(items_count);
while (clipper.Step())
{
    for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
    {
        // Heavy per-item work only executes for visible rows
        ImGui::Text("Item %d", i);
        ImGui::Text("Data: %s", large_dataset[i].label);
    }
}
clipper.End();  // Optional: destructor handles this

This pattern ensures expensive operations—string formatting, image loading, or complex widget calculations—execute only for approximately V visible rows rather than N total items.

Advanced Configuration Scenarios

Variable Item Heights

When rows have non-uniform heights, provide an average estimate to Begin():

// Second parameter is approximate height in pixels
clipper.Begin(items_count, 20.0f);

The clipper recalculates precise ranges after measuring actual rendered heights during the first frame.

Unknown Item Counts

For streaming data or lazily-generated lists, pass INT_MAX and break when data ends:

clipper.Begin(INT_MAX);
while (clipper.Step())
{
    for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
    {
        if (i >= live_data_buffer.size()) 
            break;
        ImGui::Text("Live item %d", i);
    }
}

Table Integration

Inside BeginTable calls, the clipper automatically calls TableNextRow() for each visible index:

if (ImGui::BeginTable("LargeTable", 3, ImGuiTableFlags_ScrollY))
{
    ImGuiListClipper clipper;
    clipper.Begin(row_count);
    while (clipper.Step())
    {
        for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++)
        {
            ImGui::TableNextRow();
            ImGui::TableSetColumnIndex(0);
            ImGui::Text("%d", row);
            ImGui::TableSetColumnIndex(1);
            ImGui::Text("%f", data[row].value);
            ImGui::TableSetColumnIndex(2);
            ImGui::Text("%s", data[row].name);
        }
    }
    ImGui::EndTable();
}

Disable automatic table row management by passing ImGuiListClipperFlags_NoSetTableRowCounters to Begin() if you need manual control over row advancement.

Summary

  • ImGuiListClipper lives in imgui.h and eliminates invisible item processing in large lists
  • Begin(), Step(), End() form the core API for driving the visibility algorithm
  • DisplayStart/DisplayEnd provide the index range for your rendering loop
  • Variable heights and tables receive automatic optimization with optional manual override flags
  • INT_MAX support enables streaming data scenarios without predetermined counts

Frequently Asked Questions

What is ImGuiListClipper and why is it necessary?

ImGuiListClipper is a CPU optimization utility that calculates which rows of a scrolling list are actually visible within the current window clipping rectangle. Without it, iterating through thousands of items to call ImGui functions—even for off-screen rows—wastes significant CPU cycles on invisible geometry that gets discarded during rendering.

How does ImGuiListClipper handle items with different heights?

The clipper accepts an estimated height parameter in Begin(). During the first frame, it uses this estimate to guess the visible range, then refines the calculation after ImGui measures actual item heights. Subsequent frames use the cached measurements for accurate positioning regardless of row-to-row height variation.

Can ImGuiListClipper work with Dear ImGui tables?

Yes. When used inside BeginTable/EndTable blocks, the clipper automatically invokes TableNextRow() for each visible index, maintaining proper table row counters and clip rectangles. Use ImGuiListClipperFlags_NoSetTableRowCounters only if your code manually manages row advancement via TableNextRow().

What happens if I don't know the total number of items?

Pass INT_MAX as the item count to Begin(). The clipper will continue emitting steps until Step() returns false or you break the loop internally. This supports use cases like log tailing or infinite scroll where data arrives continuously and the total size is unknown.

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 →