Appearance
Are you an LLM? You can read better optimized documentation at /docs/custom-widgets/v2/rendering-and-dom.md for this page in Markdown format
Rendering & DOM
Understanding where your widget code runs is essential for writing widgets that work correctly. This page explains the widget rendering environment, how to access your widget's DOM elements, and how to handle multiple widget instances on the same page.
Overview
Widget HTML, CSS, and JavaScript run inside a <gs-cc-registry-widget> custom element that uses Shadow DOM encapsulation. Your widget's markup and styles are isolated from the host page and from other widgets.
This encapsulation has important implications:
- Elements inside your widget are not accessible from the main document
document.querySelector()cannot reach your widget elementsdocument.currentScriptmay benullin the widget script context- Multiple widget instances each have their own Shadow DOM
Shadow DOM and Host Element
The host element is a <gs-cc-registry-widget> custom element that wraps your widget. To access elements inside your widget, query for the host element and access its shadowRoot.
Incorrect — does not work inside a widget:
js
// This returns null — document cannot see into Shadow DOM
const el = document.querySelector('.my-class');1
2
2
Correct — query through the shadow root:
js
const hosts = document.querySelectorAll(
'gs-cc-registry-widget[data-widget-type*="your_widget_type"]'
);
hosts.forEach(function(host) {
const root = host.shadowRoot;
if (root) {
const el = root.querySelector('.my-class');
}
});1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Replace your_widget_type with the type value from your widget's extensions_registry.json entry.
The *= operator matches widgets whose data-widget-type attribute contains your type string — this is more robust than exact match (=) if the platform adds a prefix or suffix to the attribute value.
Script Context
document.currentScript may be null in the widget execution context. This affects patterns like:
js
// This may fail — document.currentScript can be null in the widget context
window.WIDGET_BASE_URL = document.currentScript.src.replace(/[^/]+$/, '');1
2
2
Do not rely on document.currentScript inside widget code. Use the host element approach instead to interact with your widget's DOM.
Multiple Widget Instances
The same widget type can appear multiple times on a page. If you use querySelector (which returns only the first match), only one instance updates while the others remain unchanged.
Incorrect — only updates the first instance:
js
const host = document.querySelector('gs-cc-registry-widget[data-widget-type*="weather"]');1
Correct — updates all instances:
js
const hosts = document.querySelectorAll('gs-cc-registry-widget[data-widget-type*="weather"]');
hosts.forEach(function(host) {
const root = host.shadowRoot;
if (root) {
// Update this instance's DOM
}
});1
2
3
4
5
6
7
2
3
4
5
6
7
DOM Hierarchy
Full Example
A minimal widget that fetches weather data through a connector and updates its own DOM, combining the patterns above:
html
<!-- index.html — your widget entry file -->
<script src="https://static.customer-hub.northpass.com/widget-sdk/latest/index.umd.js"></script>
<div class="weather-widget">
<p class="status">Loading...</p>
<div class="result" style="display:none">
<h3 class="city-name"></h3>
<p class="temperature"></p>
</div>
</div>
<script>
(async function() {
// Find ALL instances of this widget type on the page
const hosts = document.querySelectorAll(
'gs-cc-registry-widget[data-widget-type*="weather"]'
);
for (var i = 0; i < hosts.length; i++) {
var host = hosts[i];
var root = host.shadowRoot;
if (!root) continue;
// Query elements through the shadow root, not document
var statusEl = root.querySelector('.status');
var resultEl = root.querySelector('.result');
var cityEl = root.querySelector('.city-name');
var tempEl = root.querySelector('.temperature');
try {
var sdk = new window.WidgetServiceSDK();
var data = await sdk.connectors.execute({
permalink: 'weather-api',
method: 'GET',
queryParams: { q: 'Warsaw' }
});
cityEl.textContent = data.city;
tempEl.textContent = data.temperature + '°C';
statusEl.style.display = 'none';
resultEl.style.display = 'block';
} catch (err) {
statusEl.textContent = 'Failed to load weather data.';
console.error('Connector error:', err);
}
}
})();
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Key points in this example:
- The SDK is loaded explicitly with a
<script>tag — it is not auto-injected - All host elements are iterated with a
forloop to safely await each instance - Elements are queried through
host.shadowRoot, notdocument - Each instance is updated independently in the loop
Next Steps
- Widget Runtime — The
init(sdk)contract and SDK API reference - Widget Definition Reference — The
typefield used in querySelector selectors - SDK — Install the SDK used in widget scripts
- Card Grid widget in the template repository — Working example of Shadow DOM manipulation with dynamic content and connector data

