<table>The symptom is always the same. You are looking at something that is unmistakably a table, with headers and rows and sortable columns, and the tool you reached for says there is no table on this page.
Modern data grids do not use <table> markup. AG Grid, MUI DataGrid, TanStack Table, Handsontable, Ant Design Table and most in-house React grids render nested div elements instead, and describe the structure to screen readers with ARIA roles:
<div role="grid" aria-rowcount="2431">
<div role="row">
<div role="columnheader">Ticker</div>
<div role="columnheader">Last</div>
</div>
<div role="row">
<div role="gridcell">AAPL</div>
<div role="gridcell">228.14</div>
</div>
</div>
They do it because table layout is hard to virtualise, hard to pin columns in, and hard to resize smoothly. The trade is that anything looking for document.querySelectorAll('table') comes back empty.
Open DevTools, go to the Console, and run:
document.querySelectorAll('table').length
document.querySelectorAll('[role="grid"], [role="treegrid"], [role="table"]').length
If the first is 0 and the second is not, you have a div grid, and you now know why the extension found nothing.
Look for the grid's own export first. Many admin panels have a Download CSV or Export button hiding in a toolbar or a row of icons. It is worth ten seconds of looking, because that route asks the server and gets every row, not just the ones on screen.
Use an extractor that reads ARIA roles. Anything that walks role="row" and role="gridcell" handles div grids. GridPick was built around this case: it reads real table markup and ARIA grids, and it walks open shadow roots, which matters because querySelectorAll does not cross a shadow boundary and several component libraries put their grid inside one.
Or paste this into the console. For a one-off, this copies the visible grid to your clipboard as tab-separated text, ready to paste straight into Excel:
copy([...document.querySelectorAll('[role="row"]')]
.map(r => [...r.querySelectorAll('[role="gridcell"],[role="columnheader"]')]
.map(c => c.innerText.trim().replace(/\s+/g, ' '))
.join('\t'))
.join('\n'))
copy() is a DevTools helper, not a page function, so this only works from the console. If the grid is inside an iframe, switch the console context dropdown to that frame first.
Only the visible rows are in the page. Virtualised grids create and destroy rows as you scroll, so the DOM holds fifty of them no matter how many the grid claims. Nothing running in the browser can see the rest. See why your table export only has the rows you can see.
Merged headers move your columns. A header cell spanning two columns, or a section row written as colspan="99", quietly shifts everything under it. A naive extractor produces a 99-column export from that section row. The fix is to measure the real column count from the widest rows and clamp every span to it, which is what GridPick does before it hands you anything.
When the page fetches its rows from an API, the cleanest route is not the DOM at all. Open the Network tab, filter to Fetch/XHR, sort the grid or turn a page so it refetches, and look at the JSON response. Most of the time you can call that endpoint yourself with a larger page size and get everything in one request. Copy the request as cURL from the context menu and it carries your session cookies with it.
GridPick does this for you. It reads real tables and div-based grids, expands merged cells, warns you when a grid is only half loaded, and exports to Excel, CSV, JSON or Markdown. The free version has no row limit; Pro is a one-time payment.