Scripting (Regular Edition)
This page applies only to the Regular edition of the visual. HTML Content Secure deliberately strips scripts as part of certification - nothing on this page applies to it.
The Regular edition renders author-supplied content as written. That includes embedded <script> elements and inline event handlers (onclick, onerror, …), which execute. This is not a formally supported feature, but it works reliably, and this page documents how and why - so authors can build interactive content (in-DOM filtering, modals, progressive disclosure) on top of a measure-driven UI.
Edition Behavior
| Edition | <script> | Inline on* handlers | <style> / custom CSS |
|---|---|---|---|
| Regular | runs | fires | rendered as-is |
| Secure (Certified) | removed by the sanitizer | element dropped | sanitized |
In the Regular edition the sanitizer is never invoked - not for the DOM and not for the stylesheet. Content is the author's responsibility.
Why Scripts Execute
The visual turns an HTML string into DOM with Range.createContextualFragment(). Unlike innerHTML or DOMParser, fragments produced this way contain executable <script> nodes: the script runs the moment the fragment is connected to the live document. This is a well-known XSS sink - and precisely why the Secure edition sanitizes content to neutralize it. The Regular edition skips sanitization, so scripts survive and run.
Execution Model
- A
<script>runs once, when its node is first inserted into the live DOM. - Order: the Body template is inserted first, then each row. Define shared functions in the body template; per-row scripts can then call them.
- Render mode (Content formatting > On data update):
- Rebuild content - everything is re-parsed and re-inserted on each update, so every script re-runs.
- Preserve unchanged content - only changed rows are re-inserted, so only their scripts re-run. The body/global script persists and does not re-run; unchanged rows keep their DOM and any in-page state.
- Inline
on*handlers fire on the event, every time - no execution-timing caveat. - Everything runs inside the visual's sandboxed iframe; scripts cannot reach the Power BI host page. See Limitations for what the sandbox permits.
Recommended Pattern
Pair Templates with scripting like this:
- Body template - emit one
<script>that defines your shared functions once (and assigns them towindowif rows reference them by name). - Content measure (per row) - emit a tiny
<script>that only calls a shared function to initialise that row. Do not redefine shared functions per row. - Row template -
{{row}}, so the content's own root element is the row.
A generic shape - a shared namespace defined once, called per row:
Body template -- toolbar/header with onclick="app.filterBy('groupA')" ...
-- <div class="rows">{{content}}</div>
-- <script>
window.app = {
filterBy(key) { /* in-DOM show/hide of rows */ },
initRow(el) { /* collapse detail, wire local handlers */ }
};
</script>
Content (one -- <div class="row" id="row_{key}" data-group="...">
row) ... row markup, detail, modal ...
<script>app.initRow(document.getElementById('row_{key}'))</script>
</div>
The toolbar's onclick calls a shared function to filter rows in the DOM; each row's init script wires up just that row. Because each row's root element is also the visual's selectable node, native cross-filtering works alongside the JavaScript.
Reading Theme Colors in Script
The visual publishes the report's theme palette as --pbi-theme-* CSS variables. Consuming them in CSS works in every edition, but CSS alone can't turn a variable into a value you can compute with. A script can - useful when you draw to a <canvas>, build inline SVG, or derive a color:
const styles = getComputedStyle(document.documentElement);
const fg = styles.getPropertyValue("--pbi-theme-fg").trim(); // e.g. "#252423"
const c1 = styles.getPropertyValue("--pbi-theme-color-1").trim();
// ...use fg / c1 in canvas fillStyle, an SVG attribute, etc.
The values update on the next visual update after a theme or high-contrast change, so re-read them rather than caching across updates.
Debugging
Use the visual's own Diagnostics surface:
- The Console tab captures your scripts'
console.log/warn/erroroutput while diagnostics is active. - The Raw HTML tab shows the exact processed markup the visual rendered, so you can confirm your measure produced the HTML you expected.
- The Events tab logs visual updates and interactivity events, so you can see when and why the visual re-rendered.
Gotchas
- Single-root rows - with
{{row}}passthrough, the content must have exactly one root element - that element becomes the row's keyed node (and its selectable node for cross-filter). Nest any modals/scripts inside that root, not as siblings. - Cross-filter vs. inner clicks - the row's root is the selectable node. If an inner control (link, button) must not trigger cross-filter, call
event.stopPropagation()in its handler - or, more simply, mark the elementdata-hc-suppress="filter"(use"all"for an overlay like a modal). The declarative form needs no script, also covers the context menu and tooltip, and works in the Secure edition too. See Suppressing Interactivity on Specific Elements. - Preserve unchanged content + reorder - a row that moves during an update in this mode is detached and reattached, so its scripts/iframes re-run. Updates that don't reorder rows preserve them.
- Trust - the Regular edition runs whatever the content contains. Treat the measures that build your HTML/JS as code you own. Never point it at untrusted or user-supplied HTML.