Build a dynamic column visibility toggle
In this tutorial, you will build a checkbox list outside the grid that shows or hides columns on demand. You will learn how to use updateSettings to update the columns array while preserving each column’s type, renderer, and validator configuration.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
registerAllModules();
/* start:skip-in-preview */const data = [ { name: 'Alice Johnson', department: 'Engineering', role: 'Senior Engineer', salary: 95000, startDate: '2019-03-12', location: 'New York', status: 'Active' }, { name: 'Bob Martinez', department: 'Marketing', role: 'Marketing Manager', salary: 78000, startDate: '2020-07-01', location: 'Chicago', status: 'Active' }, { name: 'Carol Lee', department: 'Engineering', role: 'Tech Lead', salary: 115000, startDate: '2017-11-15', location: 'San Francisco', status: 'Active' }, { name: 'David Kim', department: 'HR', role: 'HR Specialist', salary: 65000, startDate: '2021-02-28', location: 'Austin', status: 'On Leave' }, { name: 'Eva Novak', department: 'Finance', role: 'Financial Analyst', salary: 82000, startDate: '2018-09-03', location: 'New York', status: 'Active' }, { name: 'Frank Chen', department: 'Engineering', role: 'Junior Engineer', salary: 72000, startDate: '2022-05-16', location: 'Seattle', status: 'Active' }, { name: 'Grace Okafor', department: 'Sales', role: 'Sales Executive', salary: 70000, startDate: '2020-01-20', location: 'Dallas', status: 'Active' }, { name: 'Henry Walsh', department: 'Finance', role: 'Finance Director', salary: 130000, startDate: '2015-06-10', location: 'Chicago', status: 'Active' },];/* end:skip-in-preview */
// The full columns config is the immutable source of truth.// Never mutate this array -- always derive a visible subset from it.const allColumns = [ { data: 'name', title: 'Name', type: 'text', width: 140 }, { data: 'department', title: 'Department', type: 'text', width: 120 }, { data: 'role', title: 'Role', type: 'text', width: 150 }, { data: 'salary', title: 'Salary', type: 'numeric', numericFormat: { pattern: '$0,0', culture: 'en-US' }, width: 110, }, { data: 'startDate', title: 'Start Date', type: 'date', dateFormat: 'YYYY-MM-DD', width: 110 }, { data: 'location', title: 'Location', type: 'text', width: 110 }, { data: 'status', title: 'Status', type: 'dropdown', source: ['Active', 'On Leave', 'Inactive'], width: 100, },];
// Track which column indices (into allColumns) are currently visible.// Start with all columns visible.const visibleIndices = new Set(allColumns.map((_, i) => i));
// Returns only the column configs that are currently visible.function getVisibleColumns() { return allColumns.filter((_, i) => visibleIndices.has(i));}
// Returns only the column headers that are currently visible.function getVisibleHeaders() { return allColumns.filter((_, i) => visibleIndices.has(i)).map(col => col.title);}
const container = document.querySelector('#example1');
const hot = new Handsontable(container, { data, columns: getVisibleColumns(), colHeaders: getVisibleHeaders(), rowHeaders: true, height: 'auto', width: '100%', autoWrapRow: true, licenseKey: 'non-commercial-and-evaluation',});
// Build a checkbox for each column and attach toggle logic.const togglesContainer = document.querySelector('#column-toggles');
allColumns.forEach((col, index) => { const label = document.createElement('label'); label.style.marginRight = '12px'; label.style.display = 'inline-flex'; label.style.alignItems = 'center'; label.style.gap = '4px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox'; checkbox.checked = true; // all columns are visible on load checkbox.dataset.colIndex = String(index);
checkbox.addEventListener('change', () => { if (!checkbox.checked) { // Prevent hiding the last visible column. if (visibleIndices.size === 1) { checkbox.checked = true; return; } visibleIndices.delete(index); } else { visibleIndices.add(index); }
// Apply the new visible subset. hot.updateSettings() re-renders the grid // with only the provided columns config -- no DOM manipulation needed. hot.updateSettings({ columns: getVisibleColumns(), colHeaders: getVisibleHeaders(), });
// When only one column remains visible, disable its checkbox so the user // cannot produce an empty grid. togglesContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => { const idx = Number(cb.dataset.colIndex);
cb.disabled = visibleIndices.size === 1 && visibleIndices.has(idx); }); });
label.appendChild(checkbox); label.appendChild(document.createTextNode(col.title)); togglesContainer.appendChild(label);});<div id="column-toggles" style="margin-bottom: 10px;"></div>
<div id="example1"></div>Overview
Difficulty: Beginner Time: ~15 minutes
This recipe shows how to build a checkbox list outside the grid that toggles column visibility. Each checkbox maps to a column. Checking or unchecking it calls hot.updateSettings() with a filtered subset of your full columns config. The column’s type, renderer, and validator are always preserved because the source config is never mutated.
What You’ll Build
- A
<div id="column-toggles">container above the grid, populated with one labeled checkbox per column - An
allColumnsarray that acts as the single source of truth for all column configurations - Toggle logic that adds or removes a column index from a
visibleIndicesSet on each checkbox change - A guard that prevents the user from hiding every column (at least one must remain visible)
Before you begin
This recipe uses only the built-in Handsontable API. No extra dependencies are required.
The example uses HR/workforce data with seven columns: Name, Department, Role, Salary, Start Date, Location, and Status. The Salary column uses the numeric type with formatting. The Status column uses the dropdown type. This variety demonstrates that hot.updateSettings() restores each column’s full configuration — not just its header text.
Step 1 — Define the full columns config
const allColumns = [ { data: 'name', title: 'Name', type: 'text', width: 140 }, { data: 'department', title: 'Department', type: 'text', width: 120 }, { data: 'role', title: 'Role', type: 'text', width: 150 }, { data: 'salary', title: 'Salary', type: 'numeric', numericFormat: { pattern: '$0,0', culture: 'en-US' }, width: 110, }, { data: 'startDate', title: 'Start Date', type: 'date', dateFormat: 'YYYY-MM-DD', width: 110 }, { data: 'location', title: 'Location', type: 'text', width: 110 }, { data: 'status', title: 'Status', type: 'dropdown', source: ['Active', 'On Leave', 'Inactive'], width: 100, },];What’s happening: allColumns is declared once and never modified. Every toggle operation reads from it to produce a filtered subset. Keeping this array immutable means you can always reconstruct any combination of visible columns without storing redundant state.
Why not mutate? If you splice or delete entries from allColumns, you lose the config for hidden columns and cannot restore them. An immutable source lets you re-derive the visible set at any time.
Step 2 — Track visible column indices
const visibleIndices = new Set(allColumns.map((_, i) => i));What’s happening: visibleIndices is a Set of integer indices into allColumns. It starts with every index (all columns visible). A Set is used rather than an array because membership checks (has) and removals (delete) are O(1), and duplicates are automatically prevented.
Deriving the visible subset:
function getVisibleColumns() { return allColumns.filter((_, i) => visibleIndices.has(i));}
function getVisibleHeaders() { return allColumns.filter((_, i) => visibleIndices.has(i)).map(col => col.title);}These two helpers produce the arguments that hot.updateSettings() needs on every toggle. They are pure functions with no side effects.
Step 3 — Initialize Handsontable
const hot = new Handsontable(container, { data, columns: getVisibleColumns(), colHeaders: getVisibleHeaders(), rowHeaders: true, height: 'auto', width: '100%', autoWrapRow: true, licenseKey: 'non-commercial-and-evaluation',});What’s happening: The grid starts with all columns visible, so getVisibleColumns() and getVisibleHeaders() return full arrays at this point. The initial state of the grid and the initial checkbox state both derive from visibleIndices, so they are always in sync.
Step 4 — Generate the checkbox list
const togglesContainer = document.querySelector('#column-toggles');
allColumns.forEach((col, index) => { const label = document.createElement('label'); const checkbox = document.createElement('input');
checkbox.type = 'checkbox'; checkbox.checked = true; checkbox.dataset.colIndex = String(index);
label.appendChild(checkbox); label.appendChild(document.createTextNode(col.title)); togglesContainer.appendChild(label);});What’s happening: The checkboxes are generated programmatically from allColumns, so the list never goes out of sync with the columns config. Each checkbox stores its column index in dataset.colIndex. Setting checkbox.checked = true on creation matches the initial state of visibleIndices.
Why generate checkboxes in JS rather than hardcode them in HTML? Generating them from the same allColumns array means a single source of truth. If you add or rename a column config entry, the checkbox list updates automatically.
Step 5 — Implement the toggle handler
checkbox.addEventListener('change', () => { if (!checkbox.checked) { if (visibleIndices.size === 1) { checkbox.checked = true; // revert -- cannot hide last column return; } visibleIndices.delete(index); } else { visibleIndices.add(index); }
hot.updateSettings({ columns: getVisibleColumns(), colHeaders: getVisibleHeaders(), });
togglesContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => { const idx = Number(cb.dataset.colIndex);
cb.disabled = visibleIndices.size === 1 && visibleIndices.has(idx); });});What’s happening, step by step:
- When the user unchecks a box, the handler checks if only one column is currently visible. If so, it reverts the checkbox to checked and returns — the grid is not changed.
- Otherwise it removes the index from
visibleIndices(hide) or adds it (show). - It calls
hot.updateSettings()with the newcolumnsandcolHeadersarrays. Handsontable re-renders immediately. - It scans all checkboxes and disables the one checkbox whose column is the sole remaining visible column. A disabled checkbox shows the user that this column cannot be hidden right now.
Why hot.updateSettings() instead of DOM manipulation? Handsontable owns the grid’s DOM. Modifying column elements directly would bypass Handsontable’s internal state and cause rendering inconsistencies. updateSettings() is the documented way to change column configuration at runtime. It triggers a full re-render and keeps all internal state consistent.
Why does the column type survive toggling? When you re-show a column, getVisibleColumns() reads the original config object from allColumns. That object still has its type, numericFormat, source, or any other property you set. Nothing was lost because nothing was mutated.
How It Works - Complete Flow
- Page load:
allColumnsis declared.visibleIndicescontains all indices. The grid initializes with all columns. Checkboxes are generated withchecked = true. - User unchecks “Salary”: The change handler removes index 3 from
visibleIndices.hot.updateSettings()is called with a four-columncolumnsarray and a four-itemcolHeadersarray. The grid re-renders without the Salary column. - User checks “Salary” again: Index 3 is added back to
visibleIndices.hot.updateSettings()restores the full numeric column config — includingnumericFormat— and the grid re-renders with Salary visible. - User hides columns until only one remains: The handler disables the last remaining checkbox. The user cannot produce an empty grid.
What you learned
- Declare an immutable
allColumnsarray as the single source of truth for all column configurations. - Use a
Setof indices to track visibility state and derive the active subset with a filter. - Call
hot.updateSettings({ columns, colHeaders })to apply column changes at runtime. - Generate checkbox controls programmatically from the same config array to keep the UI in sync.
- Guard against an empty grid by checking
visibleIndices.sizebefore hiding a column.
Next steps
- Add a Show all / Hide all button that sets
visibleIndicesto a full or minimal set and callshot.updateSettings()once. - Persist the visible set to
localStorageso the user’s column preferences survive page refreshes. - Combine this pattern with
manualColumnResizeormanualColumnMovefor a full column-management toolbar. - Replace the checkbox list with a drag-and-drop column chooser panel for more advanced UI.