Handsontable data grid running as a Lightning Web Component, connected to live Salesforce Account data with full CRUD support. No workaround code — the grid runs on its built-in Shadow DOM and Lightning Web Security support (Handsontable 18.1.1+).
- Handsontable embedded in LWC (Lightning's default synthetic Shadow DOM, Lightning Web Security; the native opt-in also works - see Shadow DOM mode below)
- Live read/write from Salesforce Accounts via Lightning Data Service (no Apex)
- Auto-detected field types — picklists become dropdowns, numbers become numeric, booleans become checkboxes
- Column grouping with collapsible columns
- Row create/delete synced to Salesforce
- Built-in copy/paste, cell editors, selection, and context menu — no Locker/LWS workarounds
- Allowlist HTML sanitizer wired through the grid's
sanitizeroption (Handsontable 18+ ships none built in)
force-app/main/default/
├── lwc/
│ ├── hotGrid/ # Handsontable wrapper (loads resources, creates the grid)
│ └── handsontableApp/ # App component (Salesforce data + column config)
├── staticresources/
│ ├── handsontable/ # Handsontable JS/CSS (unpacked files, not a zip)
│ └── handsontable.resource-meta.xml
├── flexipages/ # "Handsontable Grid" Lightning app page
├── tabs/ # Tab for the page
└── permissionsets/ # Makes the tab visible
- Salesforce CLI (sf)
- A Salesforce org (Developer Edition, Sandbox, or Starter Trial)
- Account records in the "All Accounts" list view (Developer Editions ship with samples)
- Handsontable with Shadow DOM support (the bundled build is the official 18.1.1 release, which includes the Shadow DOM fixes from handsontable#13194 and the Lightning Web Security clipboard fix from handsontable#13388)
sf org login web --alias my-org --set-defaultsf project deploy start --source-dir force-app/main/default --target-org my-orgsf org assign permset --name Handsontable_Grid --target-org my-orgsf org open --target-org my-org --path "/lightning/n/Handsontable_Grid"Handsontable wrapper. Loads the static resources through lightning/platformResourceLoader, then creates the grid inside the component's Shadow DOM (lwc:dom="manual"). Exposes @api properties:
| Property | Description |
|---|---|
data |
2D array of cell values |
columns |
Column type definitions (text, numeric, dropdown, checkbox) |
nested-headers |
Grouped column headers |
collapsible-columns |
Collapsible column group config |
Events: cellchange, rowcreate, rowremoverequest
rowremoverequest fires from beforeRemoveRow, which returns false to cancel the grid's own removal. The app component owns data, so it removes the row optimistically and puts it back when the org refuses the delete.
Connects the grid to Salesforce Account data using Lightning Data Service only:
getObjectInfo— field metadata (types and labels)getPicklistValuesByRecordType— picklist values for dropdown sources;getObjectInforeports that a field is a picklist but does not list its valuesgetListUi— record data from the "All Accounts" list viewupdateRecord/createRecord/deleteRecord— CRUD synced on grid events
Lightning Experience renders LWC components with the synthetic Shadow DOM polyfill by default. This project sets no shadowSupportMode, so the grid runs in that default mode (verified in a Developer Edition org). Handsontable also works with the native opt-in (static shadowSupportMode = 'native'), with one change: Salesforce's loadStyle injects CSS into document.head, which a native shadow root ignores. Inject the two stylesheets into the component's shadow tree instead - append <link> elements inside the lwc:dom="manual" container and wait for their load events before creating the grid.
Handsontable 18+ ships without a built-in HTML sanitizer, and Lightning Web Security sanitizes writes to shared DOM — not to a component's own shadow root, which is where the grid writes. hotGrid passes a small allowlist sanitizer (sanitizeHtml in hotGrid.js) through the grid's sanitizer option, so header labels, menu item labels, and clipboard payloads never reach innerHTML raw. It keeps basic inline formatting (b, strong, i, em, u, br, span), strips every attribute except class (the grid's menus mark checked items with a span.selected that the theme CSS targets), and drops script-bearing elements outright. If the DOM APIs it parses with ever throw in a sandboxed host, it fails closed to a pure-string HTML escape. For a richer policy, load a library such as DOMPurify as another static resource and call it from the same option.
Replace the files in force-app/main/default/staticresources/handsontable/ with a newer release (dist/handsontable.full.min.js, styles/handsontable.min.css, styles/ht-theme-main.min.css) and redeploy the staticresources directory. Load both stylesheets — without the base stylesheet the cell editor renders in wrong positions.
Uses licenseKey: 'non-commercial-and-evaluation' — replace with your commercial key for production use.