How to use custom code JavaScript in Custom pages and System level dashboards

If you have ever written JavaScript that runs flawlessly on a normal web page only to watch it quietly fail inside a dashboard panel, you have already met the Shadow DOM. The code is not wrong; it is simply looking in the wrong place. This article walks through why that happens, where your scripts should actually run inside a Shadow Root, and the small adjustments that turn
classic
JavaScript into Shadow-friendly JavaScript.

The working model: Page vs Panel

The simplest way to understand the difference is, in classic JavaScript, everything lives on the page (the
document
). In Shadow Root JavaScript, everything lives inside a panel (a shadow root), and the page is only the outer shell.
A useful analogy, a classic page is one open-plan office where everyone can see everyone. A shadow-root panel is a private cabin built inside that office. Your JavaScript has to step into the cabin before it can interact with anything inside.
Once that working model clicks, almost every Shadow DOM problem becomes predictable.

Finding Elements (most common issue)

The majority of Shadow DOM issues come from a single root cause, querying document for an element that actually sits inside a shadow root. Consider a common case: finding a button with id
'saveBtn'
.
// Classic page document.getElementById('saveBtn'); // Shadow Root panel shadowRoot.getElementById('saveBtn');
Elements inside a shadow root are invisible to
document
. The fix is not to "
search harder
", instead to start the search from the panel or shadow root itself.

Styling: why your CSS suddenly disappears

A second issue developers run into is missing styles. A global rule like
button { background: blue; }
will style every button on a normal page, but it will not reach inside a shadow root.
To style elements inside a panel, the CSS has to be injected inside the panel:
<style> button { background: blue; } </style>
Shadow DOM is intentionally isolated; that isolation is the feature, not the bug.

Events: why buttons stop responding

Inline handlers such as
<button onclick='save()'>Save</button>
rely on the global scope and document-level lookups, both of which break inside a shadow root. The reliable pattern is to attach Listener directly to the panel container:
panel.addEventListener('click', handler);
Listeners scoped to the panel are safer, more predictable, and easier to clean up when panels are destroyed and recreated.

Third-Party libraries: Pass the element, Not the selector

Many libraries (Data tables, charting libraries, grid widgets) default to scanning the page for elements. That scan never enters a shadow root.
// Classic usage — searches document new DataTable('#tasks'); // Shadow-friendly usage — uses the real element new DataTable(shadowRoot.getElementById('tasks'));
By handing the library an actual DOM node, you guarantee it operates inside the correct panel rather than the page.

Multiple panels on a single page

Dashboards frequently load the same widget several times on a single page. A pattern like
window.myTable = table;
will work for the first panel and silently break the rest, because each new instance overwrites the previous one. Keep state scoped per panel instead:
window.tables[panelId] = table;
The rule is simple, each panel must own its own state, and globals must never be shared across panels.

Timing: page ready is not panel ready

On classic pages,
DOMContentLoaded
is usually enough to know your HTML is in place. Dashboards behave differently, frameworks like Angular inject panel content after the page is "ready", so your script may run before the elements it needs even exist. The conclusion, page ready does not mean panel ready, and panel-aware code should retry until the mount actually appears.

Go-live checklist

Before shipping a dashboard panel, confirm the following:
  • Each panel instance has a
    unique container ID
    .
  • Every element query is scoped to the panel or shadow root, never
    document
    .
  • All required
    styles are included inside the panel
    .
  • Libraries are initialised with
    real DOM elements
    , not selectors.
  • There are
    no shared global variables
    between panels.

DataTables inside a Shadow Root

To make the principles concrete, here is a self-contained example showing how to mount a Data Table grid inside a HighQ dashboard panel. The panel content lives under
<app-dashboard-builder>
in the Shadow DOM, so the script first resolves the correct "world" to search, then attaches the table to the actual
<table>
node it finds inside that tree.
How to use it
  • Paste the block into the HTML source of a UDB / Content Editor panel and view the dashboard in
    View
    mode with
    Run scripts
    enabled.
  • If you place a second copy on the same page, change both the mount ID and the table ID so each panel stays unique.
  • If the table does not appear, open the browser console (F12) and look for log lines prefixed with
    [DT shadow demo]
    . The script retries for a few seconds to accommodate Angular's late rendering.
  • Dependencies:
    DataTables 2.1.8
    from CDN (no jQuery required).
The code
<link rel="stylesheet" href="https://cdn.datatables.net/2.1.8/css/dataTables.dataTables.min.css" /> <script src="https://cdn.datatables.net/2.1.8/js/dataTables.min.js"></script> <style type="text/css"> .dt-demo-scope { box-sizing: border-box; max-width: 100%; padding: 8px 0; } .dt-demo-scope table.dataTable tbody td { font-size: 14px; } </style> <div id="dt-shadow-demo-root" class="dt-demo-scope"> <h2 style="margin:0 0 12px; font-size:1.1rem">Sample table (DataTables + shadow root)</h2> <table id="dt-demo-table" class="display compact" style="width:100%"> <thead> <tr><th>Name</th><th>Role</th><th>Office</th></tr> </thead> <tbody> <tr><td>Alice</td><td>Architect</td><td>London</td></tr> <tr><td>Bob</td><td>Engineer</td><td>New York</td></tr> <tr><td>Carol</td><td>Designer</td><td>Tokyo</td></tr> <tr><td>Dan</td><td>Analyst</td><td>Berlin</td></tr> </tbody> </table> </div> <script type="text/javascript"> (function () { "use strict"; /* Change these IDs if you duplicate the example on one page */ var MOUNT_ID = "dt-shadow-demo-root"; // outer wrapper the script searches for var TABLE_ID = "dt-demo-table"; // the <table> DataTables will enhance /* Retry budget — Angular sometimes paints the panel after the script runs */ var MAX_TRIES = 40; var TRY_MS = 100; // Inside UDB: use the dashboard host's shadow root. // Plain browser test: fall back to document so the demo still runs. function resolveShadowOrDocument() { var host = document.querySelector("app-dashboard-builder"); if (host && host.shadowRoot) return host.shadowRoot; return document; } function getMount() { var root = resolveShadowOrDocument(); if (root && typeof root.getElementById === "function") { return root.getElementById(MOUNT_ID); } return null; } // Find the table INSIDE the wrapper — never rely on document alone. function getTableEl() { var mount = getMount(); if (!mount) return null; if (typeof mount.querySelector === "function") { return mount.querySelector("#" + TABLE_ID); } return null; } // Turn the plain table into a DataTable (once). function initOnce() { if (window.__dtDemoInited) return true; var table = getTableEl(); if (!table) return false; var DT = window.DataTable; if (typeof DT !== "function") { console.warn("[DT shadow demo] DataTable constructor not found (script order?)"); return false; } try { new DT(table, { paging: true, pageLength: 3, ordering: true, searching: true, info: true }); window.__dtDemoInited = true; try { console.info("[DT shadow demo] initialized on #" + TABLE_ID); } catch (_e) {} return true; } catch (err) { console.error("[DT shadow demo] init failed", err); return true; } } // Try now, then retry — friendly for slow dashboard rendering. function schedule() { var n = 0; (function tick() { n++; if (initOnce() || n >= MAX_TRIES) return; setTimeout(tick, TRY_MS); })(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", schedule); } else { schedule(); } })(); </script>
Key details to remember:
  • resolveShadowOrDocument()
    lets the same code run in production (where the shadow host exists) and in a plain browser test (where it falls back to
    document
    ).
  • getTableEl()
    scopes the lookup to the mount div, ensuring DataTables receives a real DOM node rather than a document-level selector.
  • schedule()
    repeats
    initOnce()
    for up to four seconds, accommodating the lag between page-ready and panel-ready.

Key Takeaway

Shadow DOM places every panel inside its own bubble. Your JavaScript still works exactly the same, it just has to start from the panel, not the page.