Column Visibility (Hide / Show) Implementation in Vanilla-Grid

This document explains how column visibility is implemented in Vanilla-Grid, including key normalization, hide eligibility rules, refresh behavior, persistence, and interactions with ordering/freeze.

File location (May 2026): the visibility logic lives in src/vanilla-grid/features/columns-visibility.feature.js as a prototype extension of VanillaGridColumnsFeature.


1. Feature Scope

Column visibility supports:

Public API:


2. Internal Data Model

Visibility uses two core collections:

  1. _allColumns
    • Source-of-truth list of all columns configured by host code (including hidden columns)
  2. _hiddenColumnKeys: Set<string>
    • Normalized keys of currently hidden data columns

this.columns is always the visible projection derived from _allColumns minus _hiddenColumnKeys.


3. Key Normalization and Selection Column Rules

All visibility operations normalize keys through:

_normalizeColumnKey(columnKey) => String key trimmed + lowercased

The selection checkbox column is protected by _isSelectionColumnKey(...) and is never hideable.

Consequences:


4. Initialization and Column-Set Flow

When setColumns(...) runs:

  1. Incoming columns are stored as _allColumns.
  2. Declarative hidden defaults are seeded: if localStorage has no persisted hidden-column state for this grid, columns with hidden: true (or hidden HTML attribute on <vn-grid-column>) are added to _hiddenColumnKeys. If persisted state exists, declarative defaults are ignored — the user's previous interactions take full priority.
  3. _pruneHiddenColumnKeys() removes stale hidden keys not present anymore.
  4. Hidden-key set is persisted.
  5. Visible projection is computed with _computeVisibleColumnsFromAll().
  6. Selection key mapping is updated.
  7. Saved order is applied.
  8. Freeze grouping is re-applied.
  9. Header/body layout is refreshed.

Declarative hidden columns

The hidden attribute on <vn-grid-column> sets the initial visibility state:

<vn-grid-column field="InternalId" header="ID" type="number" hidden="true"></vn-grid-column>

Or as a bare attribute (equivalent to hidden="true"):

<vn-grid-column field="InternalId" header="ID" type="number" hidden></vn-grid-column>

The column starts hidden but the user can reveal it through the context menu "Show all columns" action. Once the user interacts with visibility (any hide/show action), the state is persisted to localStorage, and subsequent page loads use the persisted state instead of the declarative default.

This sequence ensures visibility state remains stable across schema changes.


5. Visibility Projection

_computeVisibleColumnsFromAll() builds this.columns by filtering _allColumns:

So hidden state is represented as exclusion, not destructive mutation.

5.1 The second exclusion source: group-hidden columns

There are two independent exclusion sources, and they must not be conflated:

user hidden set group-hidden set
Owner _hiddenColumnKeys (this feature) VanillaGridGroupingFeature
Origin hideColumn() / a column's hidden: true derived from the applied group state
Persisted yes never
Public through getHiddenColumns() getGroupState()
Cleared by showAllColumns() / showColumn() ungroupColumn() / clearGrouping() only

An applied group level's column lives in its caption row, not in a column repeating one value on every member row, so it leaves the grid while grouping is applied. That could not have been implemented by writing into the user hidden set, for three independent reasons:

  1. hideColumn()/_canHideColumn() (§ 6) refuse a column that carries an active sort, is frozen, or would leave fewer than two visible data columns — all three would reject group removal for reasons that do not apply to it.
  2. The hidden set is persisted (§ 9) and publicly readable, so group-driven keys written into it would outlive the grouping that caused them — a column would stay hidden after clearGrouping(), or come back hidden on a later reload from a stale storage key.
  3. showAllColumns() clears that set wholesale, which would un-hide a grouped column and leave the two states disagreeing.

The columns feature loads before the grouping feature and holds no reference to it. It reads the group-hidden set through an injected getGroupHiddenColumnKeys closure (columnsConfig in vanilla-grid.js), the same pattern the grouping actions in the header menu already use. A null return means "grouping not loaded, or removing nothing" and leaves this function behaving exactly as it did before grouping existed — which is what keeps a partial bundle without grouping.feature.js working unchanged.

computeVisibleColumnsFromAll({ includeGroupHidden: true }) skips only the second source. It answers "which columns would be visible if nothing were grouped" — the denominator the grouping feature's own visible-column floor counts against — and must never be used to build the live column array.

See Row Grouping Implementation § 18 for the full removal design, including the sort transfer a grouped column's sort undergoes and the group bar that makes removal reversible.


6. Hide Eligibility Rules

_canHideColumn(columnKey) enforces business constraints:

  1. Key must normalize successfully.
  2. Selection column cannot be hidden.
  3. At least one visible data column must remain.
    • computed via _countVisibleDataColumns()
  4. Column must currently exist in this.columns.

If any condition fails, hide is rejected (false).


7. Hide / Show API Behavior

7.1 hideColumn(columnKey)

Flow:

  1. Normalize and validate key.
  2. Ensure _allColumns exists (rebuild from this.columns if necessary).
  3. Verify key exists in _allColumns.
  4. Reject if already hidden or not hideable.
  5. Add key to _hiddenColumnKeys.
  6. Remove same key from _frozenColumnKeys (hiding a frozen column auto-unfreezes it).
  7. Apply refresh pipeline via _applyColumnVisibilityAndRefresh().

7.2 showColumn(columnKey)

Flow:

  1. Normalize key.
  2. Reject if selection column or key not currently hidden.
  3. Reject if the key is group-hidden — a grouped column is not "hidden", it is "grouped", and showing it is ungroupColumn()'s job, not this one's.
  4. Delete key from _hiddenColumnKeys.
  5. Apply refresh pipeline.

7.3 showAllColumns()

Flow:

  1. If hidden set is empty, return false.
  2. Clear _hiddenColumnKeys.
  3. Apply refresh pipeline.

7.4 Query APIs


8. Refresh Pipeline (_applyColumnVisibilityAndRefresh)

After any hide/show mutation, Vanilla-Grid runs a full visibility-aware refresh:

  1. Compute nextColumns from _allColumns.
  2. Compare current and next key arrays; if identical, no-op (false).
  3. Close open header context menu.
  4. Replace this.columns = nextColumns.
  5. Reapply saved column order.
  6. Reapply freeze grouping.
  7. refreshHeaderLayout().
  8. Persist hidden keys.
  9. Rebuild/render body:
    • if empty, show empty/loading state and update scrollbar
    • else preserve scrollTop, init/reuse virtual pool, re-render rows, update scrollbar

The scroll-preservation step pre-sets spacer heights before render to avoid browser clamping scroll position near zero during pool rebuild.


9. Persistence Model

Control options:

9.1 Storage key

resolveHiddenColumnsStorageKey() default:

vanilla-grid:hidden-columns:{pathname}:{scope}

Where scope is first available of:

  1. gridId option (auto-populated from the <vanilla-grid> host element id)
  2. bodyTable.id
  3. viewport.id
  4. headerTable.id
  5. 'default'

9.2 Stored format

JSON array of keys:

["email", "weight"]

9.3 Save path

persistHiddenColumnsToStorage():

9.4 Load path

loadHiddenColumnsFromStorage():

If parse fails, it logs warning and resets to empty set.


10. Interaction with Other Features

10.1 Column order

Visibility refresh calls applySavedColumnOrder() after projection. This preserves user order among currently visible columns.

10.2 Frozen columns

10.3 Virtualization and scrollbars

Visibility changes can alter column count/width layout. The refresh path reinitializes pool/layout as needed and updates scrollbar thumb state.

10.4 Row grouping

Grouping contributes the second exclusion source described in § 5.1, and reuses _applyColumnVisibilityAndRefresh() (§ 8) verbatim as its re-layout path — it already sequences width save, column set, saved order, freeze reorder, sort remap, header re-layout, and a pool rebuild that preserves scrollTop. Because it is idempotent (a no-op returning false when the resulting key list is unchanged), grouping can call it unconditionally on every group/ungroup and on every suspend/resume transition. That reuse is also what makes the group→ungroup round trip restore the column at its original position, width and freeze state for free: none of that state was ever discarded, only filtered out of a derived array.

10.5 Header context menu

Context menu labels (hideColumn, showAllColumns) are localized via messages.* and menu is closed before applying visibility mutations.


11. Failure Safety


12. Example Configuration

const grid = new VanillaGrid({
  persistence: {
    hiddenColumns: { enabled: true, storageKey: 'my-grid:hidden:v1' }
  }
});

grid.hideColumn('email');
grid.showColumn('email');
grid.showAllColumns();

For session-only behavior:

persistence: { hiddenColumns: { enabled: false } }