Widgets — common troubleshooting
When a widget fails to load you see a red "Could not load the widget" message with a link to this guide. Open the browser DevTools console — the real cause is logged there as one of:
Widget initialization error: <reason> (Globe, Map)
Widget error: <reason> (Live Users, Top Countries)
Failed to load script: <url>
API error: <status>
Most issues come from the visitor's browser environment (extensions, network policies) or the host page (security headers, conflicting libraries) rather than from the widget itself. Walk through the sections below in order — they're ordered roughly from "most likely cause" to "least."
If the widget is silent (nothing renders, no console error), jump to Widget never executed at the bottom.
For issues that only affect one widget, see the per-widget pages:
- 3D Globe troubleshooting
- Live Users troubleshooting
- Visitor Map troubleshooting
- Top Countries troubleshooting
Ad blockers and privacy extensions
uBlock Origin, AdGuard, Privacy Badger, Brave Shields, and similar tools sometimes match statable.com/js/* against generic tracker blocklists and prevent the script from loading. This is the single most common reason a widget doesn't appear.
Signs:
- Network tab: the request for
https://statable.com/js/<widget>.jsshows status (blocked) in red and no response body. - Console: no errors from the widget (because it never ran).
- The container element is empty.
How to confirm:
- Open the page in a private/incognito window with extensions disabled.
- If the widget appears, an extension is the cause.
There's no widget-level workaround — the visitor has to allowlist your domain (or statable.com) in their blocker. If a significant share of your audience uses blockers, render a fallback server-side using the Stats API and only embed the widget as progressive enhancement.
Content Security Policy (CSP) blocks a required resource
If your page sends a Content-Security-Policy header, every external script, fetch, and image the widget uses must be allowlisted. Console messages from CSP violations look like:
Refused to load the script 'https://d3js.org/d3.v6.min.js' because it
violates the following Content Security Policy directive: "script-src 'self'".
Required entries by widget:
| Resource | Globe | Live Users | Map | Top Countries |
|---|---|---|---|---|
script-src https://statable.com (the widget itself) | yes | yes | yes | yes |
script-src https://d3js.org (D3.js) | yes | yes | yes | no |
script-src https://unpkg.com (TopoJSON) | yes | no | yes | no |
connect-src https://statable.com (world atlas JSON) | yes | no | yes | no |
connect-src https://statable.com (data API) | yes | yes | yes | yes |
img-src https://cdn.jsdelivr.net (country flags) | no | yes | no | yes |
style-src 'unsafe-inline' (injected styles) | yes | yes | yes | yes |
style-src 'unsafe-inline' is required because the widgets inject <style> blocks and inline style="..." attributes at runtime. The widgets don't currently support CSP nonces.
API errors
Every widget surfaces backend HTTP failures as API error: <status>:
| Status | What it means | What to do |
|---|---|---|
400 | Malformed request (very rare with stock embed). | Re-copy the snippet from the dashboard to confirm data-hash looks correct. |
404 | data-hash doesn't match any registered site. | Re-copy from Site settings → Widget → [widget]. The hash isn't the same as the numeric data-id you use for the tracking script. |
429 | Rate limit per IP. | Almost never happens for end-user traffic. If you see it, you're likely load-testing or behind a shared NAT. |
5xx | Backend incident. | Check status.statable.com. Nothing to fix on your side. |
For Live Users specifically, two endpoints run in parallel — either one returning a non-2xx fails the whole widget. Look at the Network tab to see which call returned the error.
Host page has a different version of D3 or TopoJSON loaded
The Globe, Map, and Live Users widgets check 'd3' in window and 'topojson' in window before loading their own copies. If your page already has D3 from a CDN — common with Observable embeds, data-vis pages, or charting libraries — the widget reuses your version. If that version is older than D3 v6, the widget calls APIs that don't exist and crashes:
Widget initialization error: TypeError: d3.geoOrthographic is not a function
Widget error: TypeError: d3.scaleBand is not a function
The same applies to TopoJSON.
Workarounds:
- Upgrade the host page's D3 to v6 or newer. v6 is backwards-compatible with v7 for the calls the widget uses.
- If you can't upgrade, remove the host page's
<script src="…d3…">tag and let the widget load its own copy. - Load both isolated inside separate iframes if you genuinely need two D3 versions on one page.
Page is served over HTTP (mixed content)
Browsers block https:// fetches from http:// pages. Console:
Mixed Content: The page at 'http://example.com' was loaded over HTTP, but
requested an insecure XMLHttpRequest endpoint 'https://statable.com/...'.
This request has been blocked.
The widget API is HTTPS-only and won't be served over plain HTTP. Migrate your page to HTTPS — there's no alternative.
Widget renders but its container has zero height
The widget renders successfully into a container that the host page has collapsed to zero pixels. Common patterns that hide the container:
- Wrapped in a flex/grid cell with
height: 0. - Inside a collapsed
<details>element or an accordion that defaults todisplay: none. - Tab content that's hidden until the tab is selected — the widget mounted when the tab was hidden, into a 0×0 area.
- A parent with
overflow: hiddenand zero height.
There's no console error because nothing failed. Open DevTools → Elements, find the widget's <div> container, and check its computed height.
If you embed inside a collapsed/hidden region, mount the widget after the region becomes visible — or set explicit data-width / data-height so it has a guaranteed footprint.
Iframe with restrictive sandbox attribute
If you embed your page inside an iframe with sandbox but no allow-scripts, no script (including the widget) runs at all. With allow-scripts but no allow-same-origin, fetches to statable.com can still fail in some browsers.
The widget needs at minimum:
SPA navigation: widgets pile up or disappear
In single-page apps (React, Vue, Nuxt, Next.js client navigation), the <script> tag is re-executed on each navigation that mounts the snippet. The widget creates a new container next to its tag and the previous instance is left orphaned in the DOM. After several route changes the page accumulates dead widget containers.
Cleanest options:
- Don't unmount/remount. Render the widget once at a stable layout slot (sidebar, footer, sticky panel) outside the routed area.
- Mount via iframe. Put the embed snippet on a static HTML page and load that page in an
<iframe>inside your SPA. Iframe content is isolated, so SPA navigation never touches it. - If you need full programmatic control, the dashboard preview component can serve as a reference for cleanup — every widget exposes a
container.__usdCleanupfunction on its root element.
Widget never executed
If nothing rendered and there are zero widget-related console messages — neither errors nor Widget initialization error — the widget bailed out before it could log anything. There are two such silent exits:
No usable
<script>tag in the DOM. The widget locates itself viadocument.currentScriptand falls back to a DOM lookup for<script src="…/<widget>.js">. If you created the tag withdocument.createElementand never appended it, or appended and removed it before the load event, neither method finds the tag.<script>tag detached. The tag exists in your source but a router or framework removed it between parsing and the widget's bootstrap.
Fix: leave the snippet exactly as copied from the dashboard. The widget removes its own tag after it places the container — don't manipulate the tag yourself.
Branding badge ("Powered by Statable") appears on a paid plan
The badge renders only when the loaded bundle is the hobby variant (/js/t/<widget>.js) or the <script> tag has data-preview-hobby="true" (dashboard preview).
If you're on a paid plan and the badge appears, the snippet was copied from a hobby preview or the URL was hand-edited. Re-copy from Site settings → Widget → [widget] — the paid snippet links to /js/<hash>/<widget>.js and doesn't trigger branding.
Still stuck?
Include the following when contacting [email protected]:
- The widget (
gw.js,luw.js,mw.js,tcw.js) and the full embed snippet you used. - The console error verbatim.
- A Network tab screenshot of the failing request (URL, status, response headers).
- Browser + OS, and whether the issue reproduces in a private window with all extensions disabled.
- If your page has a CSP header, the full header value.
Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.