How to Create Multi-Column Layouts with Sorting and Resizing Using ImGui Tables

To create multi-column layouts with sorting and resizing in ImGui, call ImGui::BeginTable() with ImGuiTableFlags_Resizable and ImGuiTableFlags_Sortable, declare columns using ImGui::TableSetupColumn(), emit headers with ImGui::TableHeadersRow(), and retrieve sort specifications via ImGui::TableGetSortSpecs() to reorder your underlying data.

The Dear ImGui library (ocornut/imgui) implements a self-contained Tables API in imgui_tables.cpp that supports complex multi-column layouts with sorting and resizing through a persistent ImGuiTable instance stored in the global context. The public interface lives in imgui.h, providing everything needed to create interactive data grids without external dependencies.

Understanding the Tables API Architecture

ImGui handles tables through a single module architecture. The heavy lifting occurs in imgui_tables.cpp, while the public-facing functions are declared in imgui.h. When you create a table, the system allocates (or reuses) an ImGuiTable instance stored in GImGui, which persists across frames to maintain state such as column widths, display order, and sort specifications without requiring manual storage in your application code.

Essential Flags for Resizable and Sortable Tables

The table behavior is controlled by flags passed to BeginTable():

  • ImGuiTableFlags_Resizable – Enables dragging column borders to resize. Internally, resize requests are queued and applied via TableApplyQueuedRequests() around lines 78-90 in imgui_tables.cpp, which calls TableSetColumnWidth() to update WidthAuto and recalculate stretch weights.
  • ImGuiTableFlags_Sortable – Allows clicking headers to toggle sort order. Click handling occurs in TableSortSpecsClickColumn() (around lines 58-60), which updates the ImGuiTableSortSpec array and marks the table for resorting.
  • ImGuiTableFlags_Reorderable – Permits dragging headers to change column order. The request is stored in TableQueueSetColumnDisplayOrder() and applied later by TableSetColumnDisplayOrder() (lines 777-785).
  • ImGuiTableFlags_ScrollX / ScrollY – Creates an internal scrolling child window within BeginTableEx() (lines 40-46), essential when total column width exceeds the available outer size.

Step-by-Step Table Creation Flow

Step 1: Initialize with BeginTable

Call ImGui::BeginTable(const char* id, int columns, ImGuiTableFlags flags, ImVec2 outer_size = ImVec2(0,0), float inner_width = 0) to allocate the table instance and determine sizing policies through TableFixFlags(). An outer size of (0,0) allows the table to fill remaining window space.

Step 2: Declare Columns with TableSetupColumn

Define each column using ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0, ImGuiID user_data = 0). This must be called before the first row (enforced by an assertion in the implementation) to store per-column metadata including width policy (WidthFixed or WidthStretch) and optional user IDs for sorting.

Step 3: Emit Headers with TableHeadersRow

Invoke ImGui::TableHeadersRow() to iterate over declared columns and render header cells. This function automatically registers click and resize interactions. When ImGuiTableFlags_Sortable is active, left-clicks trigger TableSortSpecsClickColumn() to update sort direction.

Step 4: Fill Rows and Cells

Use ImGui::TableNextRow() to create a new row and ImGui::TableNextColumn() to advance to the next cell. Row height is computed from the tallest visible cell, ensuring consistent alignment even with hidden columns.

Step 5: Finalize with EndTable

Call ImGui::EndTable() to complete drawing, merge draw channels, and persist settings to the .ini file if enabled.

Implementing Sort Logic

After TableHeadersRow(), retrieve the current sort specifications:

const ImGuiTableSortSpecs* sorts = ImGui::TableGetSortSpecs();
if (sorts && sorts->SpecsCount > 0)
{
    for (int n = 0; n < sorts->SpecsCount; n++)
    {
        const ImGuiTableSortSpec* spec = &sorts->Specs[n];
        // spec->ColumnIndex, spec->SortDirection, spec->ColumnUserID
    }
}

The ImGuiTableSortSpec struct contains ColumnIndex, SortOrder (for multi-column sorts), SortDirection (ascending/descending), and ColumnUserID (if specified in TableSetupColumn()). Your application reads this data and reorders its data source accordingly.

Managing Resizing and Reordering Internally

When a user drags a column border, ImGui records ResizedColumn and ResizedColumnNextWidth. On the subsequent frame, TableApplyQueuedRequests() (called from BeginTableEx) invokes TableSetColumnWidth() to apply the new width. For reordering, drag operations queue via TableQueueSetColumnDisplayOrder() and are processed by TableSetColumnDisplayOrder() to update the display indices persistently.

Complete Implementation Examples

Example 1: Basic Resizable, Sortable, and Reorderable Table

if (ImGui::BeginTable("my_table", 3,
                     ImGuiTableFlags_Resizable |
                     ImGuiTableFlags_Sortable |
                     ImGuiTableFlags_Reorderable |
                     ImGuiTableFlags_Borders |
                     ImGuiTableFlags_RowBg))
{
    // Define columns: Fixed width for ID, stretch for others
    ImGui::TableSetupColumn("ID",    ImGuiTableColumnFlags_WidthFixed,   80.0f);
    ImGui::TableSetupColumn("Name",  ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("Score", ImGuiTableColumnFlags_WidthStretch);
    
    ImGui::TableHeadersRow();

    struct Item { int id; const char* name; int score; };
    static Item items[] = { {1,"Alice", 95}, {2,"Bob", 78}, {3,"Carol", 88} };

    // Handle sorting
    const ImGuiTableSortSpecs* sorts = ImGui::TableGetSortSpecs();
    if (sorts && sorts->SpecsCount > 0)
    {
        const ImGuiTableSortSpec* spec = sorts->Specs;
        std::stable_sort(std::begin(items), std::end(items),
            [spec](const Item& a, const Item& b)
            {
                if (spec->ColumnIndex == 0)
                    return (spec->SortDirection == ImGuiSortDirection_Ascending) ? 
                           a.id < b.id : a.id > b.id;
                else if (spec->ColumnIndex == 2)
                    return (spec->SortDirection == ImGuiSortDirection_Ascending) ? 
                           a.score < b.score : a.score > b.score;
                return false;
            });
    }

    // Populate rows
    for (const Item& it : items)
    {
        ImGui::TableNextRow();
        ImGui::TableNextColumn(); ImGui::Text("%d", it.id);
        ImGui::TableNextColumn(); ImGui::TextUnformatted(it.name);
        ImGui::TableNextColumn(); ImGui::Text("%d", it.score);
    }
    ImGui::EndTable();
}

Example 2: Scrollable Table with Frozen Columns

if (ImGui::BeginTable("scrollable_table", 5,
                     ImGuiTableFlags_Resizable |
                     ImGuiTableFlags_Sortable |
                     ImGuiTableFlags_ScrollX |
                     ImGuiTableFlags_ScrollY |
                     ImGuiTableFlags_FreezeFirstColumn |
                     ImGuiTableFlags_Borders))
{
    // Freeze first 2 columns horizontally
    ImGui::TableSetupScrollFreeze(2, 0);

    ImGui::TableSetupColumn("Idx",     ImGuiTableColumnFlags_WidthFixed,   50);
    ImGui::TableSetupColumn("Name",    ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("Age",     ImGuiTableColumnFlags_WidthFixed,   40);
    ImGui::TableSetupColumn("Country", ImGuiTableColumnFlags_WidthStretch);
    ImGui::TableSetupColumn("Score",   ImGuiTableColumnFlags_WidthFixed,   60);

    ImGui::TableHeadersRow();

    for (int n = 0; n < 200; n++)
    {
        ImGui::TableNextRow();
        ImGui::TableNextColumn(); ImGui::Text("%d", n);
        ImGui::TableNextColumn(); ImGui::Text("Player %d", n);
        ImGui::TableNextColumn(); ImGui::Text("%d", 20 + n % 30);
        ImGui::TableNextColumn(); ImGui::Text("Country %d", n % 5);
        ImGui::TableNextColumn(); ImGui::Text("%d", rand() % 100);
    }
    ImGui::EndTable();
}

Example 3: Sorting with Custom User IDs

if (ImGui::BeginTable("custom_id_table", 2,
                     ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable))
{
    // Assign stable IDs to identify columns by logical name rather than index
    ImGui::TableSetupColumn("File",      ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)1);
    ImGui::TableSetupColumn("Size (KB)", ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)2);
    ImGui::TableHeadersRow();

    const ImGuiTableSortSpecs* sort = ImGui::TableGetSortSpecs();
    if (sort && sort->SpecsCount > 0)
    {
        const ImGuiTableSortSpec* spec = sort->Specs;
        // Use spec->ColumnUserID (1 or 2) to determine sort logic
        // rather than relying on spec->ColumnIndex
    }

    // ... fill rows ...
    ImGui::EndTable();
}

Key Source Files for Reference

File Purpose Location
imgui.h Public API declarations for BeginTable, TableSetupColumn, TableHeadersRow, and TableGetSortSpecs imgui.h
imgui_tables.cpp Core implementation including TableApplyQueuedRequests, TableSortSpecsClickColumn, and TableSetColumnDisplayOrder imgui_tables.cpp
imgui_demo.cpp Real-world usage examples demonstrating column setup, resizing policies, and sorting implementations imgui_demo.cpp

Summary

  • Use ImGuiTableFlags_Resizable and ImGuiTableFlags_Sortable in BeginTable() to enable interactive column features.
  • Call TableSetupColumn() before the first row to define width policies (WidthFixed vs WidthStretch) and optional user IDs.
  • Invoke TableHeadersRow() to generate clickable headers that automatically handle sort direction toggles and resize interactions.
  • Access ImGui::TableGetSortSpecs() after drawing headers to retrieve an array of ImGuiTableSortSpec containing ColumnIndex, SortDirection, and ColumnUserID.
  • Resize and reorder requests are queued internally in TableApplyQueuedRequests() and applied on the next frame via functions like TableSetColumnWidth() and TableSetColumnDisplayOrder().
  • The ImGuiTable instance persists across frames in the global context, automatically preserving column widths, order, and sort state.

Frequently Asked Questions

How do I disable resizing for specific columns only?

Pass ImGuiTableColumnFlags_NoResize to TableSetupColumn() for that specific column while keeping ImGuiTableFlags_Resizable on the table. This allows most columns to be resized while freezing the width of critical columns.

Can I sort by multiple columns simultaneously?

Yes. When multiple sort criteria are active, TableGetSortSpecs() returns an array where SpecsCount indicates how many columns are involved in the sort. Iterate through all ImGuiTableSortSpec entries, each providing ColumnIndex, SortOrder (priority), and SortDirection to implement multi-level sorting in your comparison function.

How do I persist column widths between application restarts?

ImGui automatically saves table settings including column widths to the .ini file when ImGuiTableFlags_SaveSettings is enabled (default in most configurations), provided io.IniFilename is set. The settings are loaded automatically on startup and saved when the context shuts down or when SaveIniSettingsToDisk() is called.

Why must TableSetupColumn be called before the first row?

The implementation in imgui_tables.cpp enforces this via an assertion because column metadata must be fully initialized before BeginTableEx() calculates the initial layout and before TableHeadersRow() iterates over the column array to render headers and register interaction hit boxes.

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 →