Using ImGui's Table API for Complex Layouts: A Complete Technical Guide

ImGui's table API provides a state-driven grid system that replaces the deprecated Columns API with frozen panes, sortable headers, nested sub-tables, and column-spanning capabilities for building sophisticated UI layouts in Dear ImGui.

The modern table system introduced in Dear ImGui (ocornut/imgui) version 1.80 offers a declarative alternative to manual coordinate calculations when constructing data-heavy interfaces. This API handles automatic layout, clipping, scrolling, and interaction through an interface defined primarily in imgui.h, allowing developers to build complex layouts ranging from simple property grids to nested configuration panels with minimal boilerplate.

Core Table API Components

Table Initialization and Lifecycle

Every table begins with ImGui::BeginTable() and ends with ImGui::EndTable(). According to the source code in imgui.h (lines 886-907), BeginTable accepts a string ID, column count, optional flags for behavior customization, and sizing parameters:

bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, 
                const ImVec2& outer_size = ImVec2(0,0), float inner_width = 0.0f)

The function returns false when the table is clipped outside the visible region, enabling early-out optimizations. This check occurs before any column data is processed, making it efficient for large datasets.

Column Declaration and Configuration

Column properties are defined through TableSetupColumn(), as documented in imgui.h (lines 921-922):

void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, 
                      float init_width_or_weight = 0.0f, ImGuiID user_data = 0)

This function establishes resizing policies, default sorting behavior, and visibility states. When combined with TableHeadersRow() (lines 924-925), it automatically generates a header row with context menus for column reordering and sorting.

Row and Column Navigation

The API provides two navigation models. Sequential access uses TableNextRow() followed by TableNextColumn() (lines 908-910), where TableNextColumn() automatically wraps to the next row when the current row reaches its column limit. Random access uses TableSetColumnIndex(int column_n) (lines 910-911) to jump directly to specific columns, essential for irregular layouts where cells are skipped.

Advanced Layout Controls

For fixed headers and sidebars, TableSetupScrollFreeze(cols, rows) (lines 922-923) locks the first cols columns and rows rows during scrolling. Visual highlighting is achieved through TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n = -1) (lines 940-941), which supports cell, row, or column-level coloring.

Complex Layout Patterns

Nested Tables

Tables can embed sub-grids by calling BeginTable inside a cell. This pattern is useful for grouping related controls within a master-detail interface or creating hierarchical configuration panels without breaking the outer layout flow.

Column Spanning

Irregular grids are constructed by using TableSetColumnIndex() to skip columns, then drawing widgets that visually span the remaining space. A full-width button, for example, can occupy an entire row by drawing at column 0 with a width of -FLT_MIN and never visiting subsequent columns in that row.

Frozen Panes

Data-heavy applications use TableSetupScrollFreeze to lock header rows or identity columns in place while scrolling through large datasets. This creates Excel-like behavior where context remains visible during navigation.

Variable Row Heights

While tables default to uniform row heights, passing a non-zero min_row_height to TableNextRow() or using TableSetBgColor with expanded vertical padding forces specific rows to consume additional vertical space.

Custom Header Widgets

After calling TableHeadersRow(), developers can navigate back to header columns using TableSetColumnIndex() and render interactive widgets like combo boxes or filter inputs instead of static text labels.

Practical Implementation Examples

Sortable Data Grid with Scrolling

This example demonstrates a typical data grid with frozen headers, sortable columns, and alternating row backgrounds:

if (ImGui::BeginTable("##DemoTable", 4,
        ImGuiTableFlags_Resizable |
        ImGuiTableFlags_Reorderable |
        ImGuiTableFlags_Sortable |
        ImGuiTableFlags_RowBg |
        ImGuiTableFlags_Borders |
        ImGuiTableFlags_ScrollY,
        ImVec2(0, ImGui::GetContentRegionAvail().y))) {
    
    ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
    ImGui::TableSetupColumn("Health", ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("Score", ImGuiTableColumnFlags_WidthFixed);
    ImGui::TableSetupColumn("Active", ImGuiTableColumnFlags_NoSort);
    ImGui::TableHeadersRow();

    for (int i = 0; i < 30; ++i) {
        ImGui::TableNextRow();
        ImGui::TableNextColumn(); ImGui::Text("Player %d", i);
        ImGui::TableNextColumn(); ImGui::ProgressBar(0.4f + i*0.02f);
        ImGui::TableNextColumn(); ImGui::Text("%d", 1000 - i*10);
        ImGui::TableNextColumn(); ImGui::Checkbox("##active", &playerActive[i]);
    }
    ImGui::EndTable();
}

Nested Configuration Panels

This pattern embeds a sub-table within an outer table cell to create grouped settings:

if (ImGui::BeginTable("##Outer", 2, ImGuiTableFlags_Borders)) {
    ImGui::TableNextRow();
    ImGui::TableNextColumn(); ImGui::Text("General Settings");

    ImGui::TableNextColumn();
    if (ImGui::BeginTable("##Inner", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) {
        ImGui::TableSetupColumn("Option");
        ImGui::TableSetupColumn("Value");
        ImGui::TableHeadersRow();

        ImGui::TableNextRow(); 
        ImGui::TableNextColumn(); ImGui::Text("VSync");   
        ImGui::TableNextColumn(); ImGui::Checkbox("##vsync", &vsync);
        
        ImGui::TableNextRow(); 
        ImGui::TableNextColumn(); ImGui::Text("FPS Cap"); 
        ImGui::TableNextColumn(); ImGui::SliderInt("##fps", &fpsCap, 30, 240);
        
        ImGui::EndTable();
    }
    ImGui::EndTable();
}

Column Spanning and Interactive Headers

This example combines a full-width spanning button with a custom combo box header:

if (ImGui::BeginTable("##SpanDemo", 3, ImGuiTableFlags_Borders)) {
    ImGui::TableSetupColumn("ID");
    ImGui::TableSetupColumn("Mode");
    ImGui::TableSetupColumn("Action");
    ImGui::TableHeadersRow();

    // Replace header of column 1 with a combo box
    ImGui::TableNextColumn(); // Skip ID header
    ImGui::TableNextColumn(); // Position in Mode header
    ImGui::SetNextItemWidth(-FLT_MIN);
    ImGui::Combo("##mode", &modeIdx, "Auto\0Manual\0");
    ImGui::TableNextColumn(); // Skip Action header

    // Full-width spanning row
    ImGui::TableNextRow();
    ImGui::TableSetColumnIndex(0);
    ImGui::Button("Full-Width Action", ImVec2(-FLT_MIN, 0));
    // Skip remaining columns to complete the row span

    // Standard rows
    for (int i = 0; i < 5; ++i) {
        ImGui::TableNextRow();
        ImGui::TableNextColumn(); ImGui::Text("Item %d", i);
        ImGui::TableNextColumn(); ImGui::Text("%s", (modeIdx == 0) ? "Auto" : "Manual");
        ImGui::TableNextColumn(); ImGui::Button("Run");
    }
    ImGui::EndTable();
}

Internal Architecture

The table system maintains persistent state in the ImGuiTable structure (defined in imgui_internal.h, lines 3055-3119), which stores column definitions, sort specifications, and visibility flags. Each column is represented by ImGuiTableColumn (lines 2940-2990), containing flags, display order, and sizing weights.

Per-frame transient data lives in ImGuiTableTempData, allocated on the stack and reused across frames. This design makes iteration cheap, as the system only stores a compact description while the actual drawing is deferred to the ImDrawList during EndTable(). The implementation logic resides primarily in imgui_tables.cpp, with comprehensive usage examples available in imgui_demo.cpp.

Summary

  • BeginTable returns a boolean indicating visibility, allowing early-out for clipped tables
  • TableSetupColumn defines column behavior including sorting, resizing, and initial width policies
  • TableSetColumnIndex enables irregular layouts by allowing random access to column positions
  • TableSetupScrollFreeze creates frozen panes for headers and sidebar columns
  • Nested tables are supported by calling BeginTable within any table cell
  • The API is state-driven and requires calling functions in the order: setup, headers, rows/cells, end

Frequently Asked Questions

When should I use the Table API instead of the old Columns API?

The Columns API is deprecated as of version 1.80. You should always use the Table API for new code, as it provides superior performance, better scrolling support, and features like sorting and freezing that the old system cannot replicate. The Table API also handles clipping and navigation automatically.

How do I create cells that span multiple columns?

Use TableSetColumnIndex(0) to position at the first column, draw your widget with a full-width flag like ImVec2(-FLT_MIN, 0), then simply do not call TableNextColumn() for the remaining columns in that row. The table cursor advances to the next row when you call TableNextRow(), effectively creating a spanning cell that occupies the entire row width.

Are nested tables expensive in terms of performance?

No. Because table data uses transient stack allocation via ImGuiTableTempData and only persists the minimal ImGuiTable structure between frames, nested tables are inexpensive. The system does not recursively allocate large buffers; instead, it manages a compact description that the draw list renders efficiently during EndTable().

How do I implement column sorting in my table?

Enable sorting by including ImGuiTableFlags_Sortable in your BeginTable flags and ImGuiTableColumnFlags_DefaultSort in TableSetupColumn for sortable columns. After calling TableHeadersRow(), check ImGui::TableGetSortSpecs() to retrieve sort specifications, then reorder your data array accordingly before populating rows. The API handles the sort indicator UI automatically.

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 →