I kept running into the same annoyance: select a table on a web page, paste it into a spreadsheet, and watch the columns drift. Merged header cells shift everything one column to the right, and numbers arrive as text. So I built a small Chrome extension, TableGrab, to fix it. Here is what I learned along the way.
1. The problem: walking cells in order misaligns columns
The naive approach is to loop over each <tr>, then each <td>, and push the text into a 2D array. That works until a cell has rowspan or colspan. A cell with rowspan="2" in row 1 occupies a slot in row 2 too, but row 2's <td> list does not mention it. Every cell after it lands one column too far left.
2. The fix: an occupancy grid
Instead of pushing cells in order, keep a grid of occupied slots:
function tableToGrid(table) { const grid = []; const rows = [...table.rows]; rows.forEach((tr, r) => { grid[r] = grid[r] || []; let c = 0; for (const cell of tr.cells) { while (grid[r][c] !== undefined) c++; // first free slot const rs = cell.rowSpan || 1; const cs = cell.colSpan || 1; const text = cell.innerText.trim(); for (let i = 0; i < rs; i++) { grid[r + i] = grid[r + i] || []; for (let j = 0; j < cs; j++) grid[r + i][c + j] = text; } c += cs; } }); return grid; } Each cell is placed at its first free (row, column), and then its whole span is filled. Filling the span with the cell text (rather than leaving blanks) means columns line up and filters and sorting still work in the spreadsheet.
3. Detecting ARIA grids and div grids
Plenty of modern "tables" are not <table> at all. Two common cases:
- ARIA grids: elements with
role="grid"orrole="table", containingrole="row"androle="gridcell"/role="columnheader". - Div grids: repeated rows of sibling elements with the same number of children and a consistent layout.
I treat the first as straightforward (the roles tell you the structure) and the second as a heuristic, so it is worth showing a preview before copying.
4. CSV gotchas
If you export CSV, three things bite:
- Quoting: wrap fields containing commas, quotes or newlines in double quotes, and double any embedded quotes.
- Excel and UTF-8: Excel often misreads UTF-8 CSV unless the file starts with a byte order mark (
\uFEFF). Add it and accented names stop turning into garbage. - Formula injection: a cell starting with
=,+,-or@can be interpreted as a formula when opened in Excel. Prefix such cells with a single quote or apostrophe so they are treated as text.
5. MV3 permissions: activeTab only
For a tool that reads page content, I wanted the smallest permission footprint. With Manifest V3 and activeTab, the extension can only touch a page after the user clicks it. There is no broad host access, no account, and nothing is uploaded. It is a nice side effect that the permission prompt is also easy to explain to users.
6. Try it
TableGrab is free for tables up to 100 rows, and Pro is a one-off US$15 (unlimited rows, .xlsx export and more). Details and the Chrome Web Store link are at https://pagepingapp.com/tablegrab/.
If you have a table that still breaks it, I would like to hear about it in the comments.

