ImGui Tables and Grids Features: Complete API and Implementation Guide

Dear ImGui's table system provides high-performance data grids with built-in sorting, resizing, column freezing, and clipping through a BeginTable/EndTable API that supports thousands of rows via ImGuiListClipper.

The ocornut/imgui repository implements a sophisticated table and grid system in imgui_tables.cpp that handles complex layout policies, interactive headers, and optimized rendering for large datasets. This article examines the actual source implementation to explain how ImGui tables manage column sizing, scrolling, and state persistence while maintaining interactive frame rates.

Table Lifecycle and Core API

The table system follows a strict lifecycle defined in imgui_tables.cpp. Understanding this flow is essential for correctly implementing data grids.

Initialization and Setup

A table begins with ImGui::BeginTable, which internally calls BeginTableEx (lines 14-22). This function creates a table instance, determines whether a child window is required for scrolling, and initializes the table flags.

Column registration happens via TableSetupColumn (lines 62-65), where you specify width policies and behavior flags:

ImGui::BeginTable("MyTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders);
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 50.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 80.0f);

Layout Computation

The first call to TableNextRow or TableHeadersRow triggers TableUpdateLayout (lines 350-380). This critical function computes:

  • Final column widths based on sizing policies
  • Display order and clipping rectangles
  • Draw channel assignments

Row Submission

Data population occurs through TableNextRow and TableNextColumn (lines 640-670). The API automatically accumulates row height from the tallest cell. TableNextColumn returns a boolean indicating whether the column is visible, allowing you to skip expensive widget creation for clipped columns.

The table finalizes with EndTable (lines 900-940), which merges draw channels, draws borders, and performs garbage collection.

Column Sizing Policies and Layout Control

ImGui tables support two orthogonal sizing concepts controlled through TableSetupColumn flags and the TableFixFlags helper (lines 75-85).

Width Policies

WidthFixed: The column uses a specific pixel width. If you specify 0.0f, the width automatically fits the content.

WidthStretch: The column receives a weight value and occupies a proportional share of remaining horizontal space.

You can mix fixed and stretch columns in the same table. A common pattern uses fixed widths for leading columns (like IDs) and stretch policies for trailing descriptive columns to fill remaining space.

Interaction Flags

  • NoResize: Prevents users from dragging column boundaries
  • NoReorder: Disables drag-and-drop column reordering

When horizontal scrolling is active, you must supply a positive inner_width parameter to give stretch columns a meaningful reference size (see comments around lines 15-20).

Scrolling, Freezing, and Performance Optimization

ImGui tables handle large datasets through sophisticated clipping mechanisms and optional scrolling behaviors.

Scrolling and Freezing

The ImGuiTableFlags_ScrollX and ImGuiTableFlags_ScrollY flags create an internal child window for scrolling. Combine these with freezing flags to keep headers and key columns visible:

ImGuiTableFlags flags = ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY |
                        ImGuiTableFlags_FreezeTopRow | 
                        ImGuiTableFlags_FreezeLeftColumn;

Clipping and Culling

For tables with thousands of rows, use ImGuiListClipper (demonstrated in the "Tables → Vertical Scrolling" demo) to submit only visible rows. This technique intersects row rectangles with the clip rect (section "Tables Clipping/Culling", lines 158-165).

Additionally, TableNextColumn returns false when a column is fully clipped (lines 66-74), allowing you to skip rendering for hidden columns entirely. The implementation distinguishes three states: visible, clipped, and hidden (lines 166-180).

Sorting and Header Interactions

When ImGuiTableFlags_Sortable is enabled, ImGui manages sort state automatically through the TableSortSpecsClickColumn function (lines 58-60).

Sort Specifications

Clicking a header triggers the sort spec update, which populates the internal ImGuiTableSortSpecs array. Retrieve the current specs via TableGetSortSpecs to perform custom sorting in your application code:

ImGuiTableSortSpecs* sorts = ImGui::TableGetSortSpecs();
if (sorts && sorts->SpecsDirty) {
    // Apply sorts->Specs[0].ColumnIndex and sorts->Specs[0].SortDirection
    // to your data source here
    sorts->SpecsDirty = false;
}

Context Menus

The default header context menu (accessible via right-click) is built by TableDrawDefaultContextMenu (lines 55-56). Override this by calling TableOpenContextMenu before TableHeadersRow if you need custom menu items.

Drawing the Grid and Borders

Border rendering occurs in TableDrawBorders (lines 710-770) using ImDrawListSplitter to manage three distinct draw channels:

  1. Channel 0: Background and row strips
  2. Channel 1: Frozen column overlays when TableSetupScrollFreeze is active
  3. Channel 2: No-clip overlays when ImGuiTableFlags_NoClip is used

Border colors derive from the current style (ImGuiCol_TableBorderStrong and ImGuiCol_TableBorderLight) initialized during table setup (lines 446-448).

State Persistence

Tables automatically persist user modifications to the .ini file unless you specify ImGuiTableFlags_NoSavedSettings. The functions TableLoadSettings and TableSaveSettings (called from BeginTableEx and EndTable) handle serialization of:

  • Column order and visibility
  • User-resized widths
  • Sort specifications

Complete Working Example

This implementation demonstrates fixed and stretch columns, sorting, freezing, and vertical clipping for large datasets:

// Create table with scrolling, sorting, and resize capabilities
if (ImGui::BeginTable("DemoTable", 5,
    ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
    ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollX | 
    ImGuiTableFlags_ScrollY | ImGuiTableFlags_FreezeTopRow |
    ImGuiTableFlags_FreezeLeftColumn))
{
    // Configure columns: Fixed ID, stretching Name, fixed Value, auto Status, stretching Remarks
    ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 50.0f);
    ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort, 0.0f);
    ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 80.0f);
    ImGui::TableSetupColumn("Status", 0, 0.0f);
    ImGui::TableSetupColumn("Remarks", ImGuiTableColumnFlags_WidthStretch, 0.0f);
    ImGui::TableHeadersRow(); // Draws headers and enables sorting

    // Handle sorting logic
    ImGuiTableSortSpecs* sorts = ImGui::TableGetSortSpecs();
    if (sorts && sorts->SpecsDirty) {
        // Sort your data based on sorts->Specs array
        sorts->SpecsDirty = false;
    }

    // Clipper for handling 1000+ rows efficiently
    ImGuiListClipper clipper;
    clipper.Begin(1000);
    while (clipper.Step()) {
        for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) {
            ImGui::TableNextRow();
            
            // Column 0: ID
            ImGui::TableNextColumn();
            ImGui::Text("%d", row);
            
            // Column 1: Name (skip if clipped)
            if (ImGui::TableNextColumn()) {
                ImGui::Text("Item %03d", row);
            }
            
            // Column 2: Interactive widget
            ImGui::TableNextColumn();
            ImGui::SliderFloat("##val", &values[row], 0.0f, 1.0f);
            
            // Column 3: Status
            ImGui::TableNextColumn();
            ImGui::Text("%s", (row % 2) ? "OK" : "FAIL");
            
            // Column 4: Remarks
            ImGui::TableNextColumn();
            ImGui::TextUnformatted("Lorem ipsum dolor sit amet");
        }
    }
    ImGui::EndTable();
}

Summary

  • Core API: Tables use BeginTable/EndTable with TableSetupColumn for configuration, processed by TableUpdateLayout before the first row submission.
  • Sizing: Mix WidthFixed and WidthStretch policies to create responsive layouts; provide inner_width when horizontal scrolling is enabled.
  • Performance: Utilize ImGuiListClipper for vertical scrolling with many rows, and check TableNextColumn return values to skip hidden columns.
  • Interactivity: Enable Sortable, Resizable, and Reorderable flags for full UI control; access sort specs via TableGetSortSpecs.
  • Rendering: Borders draw via three-channel splitters in TableDrawBorders, with colors defined in ImGuiCol_TableBorderStrong and ImGuiCol_TableBorderLight.
  • Persistence: Column state automatically saves to .ini files via TableSaveSettings unless NoSavedSettings is specified.

Frequently Asked Questions

How do I enable horizontal scrolling in ImGui tables?

Specify the ImGuiTableFlags_ScrollX flag in BeginTable. When using horizontal scrolling with stretch columns, you must provide a positive inner_width parameter to give the stretch algorithm a reference size. This creates a child window internally that manages the scrollable region.

What is the difference between WidthFixed and WidthStretch columns?

WidthFixed columns maintain a constant pixel width (or auto-fit if set to 0.0f), while WidthStretch columns dynamically share remaining horizontal space proportionally. Fixed columns are ideal for data like IDs or dates that require consistent width, whereas stretch columns work best for content that should expand to fill available space, such as descriptions or comments.

How does sorting work in ImGui tables?

When ImGuiTableFlags_Sortable is enabled, clicking column headers triggers TableSortSpecsClickColumn, which populates an ImGuiTableSortSpecs structure. Call TableGetSortSpecs to retrieve the current sort criteria, apply the sorting to your data array when SpecsDirty is true, then set SpecsDirty to false. The table does not sort your data automatically; it only tracks which columns the user wants sorted and in which direction.

How do I optimize performance for tables with thousands of rows?

Use ImGuiListClipper to render only visible rows (as shown in imgui_demo.cpp). Additionally, check the boolean return value of TableNextColumn—it returns false for fully clipped columns, allowing you to skip expensive widget creation for off-screen cells. These techniques, combined with the internal clipping rectangles calculated in TableUpdateLayout, ensure smooth frame rates even with large datasets.

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 →