Skip to content

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:

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>.js shows 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:

  1. Open the page in a private/incognito window with extensions disabled.
  2. 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:

ResourceGlobeLive UsersMapTop Countries
script-src https://statable.com (the widget itself)yesyesyesyes
script-src https://d3js.org (D3.js)yesyesyesno
script-src https://unpkg.com (TopoJSON)yesnoyesno
connect-src https://statable.com (world atlas JSON)yesnoyesno
connect-src https://statable.com (data API)yesyesyesyes
img-src https://cdn.jsdelivr.net (country flags)noyesnoyes
style-src 'unsafe-inline' (injected styles)yesyesyesyes

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>:

StatusWhat it meansWhat to do
400Malformed request (very rare with stock embed).Re-copy the snippet from the dashboard to confirm data-hash looks correct.
404data-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.
429Rate limit per IP.Almost never happens for end-user traffic. If you see it, you're likely load-testing or behind a shared NAT.
5xxBackend 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 to display: 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: hidden and 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:

<iframe sandbox="allow-scripts allow-same-origin" src="..."></iframe>

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.__usdCleanup function 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:

  1. No usable <script> tag in the DOM. The widget locates itself via document.currentScript and falls back to a DOM lookup for <script src="…/<widget>.js">. If you created the tag with document.createElement and never appended it, or appended and removed it before the load event, neither method finds the tag.

    <!-- ❌ never appended -->
    <script>
      const s = document.createElement('script');
      s.src = 'https://statable.com/js/mw.js';
      // missing: document.head.appendChild(s)
    </script>
    
  2. <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.