Skip to content

Widget Runtime

This page explains how custom widgets are loaded, initialized, and managed by the platform. It covers the runtime model that applies to all widgets regardless of framework — React, Vue, vanilla JavaScript, or anything else.

How Widgets Run

Every widget runs inside a Shadow DOM that the platform creates for you. The Shadow DOM isolates your widget's styles and markup from the host page. At runtime, the platform also creates the sdk object that gives your code access to props, events, and the shadow root where your widget renders.

Your widget's entry file is an HTML fragment, not a full document — no <html>, <head>, or <body> tags (see Your First Widget). At the top level it may contain only an inline <style> block, your markup, and script tags (inline or type="module"); the platform inserts exactly this content into the shadow root it creates.

The init(sdk) Contract

Every widget must export an init function (or a default export). The platform calls this function with the sdk object after the widget connects to the DOM:

javascript
export async function init(sdk) {
  await sdk.whenReady()
  // Mount your UI into sdk.getContainer()
}

// OR default export
export default async function (sdk) {
  await sdk.whenReady()
  // Mount your UI into sdk.getContainer()
}

Your widget is an ES module. The platform loads it via a <script type="module"> tag inside the widget's shadow DOM.

await sdk.whenReady() must resolve before getContainer(), getProps(), or $() are used, since each of these assumes the sdk object has finished initializing — calling them before whenReady() resolves is unsupported. In practice, this means awaiting whenReady() as the first statement inside init whenever a widget uses any of these methods right away.

Widget Lifecycle

Initialization

When a widget appears on a page, the platform runs through this sequence:

Once alive, the widget receives propsChanged and custom events until it is removed.

Teardown

When the widget is removed from the page, the sdk object emits a destroy event. This is where a widget cleans up its UI framework, cancels network requests, clears timers, and removes event listeners.

Example cleanup:

javascript
export function init(sdk) {
  const interval = setInterval(() => fetchData(), 30000)

  sdk.on('destroy', () => {
    clearInterval(interval)
  })
}

Props

Props come from the widget's configuration (set via the No-Code Builder or Widget Definition Reference). They can be read with getProps(), and changes are observed via the propsChanged event:

javascript
const props = sdk.getProps()
console.log(props.title)

sdk.on('propsChanged', (newProps) => {
  console.log('Config updated:', newProps)
})

Design Tokens

Design tokens are CSS custom properties that carry a community's branding — colors, fonts, and other theme values — into a widget's styles. The platform injects them into the widget's shadow DOM automatically, so a widget can reflect each community's look without hardcoding colors or fonts:

css
h1 {
  color: var(--color-action-primary-default, #9254D9);
}

The second argument to var() is a fallback value. It matters because a widget can render before a community's branding has loaded, or outside a community context altogether — without a fallback, those cases have no color to fall back on.

See Use Design Tokens for usage patterns and Design Tokens Reference for the full token catalog.

Custom Events

Widgets can emit and listen for custom events to communicate with the platform or other widgets:

javascript
sdk.emit('taskCompleted', { taskId: 42 })

const unsubscribe = sdk.on('taskCompleted', (data) => {
  console.log('Task completed:', data)
})

The on() method returns an unsubscribe function, called when the listener is no longer needed.

Best Practices

Shadow DOM awareness

  • sdk.getContainer() returns the widget's shadow root, which is why framework apps mount there — passed directly to createRoot() (React) or createApp().mount() (Vue). For finer control over what the framework owns, sdk.$('#root') targets a specific element inside the shadow root instead of the root itself.
  • sdk.$() and sdk.$$() exist because the shadow root is not reachable from the main document. They are shorthands for shadowRoot.querySelector() and shadowRoot.querySelectorAll(), scoped to the widget's own DOM.
  • Styles are scoped to the shadow DOM automatically, which is why a widget's CSS never leaks into the host page or collides with another widget's styles.

Styling

  • Design tokens, referenced with var(), let a widget's colors and fonts adapt to each community's theme rather than being fixed at build time — see Use Design Tokens.
  • The :host selector targets the widget's own container element, which sits outside the widget's regular markup.
  • A fallback value on a design token matters because a widget can render before branding has loaded, or outside a community context altogether: var(--color-action-primary-default, #9254D9).

Bundle size

  • A widget's JavaScript is fetched and executed on every page where it appears, which is why tree-shaking aggressively and lazy-loading heavy dependencies with dynamic import() keep a widget's footprint small.

Cleanup

  • The destroy event exists because the platform does not tear down a widget's framework state or event listeners on removal — that responsibility falls to the widget itself.
  • The on() return value is an unsubscribe function for exactly this reason: it lets a widget stop listening to SDK events once they are no longer needed.
  • Timers and pending requests started during a widget's lifetime need to be cleared in the same destroy handler, since nothing else stops them.

Next Steps

Gainsight CC Developer Portal