
You spot a league table, a price list, or a Wikipedia stat block, and you want it inside a sheet — but copy-paste mangles the columns, and a screenshot is useless once the numbers change. IMPORTHTML is the one-line fix: point it at a URL, tell it whether you want the table or the list, give it an index, and Google Sheets pulls the data in and re-fetches it on a schedule. The catch is that it only sees the HTML the server actually ships, which is why half the IMPORTHTML questions on the web are really questions about why a specific page returned nothing.
What IMPORTHTML actually does
IMPORTHTML reads the raw HTML at a URL and looks for two element types: <table> tags and <ul> / <ol> lists. It indexes each type separately — the first table is index 1, the first list is also index 1 — and returns the chosen element as a range that spills into the sheet. No login, no cookies, no JavaScript execution. If the page renders its tables in the browser after load, IMPORTHTML never sees them.
That last point catches more people than every other detail combined. A modern dashboard built in React or Vue ships an empty shell of HTML and fills it in on the client; view-source: on that page shows no <table> at all. Static pages — Wikipedia, government stat releases, most reference sites — work fine. App-style pages usually don’t.
The three arguments, in order
The signature is short, and every argument matters. The official Google Docs Editors Help entry documents it as three required parameters.
=IMPORTHTML("https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)", "table", 2)
The URL must include the protocol (https:// or http://) and must be a string or a cell reference, not a bare token. The query is the literal text "table" or "list". The index is a positive integer — Sheets counts every matching element in document order, even ones hidden by CSS.
| A | B | C | |
|---|---|---|---|
| 1 | URL | Type | Index |
| 2 | https://en.wikipedia.org/… | table | 2 |
| 3 | =IMPORTHTML(A2,B2,C2) |
Driving the call from a row of cells is the pattern to use once you have more than one import. Edit the URL in A3 and the formula picks up the change; the alternative is finding and re-typing the literal URL inside the formula every time.
Find the right index without guessing
Most pages have more tables than you think. A Wikipedia article often ships a sidebar infobox as table 1, navigation tables in the footer, and the actual data table you want sitting at index 3, 4, or 5. Guessing wastes API calls and tells the reader nothing about why index 7 returned the wrong thing.
- Open the page in Chrome or Firefox, right-click the table you want, choose Inspect.
- In the DevTools elements pane, press Ctrl+F (Cmd+F on macOS) and search for
<table. - Step through matches with Enter; count from 1. The match that highlights the table you want is your index.
- For lists, search for
<uland<olseparately — but remember IMPORTHTML treats them as one indexed sequence.
If the page is structured cleanly, you usually land between index 1 and 3. If you scroll past index 10 in DevTools and still haven’t seen your table, the page is probably JavaScript-rendered and IMPORTHTML won’t help — jump to the diagnosis section below.
Slice the imported table with QUERY
The whole table spills into your sheet by default, which is fine for a 12-row reference but wasteful for a 200-row league standings page where you only need three columns. Wrap the import in QUERY and the slicing happens before the data lands, so the visible spill is exactly what you want.
=IMPORTHTML(A1,"table",2)
Spills every column, every row.
=QUERY(IMPORTHTML(A1,"table",2),
"select Col1, Col3 where Col3 > 1000 limit 25",0)
Two columns, filtered, capped.
The trailing 0 tells QUERY there is no header row in the source, which matters because IMPORTHTML returns the page’s <th> cells as ordinary row-1 values. Use Col1, Col2, … syntax instead of header names so a column rename on the upstream page doesn’t quietly break your filter. The same pattern works with QUERY’s broader SQL-like syntax — order, group, pivot, the lot.
Pair it with INDEX for single-cell lookups
If you only need one value from the table — last close price, current population, today’s exchange rate — skip QUERY entirely and feed the import straight into INDEX. =INDEX(IMPORTHTML(A1,"table",2), 4, 3) reads row 4, column 3 of the imported table without spilling the rest. Fewer cells touched, fewer downstream references to fix when the source page reshuffles a row.
When the import returns nothing
“Error” and “#N/A” tell you almost nothing on their own. Run this checklist in order before changing the formula — the answer is usually in the first three items.
- ✓ Open the URL in an incognito window. If you see a login wall, IMPORTHTML can’t get past it either.
- ✓ View page source (Ctrl+U). Search for
<table. If there are zero matches, the page is JavaScript-rendered — no formula fix exists. - ✓ Confirm the protocol matches the live page. A hard-coded
http://against a site that now redirects tohttps://sometimes returns empty. - ✓ Cap the index at the actual table count from view-source. Index 5 on a page with three tables returns nothing, silently.
- ✓ Try the formula in a fresh sheet. A workbook that already hosts many IMPORT* calls can hit a per-document soft limit and refuse new ones.
JavaScript-rendered pages are the single most common dead end. The honest answer there is to switch tools — Power Query with a browser-based connector, Apps Script with UrlFetchApp against a documented API, or a paid scraper. Don’t burn a day trying to coax IMPORTHTML into rendering JavaScript; it cannot.
Refresh cadence and the formula limit
IMPORTHTML refreshes automatically — but on Google’s schedule, not yours. The function re-fetches roughly every hour for active sheets, and the cache key is the literal argument tuple. Two formulas pointing at the same URL share the same cached payload, which is usually a feature.
=IMPORTHTML(A1 & "?r=" & NOW(), "table", 1). Use this sparingly — every change burns one of your IMPORT calls.
The other limit nobody documents until they hit it: a single spreadsheet can host roughly 50 IMPORT-family formulas before Sheets starts returning Loading... indefinitely. The fix is to consolidate — a single import feeding a QUERY, instead of five imports for five slices of the same table. The same trick keeps IMPORTRANGE-heavy workbooks from grinding to a halt.
Promote the header row for downstream lookups
IMPORTHTML returns the page’s <th> row as ordinary data in row 1 of its spill. That breaks VLOOKUP, XLOOKUP, and named-range formulas that expect a labeled header. The cleanest fix is to leave the import untouched and build the lookup on a slice that starts at row 2.
=XLOOKUP("France", INDEX(IMPORTHTML(A1,"table",2), 0, 1),
INDEX(IMPORTHTML(A1,"table",2), 0, 3))
The 0 as the row argument tells INDEX to return the entire column, so XLOOKUP gets two parallel ranges and ignores row 1 implicitly because no header value will match “France”. For repeated lookups, land the import on a hidden tab once and run every formula against the static spill — same idea as wrapping a heavy ARRAYFORMULA in a helper range.
Treat IMPORTHTML as a one-line scraper for pages that ship their data as real HTML — Wikipedia, public stats portals, currency reference tables, league standings. The moment a page hides its data behind a login, a JavaScript framework, or a chart widget, switch tools rather than fight the formula. Start with the URL bar test and view-source check; if both look healthy, the index is almost always your only remaining variable.
