Skip to content

Instantly share code, notes, and snippets.

@nickname55
Created April 30, 2026 12:53
Show Gist options
  • Select an option

  • Save nickname55/548a500631672ef3e727080c136a606e to your computer and use it in GitHub Desktop.

Select an option

Save nickname55/548a500631672ef3e727080c136a606e to your computer and use it in GitHub Desktop.
Handsontable: persisting column widths to localStorage doesn't work with manualColumnResize: true (root cause + fix)

Handsontable: persisting column widths to localStorage doesn't work with manualColumnResize: true

Symptom

  • You save column widths to localStorage on afterColumnResize (e.g. {linkedTask: 42, ...}).
  • Verified in DevTools: localStorage value updates correctly after every resize.
  • On F5 reload, widths revert to the defaults defined in colWidths array.

Root cause

The manualColumnResize plugin keeps its own internal map of widths, separate from the colWidths constructor option.

When you initialize Handsontable with:

new Handsontable(container, {
  colWidths: [110, 80, 100, ...],     // base widths (physical order)
  manualColumnResize: true            // <-- plugin enabled, internal map is EMPTY
});

…the plugin sees true and infers "no saved manual widths". On the first re-render after loadData() / updateSettings() the plugin's empty internal map wins and the columns snap back to whatever colWidths provides — which is the default in your code, not the persisted value.

colWidths only acts as a base width. Manual resize values must be passed to the plugin as an array through the manualColumnResize option itself.

Fix

Save (on resize)

afterColumnResize(newSize, column /* visual */) {
  const physical = this.toPhysicalColumn(column);
  const key = COLUMNS[physical]?.key;
  if (key && newSize) {
    saveToLocalStorage(key, newSize);
  }
}

When reading widths back from a live grid (e.g. for a "Save settings" button), read by visual index — getColWidth(physical) returns the wrong cell after a manualColumnMove:

function getColumnWidths() {
  const widths = {};
  COLUMNS.forEach((col, physical) => {
    const visual = hot.toVisualColumn(physical);
    if (visual >= 0) widths[col.key] = hot.getColWidth(visual);
  });
  return widths;
}

Restore (on init AND after applying column order)

Pass the saved widths into BOTH colWidths and manualColumnResize:

function buildArr(savedWidths) {
  return COLUMNS.map((col, idx) => savedWidths[col.key] || allColumns[idx].width);
}
function buildManualSetting(savedWidths) {
  const arr = COLUMNS.map(col => savedWidths[col.key]); // no defaults
  return arr.some(w => w !== undefined) ? arr : true;
}

// Initial:
new Handsontable(container, {
  colWidths: buildArr(saved),
  manualColumnResize: buildManualSetting(saved),
  // ...
});

// After applyColumnOrder() / updateSettings({manualColumnMove}):
hot.updateSettings({
  colWidths: buildArr(saved),
  manualColumnResize: buildManualSetting(saved)
});

Without the second branch, switching column order via manualColumnMove clears the internal width map again.

Bonus

stretchH: 'all' will auto-redistribute widths on every render and override manual resize. Use stretchH: 'none' when you want strict persisted widths.

Why this isn't obvious

  • The Handsontable docs treat colWidths and manualColumnResize as separate options without explicitly calling out that the plugin maintains its own width state.
  • Devtools shows localStorage saving correctly, so debugging looks like a load-side issue when the load API itself is being silently overridden.

Versions

  • Svelte 4
  • Handsontable ^14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment