Skip to Content
Troubleshooting

Troubleshooting

Find the symptom you see, apply the fix, and follow the link for the full details. The entries describe version 1.6.1.

Loading and registration

The element stays empty

Work through these checks in order. The first one that fails is usually the cause.

What you seeLikely causeFix
customElements.get('mapvx-website') returns undefined in the consolemain.js did not load, or failed to parseLoad it with type="module" from a URL that returns JavaScript, and allow its host in your CSP script-src. See Unexpected token 'export' and Unexpected token '<'.
The element is defined but has no width<mapvx-website> is an inline element by default and can collapse, for example inside a flex rowAdd mapvx-website { display: block; width: 100%; }. See Sizing.
The search box and the Grid View / Map View toggle appear, but there are no cards and the map is blankNo API key reached the component, or the MapVX API requests failSet api-key in kebab-case (an apiKey attribute is ignored). In the Network tab, look for requests to public-api.mapvx.com: none at all means the component has no key. See API key.
The component renders without colors, and the map and markers look brokenstyles.css is not loadedAdd the stylesheet for the same version as main.js.
Cards appear, but the map area stays emptyA map requirement is missingSee The map stays blank.

A minimal embed that passes all these checks:

<link rel="stylesheet" href="https://unpkg.com/@mapvx/web-components-ssr@1.6.1/dist/browser/styles.css" /> <script type="module" src="https://unpkg.com/@mapvx/web-components-ssr@1.6.1/dist/browser/main.js"></script> <style> mapvx-website { display: block; width: 100%; } </style> <mapvx-website api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID"></mapvx-website>

Pin the exact version you tested. The production bundle does not report configuration problems in the console: see Debugging for what the console does show.

SyntaxError: Unexpected token 'export'

Cause: main.js is loaded as a classic script. It has been an ES module since 1.3.0, and a classic script cannot contain the export statement at the end of the file, so the browser rejects the whole file and none of it runs. The element is never registered and stays empty, with no other error. The message is Chromium’s; other browsers word it differently.

Fix: add type="module" to the script tag. Module scripts are deferred automatically, so defer is no longer needed. The same applies to any tool that inserts main.js as a classic script.

<!-- Before --> <script src="/mapvx-website/1.6.1/main.js" defer></script> <!-- After --> <script type="module" src="/mapvx-website/1.6.1/main.js"></script>

See Installation.

Unexpected token '<' in Angular

Cause: the script URL does not point to a file your server serves, so the server answers with an HTML page, typically your app’s index.html as a fallback. The browser then tries to run HTML as JavaScript. A classic script fails with Unexpected token '<'. A module script fails with a MIME type error: Chromium reports Failed to load module script and says that the server responded with a MIME type of "text/html".

Fix: copy the package’s dist/browser folder into your build output with an entry in the assets array of your build target in angular.json, and point the tags at it:

{ "glob": "**/*", "input": "node_modules/@mapvx/web-components-ssr/dist/browser", "output": "mapvx-website/1.6.1" }
<!-- src/index.html, inside <head>. On upgrade, change the version here and in angular.json. --> <link rel="stylesheet" href="mapvx-website/1.6.1/styles.css" /> <script type="module" src="mapvx-website/1.6.1/main.js"></script>

Relative paths resolve against your <base href>. Open the script URL in a browser tab: it must show JavaScript, not your app. See Angular.

chunk-*.js returns 404, or Failed to fetch dynamically imported module

Cause: main.js loads chunk-*.js files relative to its own URL, and every chunk imports ./main.js by that exact name. The chunks hold the 3D map code, which main.js imports only for 3D maps. The component’s map in 1.6.1 is 2D, so a page load requests only main.js and styles.css from the package, and a missing or mismatched chunk goes unnoticed until something loads it. The imports break when:

  • main.js was renamed (for example to bundle.js). The chunks request ./main.js and get a 404 error. If the original main.js is also there, it runs a second time: see NotSupportedError.
  • The chunks were not copied next to main.js, or they come from another version. Chunk names carry a content hash, so a release that changes a chunk also renames it.
  • A cached main.js from an older version asks for chunks that no longer exist. main.js keeps the same name across releases.
  • The files are served from another origin without an Access-Control-Allow-Origin header. Browsers fetch module scripts in CORS mode, so without the header even main.js fails to load. unpkg and jsDelivr send it.
  • A server that answers missing files with an HTML page produces a MIME type error instead of a 404 error.

The message in the heading is Chromium’s; other browsers word it differently.

Fix: copy the whole dist/browser folder with unchanged file names into a versioned path, and replace the folder as a whole on every upgrade. On a CDN, pin an exact version: jsDelivr serves @latest in place and lets browsers cache it for up to 7 days.

// copy-mapvx.mjs: run after npm install import { cpSync, readFileSync, rmSync } from 'node:fs' const pkgDir = 'node_modules/@mapvx/web-components-ssr' const { version } = JSON.parse(readFileSync(`${pkgDir}/package.json`, 'utf8')) rmSync('public/mapvx-website', { recursive: true, force: true }) // Keep every file name as published: the chunks import './main.js' cpSync(`${pkgDir}/dist/browser`, `public/mapvx-website/${version}`, { recursive: true })

Then load /mapvx-website/1.6.1/main.js and /mapvx-website/1.6.1/styles.css. See Self-hosting.

NotSupportedError: the name mapvx-website has already been used

Cause: the bundle was evaluated twice. The browser runs a module once per URL, so the same bundle under two different URLs runs twice:

  • the same bundle from two paths, for example a CDN URL and a self-hosted copy;
  • a <script> tag plus an import in your bundled code;
  • a renamed copy of main.js next to the original;
  • two versions on the same page;
  • a query string on the script URL (main.js?v=2): the chunks import the plain ./main.js, which is a different URL, so the bundle runs again as soon as a chunk loads (in 1.6.1, only 3D maps load chunks).

The first registration keeps working, but the second copy still starts its own application before it fails. The error is written to the console.

Fix: load the bundle once, from one URL. To bust caches, change the directory (/mapvx-website/1.6.1/main.js) instead of adding a query string. To find the duplicate, filter the Network tab by main.js.

ReferenceError: HTMLElement is not defined

Cause: your server code imports the bundle. The bundle uses browser APIs as soon as it is evaluated, so it throws in Node.

Fix: import it only in the browser: a <script type="module">, a dynamic import() inside useEffect or onMount, or a Nuxt .client plugin. In Next.js, load it with next/script (Next.js). See Client-only setup.

// React: runs only in the browser, after hydration useEffect(() => { import('@mapvx/web-components-ssr') }, [])

React reports a hydration mismatch

Cause: with React 19 server rendering, if the bundle upgrades <mapvx-website> and renders its content before React hydrates the page, React reports Hydration failed because the server rendered HTML didn't match the client and regenerates the tree on the client.

Fix: load the bundle after hydration. In Next.js, use the <Script> component with its default afterInteractive strategy; in other React setups, use a dynamic import() inside useEffect. See Next.js.

Your framework rejects <mapvx-website>

  • Angular, 'mapvx-website' is not a known element: add schemas: [CUSTOM_ELEMENTS_SCHEMA] to the component that uses the tag.
  • TypeScript with JSX, Property 'mapvx-website' does not exist on type 'JSX.IntrinsicElements': the package ships no typings for the element, so declare it yourself. See Element typing.
  • Vue, warning Failed to resolve component: mapvx-website: set compilerOptions.isCustomElement. Without it, Vue attaches @userAction listeners for an event named user-action, so they never fire either. See Vue.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' @Component({ selector: 'app-directory', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<mapvx-website api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID"></mapvx-website>`, }) export class DirectoryComponent {}

Data and attributes

Every place card appears twice

Known issue in 1.6.1: combining initial-data with api-key lists every place twice.

Cause: with initial-data, the component still fetches the places from the API and appends them to the places from initial-data without removing duplicates. Every later change of institution-id or initial-data, and every time the element is mounted again in the same page session, appends the places once more. Map markers are not duplicated.

Workaround: leave out initial-data and let the component load the data in the browser. initial-data does not prevent the API requests and does not add place markup to your server HTML, so leaving it out only means that the first cards appear when the API responds. If you keep it, do not change institution-id or initial-data after the first render. See Known issue: duplicated place cards.

A feature disappeared after adding a boolean attribute

Cause: only the exact string "true" turns a boolean attribute on. An attribute written without a value has the value "", which counts as false, and so do "1", "TRUE" and "yes". show-category-filters and show-city-filter default to true, so writing them without a value hides the category tabs or the city filter. Removing an attribute also counts as false, not as the default.

<!-- Hides the city filter: "" counts as false --> <mapvx-website show-city-filter></mapvx-website> <!-- Shows it --> <mapvx-website show-city-filter="true"></mapvx-website>

Fix: always write "true" or "false", in kebab-case (a showCityFilter attribute is ignored). In frameworks:

  • React 19 renders default-to-map={true} as default-to-map="" (false) and drops {false} (so the default applies): pass the strings "true" and "false".
  • An Angular [attr.show-city-filter] binding set to null removes the attribute, which also counts as false.
  • As JavaScript properties, real booleans work: el.showCityFilter = false.

See Boolean attributes and Removing attributes.

default-to-map has no effect

Cause: since 1.6.0 the component remembers the visitor’s last Grid View / Map View choice in localStorage, under the key mapvx:active-view, shared by every page of your origin. A stored choice wins over default-to-map, which only decides the first view for visitors who never toggled it. Also check that the value is exactly "true".

Fix: while testing, clear the site data or remove the key. To make default-to-map win on every load, remove the key before the bundle loads. This also discards the visitor’s choice.

<script> try { localStorage.removeItem('mapvx:active-view') } catch {} </script> <script type="module" src="https://unpkg.com/@mapvx/web-components-ssr@1.6.1/dist/browser/main.js"></script>

See Remembered view.

TypeError after removing institution-id

Cause: removing the institution-id attribute sets its value to null, and the component calls .trim() on it. The console shows CustomErrorHandler followed by a TypeError (Chromium: Cannot read properties of null (reading 'trim')). Frameworks usually remove an attribute when you bind it to null or undefined.

Fix: keep the attribute and set it to an empty string instead of removing it.

el.setAttribute('institution-id', '') // instead of el.removeAttribute('institution-id')
<!-- Angular template --> <mapvx-website [attr.institution-id]="institutionId ?? ''"></mapvx-website>

Setting the initialData property to an object also throws a TypeError: pass a JSON string, JSON.stringify(data). See Removing attributes.

No category tabs, or the wrong city

Cause: the category tabs come from the institution whose id equals institution-id exactly. When the attribute is missing or matches no institution available to your API key, no tabs appear until the visitor picks a city or opens a place. If it also matches no city option, the grid falls back to the first city of your configuration. An id with a different case matches neither an institution nor a city option. An id with surrounding spaces matches no institution, although it still selects its city, because city options are compared after trimming. The production bundle logs nothing about it.

The tab bar is also hidden when show-category-filters is not "true" (see A feature disappeared), or when All is the only category left.

Fix: use the institution id MapVX gave you, exactly as given. See Institution id.

Several elements on one page misbehave

Cause: the elements on a page are not independent. They share one application: one router, one set of loaded data, one map and one SDK, and the first api-key wins. Typical symptoms: an element stops updating when another one navigates, the map jumps between elements, and opening a place scrolls to the first element.

Fix: use one element per page. To show several institutions, give each one its own page, or change institution-id on a single element, which updates at runtime (without initial-data: see Every place card appears twice).

Events and navigation

userAction listeners never fire

  • Use the exact camelCase names userAction and cardSelected. useraction and user-action never fire.
  • Attach the listener to the <mapvx-website> element itself. The events do not bubble, so listeners on document or on a parent never see them, unless they listen in the capture phase.
  • Attach it early. Events are not replayed, so a late listener misses the first ones, such as the show-place of a ?tenant= link. addEventListener works even before the element is defined.
  • React 18: event props do not work; use a ref and addEventListener. React 19: use onuserAction (onUserAction does not fire).
  • Vue: set compilerOptions.isCustomElement. Otherwise Vue attaches @userAction listeners for an event named user-action, which never fires.
const el = document.querySelector('mapvx-website') // Works before the element is defined: no need to wait or poll el.addEventListener('userAction', (event) => { const { type, data } = event.detail console.log(type, data) })

See Listening to events and Framework recipes.

The component keeps reopening a place

Cause: the component reads ?search=, ?tenant= and ?destination= from your page’s URL every time its home screen is created: on first load and each time the visitor returns home from a place profile (the route screen’s Back button goes to the place profile first). Leaving the search results keeps the same home screen, so the parameters are not read again then. If a ?tenant= parameter stays in the URL, the component’s Back button returns home, and home opens that place again. The same happens with a route (?destination=) or a search (?search=). The names search, tenant, destination and tab are reserved: if your page uses them for something else, the component treats them as deep links.

Fix: remove the deep-link parameters when you receive return-to-home. The event fires before the component navigates home, so a synchronous history.replaceState in the listener takes effect in time.

const el = document.querySelector('mapvx-website') el.addEventListener('userAction', (event) => { if (event.detail.type !== 'return-to-home') return const url = new URL(location.href) for (const name of ['search', 'tenant', 'destination']) url.searchParams.delete(name) history.replaceState(history.state, '', url) })

See Deep links and Keeping the host URL in sync.

Browser Back sends the component home

Cause: while a place profile is open, the component treats every popstate event as Back: it emits return-to-home and shows its home screen. That includes the browser’s Back and Forward buttons, history.back() and history.go(), history navigation done by your own router, and navigation that changes only the hash, such as an anchor link (#section) or a script that sets location.hash. The search and route screens do not listen: there, Back changes only your page’s URL, and the component stays where it is.

If you sync the URL: your return-to-home handler also runs after a browser Back. A pushState there adds a new history entry and clears the forward history, so use history.replaceState for return-to-home. See Browser Back.

The page scrolls when a place opens

Cause: opening a place profile (from a card, a map marker, a ?tenant= link or the Back button of the route screen) smoothly scrolls the page to the top of the first <mapvx-website> in the document. It cannot be turned off.

Fix: if a fixed header covers the top of the component, add a scroll margin:

mapvx-website { scroll-margin-top: 80px; /* height of your fixed header */ }

The map stays blank

The cards can work while the map area stays empty, because the map has its own requirements.

CauseHow to confirmFix
No API key, or the configuration or map style request failedIn the Network tab, no request to public-api.mapvx.com, or one with an error statusSet api-key. The map is created only after the configuration loads.
The browser cannot create a WebGL contextA console error containing Failed to initialize WebGL; in Chrome, check chrome://gpuEnable hardware acceleration, or try another browser or device. 1.6.1 uses WebGL2 and falls back to WebGL 1.
Your CSP blocks the map workerA CSP violation for worker-src with a blob: URLAdd worker-src blob:.
Your CSP blocks map dataCSP violations for connect-src (map tiles, sprites, label glyphs)Allow the hosts listed in the allowlist policy.
styles.css is missingColors are missing, and the map and markers look brokenLoad styles.css.

Search stays on “Looking for your results…”

Cause: a search request failed (network error, API error or a CSP block). The search view does not recover from it: it keeps showing the loading placeholders, and typing does nothing until the visitor leaves the search view with Back or with the X in the search box. The console shows a CustomErrorHandler error.

Fix: find the failed request in the Network tab. If it was blocked by your CSP, allow the MapVX API hosts in connect-src (see Allowlist policy).

The location button does nothing

Cause: the visitor denied location access, or the request timed out (after 5 seconds). In both cases the button does nothing and shows no message, and the “Use current location” option of the route origin picker is hidden while access is denied. Location also needs a secure context (HTTPS) and, inside a cross-origin <iframe>, the allow="geolocation" attribute. The browser asks for permission only when the visitor clicks the button or picks “Use current location”.

Fix: serve the page over HTTPS, allow geolocation in iframes, and ask visitors who denied it to allow location in their browser’s site settings.

<iframe src="https://www.example.com/directory" allow="geolocation"></iframe>

See Browser permissions.

Styles

Your page’s CSS changes the component

Cause: the component does not use Shadow DOM. It renders in your page’s DOM, so your rules with element or universal selectors (button, input, img, h1, h3, p, * { box-sizing: ... }) also apply inside it. The place title is an <h1> and card titles are <h3>. The styles of the component’s own elements are scoped and do not leak out; the global rules in styles.css are the exception (see the next entry).

Fix: scope your global rules so they skip the component, or limit them to your own containers:

/* Applies inside the component too */ button { border-radius: 999px; } /* Skips everything inside <mapvx-website> */ button:not(mapvx-website *) { border-radius: 999px; }

See Style isolation.

Your own MapLibre map lost its controls

Cause: styles.css is global. It hides .maplibregl-control-container on every MapLibre map on the page, which also removes the zoom controls and the attribution of your own map. It also styles generic class names (.marker, .current-location-marker and everything inside .banner-content) and defines more than 100 --definitions-* custom properties on :root.

Fix: restore the container for your map only, with a more specific selector, and rename your own classes if they clash with .marker or .banner-content:

#store-map .maplibregl-control-container { display: block; }

See Global styles.

Font Awesome icons conflict

Cause: styles.css includes the core CSS of Font Awesome 7 (.svg-inline--fa, the .fa-* classes and the fa-* keyframes), and the component also injects that CSS at runtime in a <style id="fa-auto-css">. If your page uses another Font Awesome version, both sets of rules use the same class names, so the one that comes later in the cascade decides the size, alignment and animation of your icons and of the component’s.

Fix: the component’s Font Awesome CSS cannot be turned off. Load your own Font Awesome CSS after styles.css so your version wins, then check the component’s icons. Using Font Awesome 7 on your page avoids most differences.

Colors or fonts do not change

  • --mapvx-secondary-orange and --mapvx-secondary-blue work only on :root, because they are resolved there. Setting them on mapvx-website or on a wrapper has no effect.
  • The component does not load Open Sans, its default font. Load it yourself, or set inherit-font-family="true" to use your page’s font (font sizes and weights stay the component’s).
  • Many of the component’s buttons, such as the place cards, the Back buttons and the route controls, show the browser’s default button font, with or without inherit-font-family. Add mapvx-website button { font-family: inherit; } so they follow the component’s font, and your page’s font when inherit-font-family="true" is set. See Fonts.
:root { --mapvx-secondary-orange: #b41500; --mapvx-secondary-blue: #053a96; } mapvx-website button { font-family: inherit; }

See CSS variables and Fonts.

Security and language

CSP violations in the console

Directive in the violationWhat it blocksWhat to allow
script-srcmain.js, and the Google Analytics loader from www.googletagmanager.comThe host that serves main.js, and https://www.googletagmanager.com
style-src or style-src-elemThe <style> tags the component injects into <head>'unsafe-inline', or a nonce through the ngCspNonce attribute
style-src-attrInline style attributes on some Font Awesome icons, the current-location marker and the Deals + Events text'unsafe-inline' for style-src-attr (nonces do not apply to attributes)
worker-srcThe map worker, started from a blob: URLblob:
connect-srcThe MapVX API, map tiles, sprites and label glyphs, error reports, analyticsThe hosts in the allowlist policy
img-srcPlace images, which come from your content’s hosts, and inline imageshttps:, data: and blob:

Blocking Google Analytics or the error reports sent to https://sentry.mapvx.app does not break the component, but the browser logs a CSP violation for each blocked request.

For a nonce-based policy, put ngCspNonce on the element in the served HTML, before main.js runs. The Google Analytics loader is added without a nonce, so also add 'strict-dynamic' or allow its host.

<mapvx-website ngCspNonce="NONCE_VALUE" api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" ></mapvx-website> <script type="module" nonce="NONCE_VALUE" src="/mapvx-website/1.6.1/main.js"></script>

See Content Security Policy, Nonce for injected styles and 'strict-dynamic'.

The interface is only in English

The interface text is fixed in English. There is no language attribute, and the component does not read <html lang>. The data the bundled SDK loads (such as places, categories and route instructions) is also requested in English. Map labels are not: the map shows them in the default language set in your MapVX configuration. Formats are US-style: 12-hour times, distances in feet and phone numbers as XXX.XXX.XXXX. Category tab names come from your MapVX content as written. There is no setting on the element to change the language.

Debugging

The production bundle is quiet. Its diagnostic messages print only in development builds, and there is no debug flag. The component prints nothing for these problems, although the browser can still log the failed network requests:

  • invalid initial-data JSON;
  • an institution-id that matches no institution or city;
  • failed API requests for places, configuration or institutions;
  • failed route requests.

What the console can show:

  • CustomErrorHandler followed by an error: an uncaught error inside the component.
  • requestLocationPermission granted or denied, startWatching, hasBearingPermission: informational messages from the location feature.
  • Messages from the bundled map SDK, such as clickedPlace when a place is clicked on an indoor map, and map errors such as Failed to initialize WebGL.
  • Messages in Spanish such as Error en navegación inicial (an internal navigation error) and Contexto WebGL perdido (the browser lost the map’s WebGL context).
  • CSP violations reported by the browser.

Checks in the console:

// Is the element registered? undefined means main.js did not run customElements.get('mapvx-website') // What did the component receive? Properties return the parsed values const el = document.querySelector('mapvx-website') console.log(el.apiKey, el.institutionId, el.showCityFilter, el.defaultToMap, el.rowsPerPage) // Which view is remembered for this origin? localStorage.getItem('mapvx:active-view')

Checks in the Network tab: filter by mapvx. Places, configuration, institutions, search and the map style load from public-api.mapvx.com, and routes from api.mapvx.com. Map tiles, sprites and label glyphs come from the hosts named in the map style, such as tiles.mapvx.com and api.maptiler.com. No MapVX requests at all means the component has no API key.

Stack traces point to minified code in main.js: source maps are not published. The bundled SDK sends its own error reports to MapVX; see Error reporting.

When you report a problem to MapVX, include the exact package version, the browser, the console output and the failing requests.

Last updated on