How to Serialize and Persist UI State with ImGuiStorage in Dear ImGui

ImGuiStorage is a per-window key-value container that stores int, float, and void values in a sorted vector, but because it lacks built-in persistence, you must manually iterate the Data vector, serialize the ImGuiID keys and values to disk, and reload them via SetInt(), SetFloat(), or SetVoidPtr() before the window is rendered.*

Dear ImGui (ocornut/imgui) provides the ImGuiStorage class as a lightweight state container attached to each window. While it survives across frames for the duration of the application session, it does not automatically write to disk. To persist UI state between application runs, you must implement custom serialization logic that reads from and writes to the Data vector defined in imgui.h.

Understanding ImGuiStorage Structure and Limitations

The Underlying Data Layout

ImGuiStorage is implemented as a sorted vector of ImGuiStoragePair structs. According to the source in imgui.h【/imgui.h#L2794-L2815】, each pair contains an ImGuiID key and a union holding either int val_i, float val_f, or void* val_p. The container exposes public access to this vector via ImGuiStorage::Data, which is an ImVector<ImGuiStoragePair>.

// From imgui.h - ImGuiStorage definition
struct ImGuiStorage
{
    ImVector<ImGuiStoragePair> Data;
    // ...
    void SetInt(ImGuiID key, int val);
    int GetInt(ImGuiID key, int default_val = 0) const;
    // ... float and void* variants
};

Because the storage uses a binary search algorithm to locate keys, maintaining the sorted order is critical when manually manipulating the Data vector.

Why ImGuiStorage Does Not Persist Automatically

The storage object lives inside ImGuiWindow and is accessed via ImGui::GetStateStorage() (declared in imgui.h【/imgui.h#L1047-1048】). Dear ImGui explicitly avoids file I/O dependencies in its core library. Therefore, the ImGuiStorage API provides no serialization hooks, and the Data vector is cleared when the window is destroyed or the application exits. Persistence requires external code to iterate storage->Data, extract the key-value pairs, and write them to a file format of your choice (binary, JSON, or INI).

How to Serialize ImGuiStorage to Disk

Step 1: Access the Storage Object

Retrieve the storage instance for the current window. You must call this between ImGui::Begin() and ImGui::End() for the specific window whose state you want to save.

ImGui::Begin("MyPersistentWindow");
ImGuiStorage* storage = ImGui::GetStateStorage();  // Returns storage for current window
// ... UI construction ...
ImGui::End();

Alternately, replace the storage entirely with a custom instance using ImGui::SetStateStorage(&my_storage) before calling ImGui::Begin().

Step 2: Iterate and Export Data

The Data member is a public vector you can iterate directly. Each entry contains the key and value, though you must track the value type yourself since the union does not store type information.

void SaveStorageToJSON(const char* filename, ImGuiStorage* storage)
{
    nlohmann::json j = nlohmann::json::array();
    
    for (int i = 0; i < storage->Data.Size; ++i)
    {
        const ImGuiStoragePair& p = storage->Data[i];
        // You must know the intended type; here we assume int for demonstration
        j.push_back({
            {"key", p.key},
            {"type", "int"},
            {"value", p.val_i}
        });
    }
    
    std::ofstream file(filename);
    file << j.dump(2);
}

Step 3: Handle Type Information

Because ImGuiStoragePair uses a union, the container does not store which union member is active. Your serialization scheme must encode the type (int, float, or pointer) alongside the value. Common approaches include using a naming convention for keys (e.g., prefixing with i_, f_, p_) or maintaining a side map of known keys to types.

Restoring UI State from Serialized Data

Clearing and Rebuilding the Storage

When loading, clear the existing Data vector to prevent duplicate entries, then repopulate using the type-safe setter methods. The setter functions (implemented in imgui.cpp【/imgui.cpp#L2120-2150】) handle the binary search and insertion logic automatically.

void LoadStorageFromJSON(const char* filename, ImGuiStorage* storage)
{
    std::ifstream file(filename);
    if (!file) return;
    
    nlohmann::json j;
    file >> j;
    
    storage->Data.clear();  // Remove existing entries
    
    for (const auto& elem : j)
    {
        ImGuiID key = elem["key"];
        std::string type = elem["type"];
        
        if (type == "int")
            storage->SetInt(key, elem["value"].get<int>());
        else if (type == "float")
            storage->SetFloat(key, elem["value"].get<float>());
        else if (type == "ptr")
            storage->SetVoidPtr(key, reinterpret_cast<void*>(elem["value"].get<uintptr_t>()));
    }
}

Ensuring Stable ImGuiID Generation

Critical: ImGuiID values are hashed from string labels and the ID stack via ImGui::GetID(). If your UI hierarchy changes between sessions (e.g., adding or removing widgets), the IDs will shift, causing persisted values to attach to the wrong controls. To maintain stability, always use explicit ID pushes (ImGui::PushID()) with static integers or hashes of constant strings, ensuring the same ID sequence is generated every run.

Practical Implementation Examples

JSON Serialization Save/Load

This complete example demonstrates saving window state on exit and restoring it on startup using the nlohmann/json library.

#include "imgui.h"
#include <fstream>
#include <nlohmann/json.hpp>

static void SaveImGuiState(const char* filename)
{
    ImGuiStorage* storage = ImGui::GetStateStorage();
    nlohmann::json j = nlohmann::json::array();

    for (int i = 0; i < storage->Data.Size; ++i) {
        const ImGuiStoragePair& p = storage->Data[i];
        // Determine type via convention: check if we stored this as int elsewhere
        j.push_back({ {"key", p.key}, {"type", "int"}, {"value", p.val_i} });
    }
    
    std::ofstream f(filename);
    f << j.dump(2);
}

static void LoadImGuiState(const char* filename)
{
    std::ifstream f(filename);
    if (!f) return;
    
    nlohmann::json j;
    f >> j;
    
    ImGuiStorage* storage = ImGui::GetStateStorage();
    storage->Data.clear();

    for (auto& e : j) {
        ImGuiID id = e["key"];
        std::string t = e["type"];
        if (t == "int") storage->SetInt(id, e["value"]);
        else if (t == "float") storage->SetFloat(id, e["value"]);
    }
}

// Usage in main loop:
// LoadImGuiState("ui_state.json");  // Before first ImGui::Begin
// ... render loop ...
// SaveImGuiState("ui_state.json");   // On application exit

Custom Global Storage Replacement

For state that must survive window destruction, allocate a persistent ImGuiStorage instance and assign it to the window context before rendering.

// Global or static member
static ImGuiStorage g_PersistentStorage;

// During initialization
ImGui::SetStateStorage(&g_PersistentStorage);
ImGui::Begin("MyWindow");
// All GetStateStorage() calls now return g_PersistentStorage
ImGui::End();

Persisting Selection Sets

ImGui uses storage internally to track selection states in widgets like Selectable. You can leverage this pattern for custom multi-selection persistence.

ImGuiStorage* sel = ImGui::GetStateStorage();

for (int i = 0; i < items.size(); i++) {
    ImGuiID id = ImGui::GetID(items[i].label.c_str());
    bool selected = sel->GetInt(id, 0) != 0;
    
    if (ImGui::Selectable(items[i].label.c_str(), selected)) {
        sel->SetInt(id, !selected);  // Toggle state
    }
}
// Serialize 'sel' using the JSON method above to persist selection across runs

Summary

  • ImGuiStorage stores per-window state as a sorted vector of ImGuiStoragePair in imgui.h, supporting only int, float, and void* types.
  • Access the active storage via ImGui::GetStateStorage() or replace it with ImGui::SetStateStorage() before calling ImGui::Begin().
  • Serialization requires manually iterating storage->Data, writing the ImGuiID keys and values to disk, and tracking type information externally since the union does not encode it.
  • Restoration involves clearing Data and repopulating via SetInt(), SetFloat(), or SetVoidPtr(), ensuring that ImGuiID generation remains stable between sessions.
  • According to the implementation in imgui.cpp【/imgui.cpp#L2120-2150】, the setter methods automatically handle binary search insertion to maintain the sorted invariant required for O(log n) lookups.

Frequently Asked Questions

Does ImGuiStorage support automatic serialization to imgui.ini?

No. While Dear ImGui writes window positions and sizes to the INI file automatically, it does not serialize ImGuiStorage contents. The ImGuiStorage class defined in imgui.h【/imgui.h#L2794-L2815】 contains no serialization methods, so you must implement custom save/load logic using the public Data vector.

How do I store complex data types in ImGuiStorage?

You cannot store complex types directly. ImGuiStorage only accepts int, float, and void* values. For complex data, store a pointer to your object in the void* slot using SetVoidPtr(), or hash your complex state into an integer representation. Ensure that the pointer remains valid for the duration of the storage's lifetime.

Why are my persisted values loading onto the wrong widgets?

This occurs when ImGuiID values change between sessions. Since IDs are generated by hashing the ID stack and string labels (via ImGui::GetID()), any modification to your UI hierarchy (adding, removing, or reordering widgets) shifts the IDs. Use explicit ImGui::PushID() calls with static integer constants or constant string hashes to guarantee stable IDs across application runs.

Can I share ImGuiStorage between multiple windows?

Yes. Create a persistent ImGuiStorage instance and call ImGui::SetStateStorage(&my_storage) immediately before each ImGui::Begin() call for the windows you want to link. According to imgui.h【/imgui.h#L1047-1048】, this replaces the window's default storage pointer with your custom one, causing those windows to share the same key-value space.

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 →