Documentation Index

Fetch the complete documentation index at: https://docs.unqork.io/llms.txt

Use this file to discover all available pages before exploring further.

Embedded UI Iframe Mode Reference

Prev Next

Embedded UI offers two ways to embed Unqork modules into an external web page. The default approach uses a Web Component with Shadow DOM isolation. The second approach, iframe mode, loads the module inside a native browser iframe, which provides complete separation of CSS, JavaScript, and DOM between the host page and the module. This stronger isolation comes with tradeoffs: the host page and module communicate through a messaging system instead of direct page access.

Activate iframe mode by passing iframe: true to mountModule().

Note: Iframe mode is currently Centauri only. Attempting to mount a Vega (v2.x) module in iframe mode throws an error. Workflow mounting (mountWorkflow()) always uses the web component path.

When to Use Iframe vs. Web Component

Use the following table to choose the right embedding approach. Most integrations should begin with the Web Component approach and switch to iframe mode only when complete CSS, JavaScript, or DOM isolation is required.

Criterion Web Component (default) Iframe
CSS isolation Shadow DOM Browser-level
JS isolation Shared window Separate browsing context
Communication Direct DOM and events postMessage API
Authentication Shared cookies Shared cookies
Resize Automatic (content is in-page) Auto-resize through postMessage
Data access Direct Redux and Angular scope Request and response protocol
Performance Better Slight overhead
Multiple modules Limited Naturally supports multiple
Deep linking Not supported Not supported

Embedded UI Iframe Architecture

This diagram displays how the host page and the iframe communicate. Every interaction between them passes through the postMessage API, managed by the files running inside the iframe.

graph TB
    subgraph "Host Page"
        HOST_SCRIPT["embedded.js"]
        HOST_API["Iframe API wrapper"]
        HOST_TARGET["Target container"]
    end

    subgraph "Iframe (Unqork origin)"
        IFRAME_APP["Full Unqork Express SPA"]
        EDC["EmbedDisplayController"]
        SEC["embed-security.js"]
        MSG["embed-messaging.js"]
        API_H["embed-api-handlers.js"]
        RESIZE["embed-resize.js"]
        EVENTS["embed-events.js"]
    end

    HOST_SCRIPT -->|"creates"| HOST_API
    HOST_SCRIPT -->|"appends"| HOST_TARGET
    HOST_TARGET -->|"contains"| IFRAME_APP
    HOST_API <-->|"postMessage"| MSG
    MSG --> SEC
    MSG --> API_H
    MSG --> RESIZE
    MSG --> EVENTS
    EDC --> MSG
    EDC --> API_H
    EDC --> RESIZE
    EDC --> EVENTS

Handshake Protocol

Before any data exchange, the iframe and host establish a secure channel:

sequenceDiagram
    participant Host as Host Page
    participant Iframe as Iframe (Unqork)

    Note over Iframe: Iframe loads, EmbedDisplayController initializes
    Iframe->>Host: { type: 'unqork-iframe-ready-for-init', id }
    Host->>Iframe: { type: 'unqork-iframe-init', id, secret, timestamp }
    Note over Iframe: Validates origin, stores secret
    Iframe->>Host: { type: 'unqork-iframe-ready', id, secret }
    Note over Host: Handshake complete — API calls enabled
    Note over Iframe: Starts resize monitoring

Security Model

The iframe embed uses five security layers. Each layer blocks a different type of attack, so the channel stays secure even if one layer fails.

Layer Module Purpose
Origin validation embed-security.js Checks event.origin against an allowlist of trusted origins before processing any message.
rate limiting embed-security.js Allows a maximum of 100 messages per second (configurable). Messages above the limit are dropped.
HMAC signing embed-security.js Uses SHA-256 (Hash-based Message Authentication Code) to verify message integrity through a shared secret established during the handshake.
Input sanitization embed-security.js Enforces string length limits, strips script tags, and applies object depth limits on all incoming data.
Sandbox attribute Browser The iframe is created with allow-scripts allow-same-origin allow-forms (configurable). This restricts what the iframe content can do in the browser.

Auto-Resize

The iframe automatically resizes to fit its content. Four mechanisms work together to detect size changes:

  • ResizeObserver: Watches the document body for element size changes.
  • MutationObserver: Detects DOM changes, like new components rendering.
  • Window resize listener: Handles viewport size changes.
  • Polling fallback: Runs every 500 ms in older browsers that don't support ResizeObserver.

Height changes are debounced by 50 ms and sent to the host through postMessage. CSS overrides injected inside the iframe prevent position: fixed elements from causing resize feedback loops.

EmbedDisplayController: Inside the Iframe

The EmbedDisplayController.js controller runs inside the iframe and manages all communication between the iframe and the host page:

  • Detecting the iframe context through the iframeId query param.
  • Injecting embed CSS overrides.
  • Initializing SecurityManager, MessagingManager, APIHandlers, ResizeManager, and EventManager.
  • Listening for incoming API requests.
  • Cleaning up on $scope.$destroy.

Embedded UI Iframe Limitations

Be aware of the following constraints before building an iframe-mode integration. Some are fundamental browser limitations; others are specific to how iframe mode works.

Limitation Description
Centauri only Vega modules cannot be iframe-embedded. Attempting to do so throws an error.
No workflow support mountWorkflow() always uses the web component path. Passing iframe: true has no effect on workflows.
Cross-origin cookies The allow-same-origin sandbox attribute is required for session cookies to work. Removing it breaks authentication.
No direct DOM access The host page cannot query elements inside the iframe. All interaction must go through the IframeAPI.
Timeout risk API calls default to a 20-second timeout. Large or complex forms may need a longer timeout set explicitly.

Detailed Feature Comparison: What Does Not Work in Iframe Mode

The tables below list features and behaviors that are unavailable or work differently in iframe mode compared to the default web component mode.

Unsupported Features

The following features are fully supported in Web Component mode but are not available when using iframe mode.

Feature Web Component Iframe Detail
Workflow mounting mountWorkflow() always renders through the web component path. Passing iframe: true has no effect on workflows.
Vega (v2.x) modules Iframe mode only supports Centauri (v1.x). Attempting to mount a Vega module with iframe: true throws an error.
onRuntimeError callback The global runtime.onRuntimeError(callback) handler receives errors surfaced from embed-utils.surfaceEmbedError(). These CustomEvent dispatches happen on the iframe's own window, not the host window, so the host callback never fires.
unqork-embed-error DOM event The event is dispatched on the iframe's own window, not the host window, so it does not reach the host page. Use IframeAPI.on('error', callback) to handle errors in iframe mode.
Direct access ✓ (through Redux/Angular scope) ✗ (async only) In web component mode, the Angular scope and Redux store live on the host window, so data is synchronously accessible. In iframe mode, all data access passes through the async IframeAPI.getSubmissionData() or getComponentValue() calls over postMessage.
recover() method The runtime recover() method reloads the module inside the current Shadow DOM. There is no equivalent postMessage action defined for iframe mode.
persistWorkflowState option Only applies to mountWorkflow(), which is web-component-only.
moduleData pre-fetch pass-through In web component mode, pre-fetched module JSON can be passed as moduleData to avoid a double fetch. The iframe loads through a URL (#/embed/display/{moduleId}), so the module is always fetched inside the iframe.

Behavioral Differences

The following features work in both modes but behave differently. Review the behavioral differences below to avoid unexpected behavior in your integration.

Feature Web Component Behavior Iframe Behavior Impact
CSS isolation Shadow DOM (good, not perfect): inherited properties like font, color, and cursor can leak in from the host page. Browser-level: complete isolation; no style leakage in either direction. Iframe is stronger; web component may need :host overrides for inherited styles.
JS global isolation Shared window: Angular, jQuery, and Redux all live on the host window, creating potential namespace collisions. Separate browsing context with complete JavaScript isolation. Iframe eliminates global pollution but prevents direct programmatic access.
Authentication Shared cookies; isAuthenticated(), authenticateAnonymous(), authenticateReferString(), and login pop-up all work directly. Shared cookies through the allow-same-origin sandbox flag; auth methods work but the login pop-up opens from the host origin. Removing allow-same-origin from sandbox breaks authentication entirely.
Error surfacing Errors dispatched as CustomEvent('unqork-embed-error') on host window; caught by onRuntimeError callback. Errors stay inside the iframe window; host must use IframeAPI.on('error', callback) if the iframe-side code emits them. Host error handling requires different wiring. Currently, iframe-side error events are not forwarded to the host.
Auto-resize Automatic: content is in the host DOM, so the container grows naturally. postMessage-based: embed-resize.js uses ResizeObserver, MutationObserver, and polling inside the iframe, then sends height through postMessage. Slight latency (debounced 50 ms). CSS position: fixed elements inside the iframe can cause sizing loops; the resize manager injects overrides to prevent sizing loops.
Component value mutation Direct through Angular scope or Redux dispatch. Async through IframeAPI.setComponentValue(componentId, value) with a postMessage round-trip. Subject to 20-second default timeout; large data inputs are sanitized (string max 10,000 chars, array max 1,000 items, object max 100 keys, max depth 5).
Form submission trigger Direct through Angular $scope.$emit('buttonClick', ...) or Redux action. Async through IframeAPI.triggerSubmit(). Returns a promise; timeout applies.
Custom events from module unqork-embed-module-mounted, unqork-embed-workflow-navigate, and so on dispatched on host window. Module events fire inside the iframe window; only events explicitly forwarded by embed-events.js are available through IframeAPI.on(eventName, callback). Not all web-component-mode events have iframe equivalents.
destroy() behavior Replaces the custom element with a fresh empty instance of the same tag, clearing the Shadow DOM. Removes the iframe element from the DOM and cleans up postMessage listeners and pending requests. Both allow re-mounting after destroy.
Multiple modules on one page Limited: only one Angular bootstrap per page. Naturally supported: each iframe is an independent Angular instance. Iframe is the recommended path when multiple modules are needed on a single page.
Navigation guard scope document-level click listener blocks <a> navigations inside the Shadow DOM. Browser sandbox handles navigation; allow-top-navigation is intentionally omitted from the default sandbox string. If allow-top-navigation is added to the sandbox, the iframe can navigate the host page. Avoid adding the allow-top-navigation flag.

Iframe Data Sanitization Limits

All data flowing through the postMessage API is sanitized by embed-security.js. These limits do not apply to web component mode.

Constraint Limit
String length 10,000 characters (truncated)
array length 1,000 items (truncated)
Object key count 100 keys per object
Nesting depth 5 levels
Script tags Stripped from all strings
Functions and symbols Removed (replaced with null)
Message rate 100 messages per second (configurable)

Centauri Component Compatibility: Iframe vs. Web Component

The following sections document specific Centauri components that have limited or broken functionality in Web Component (Shadow DOM) mode but work correctly in iframe mode. These limitations stem from third-party libraries that assume they operate in a standard document context, which holds true in an iframe but breaks inside a Shadow DOM.

Components with Known Limitations in Web Component Mode

Component Library Limitation in Web Component (Shadow DOM) Root Cause
RichTextEditor CKEditor 5 (@unqork/ckeditor5-build) Toolbar drop-downs, dialogs, and balloon panels render outside the Shadow DOM. CKEditor's internal DOM queries target document.body for overlay positioning. Comments and track-changes sidebars may fail to attach. CKEditor appends UI layers to document.body. Inside Shadow DOM, the appended UI layers appear outside the shadow tree, losing CSS isolation and event context.
DynamicGrid (AG Grid) @ag-grid-community/react, @ag-grid-enterprise/* Pop-up editors, filter panels, column menus, and context menus render at document.body through popupParent: document.querySelector('body'). Drop-down cell editors (react-select) explicitly portal to document.body. These pop-ups display outside the Shadow DOM, unstyled and potentially clipped. AG Grid's popupParent defaults to document.body. The cell editors (singleSelectEditor.js, multiSelectEditor.js) set menuPortalTarget = document.body.
Chart Highcharts + Handsontable Highcharts' exporting module creates a hidden iframe and form on document.body for PNG, SVG, and CSV download. Tooltip elements may be appended outside the component container. Handsontable data table injects drop-down editors to document.body. Highcharts exporting appends to document.body. Handsontable cell editors and autocomplete drop-downs escape the Shadow DOM boundary.
Sheet (Spreadsheet) Handsontable Pro (handsontable-pro) Autocomplete and drop-down cell editors, context menus, and comment overlays are appended to document.body. These elements are invisible (lack styles) or positioned incorrectly in Shadow DOM. Handsontable internally uses document.body.appendChild() for overlays and floating editors.
DateInput (legacy datetime) Flatpickr Calendar drop-down is appended to document.body by default. Flatpickr queries document for positioning and uses document.addEventListener for outside-click detection that doesn't account for Shadow DOM event retargeting. Flatpickr's appendTo defaults to document.body; its outside-click handler uses document.addEventListener('click') which receives retargeted events in Shadow DOM.
DateInput (v2) Pikaday + Cleave.js ⚠ Partially mitigated: the codebase includes Shadow DOM workarounds (detecting ShadowRoot, setting container to shadow root, using composedPath() for click detection). However, keyboard navigation, scroll-aware repositioning, and some edge-case outside-click scenarios remain fragile. Pikaday's _onClick handler relies on event.target which is retargeted in Shadow DOM. The workaround addresses outside-click detection but may not cover all interaction paths.
Select (v1, ui-select) Angular ui-select ⚠ Partially mitigated: spa-embed.js patches uiSelectDirective with a Shadow DOM workaround for outside-click detection. However, the drop-down options list positioning still depends on document.body and may overflow or clip incorrectly. Multi-select tags input may lose keyboard focus handling. ui-select's onDocumentClick uses element.contains(event.target), which fails in Shadow DOM due to event retargeting. The existing patch handles close-on-click but not positioning.
Map V2 Google Maps JavaScript API (window.google.maps) Google Maps loads scripts globally through <script> tags appended to document.head, and queries document for its container element. The map renders inside the shadow tree (the div is local), but autocomplete widgets (google.maps.places.Autocomplete) append suggestion drop-downs to document.body. Info windows and controls may have z-index issues. Google Maps API inserts auxiliary DOM (pac-container for autocomplete, info windows) at document.body. These are unstyled and positioned incorrectly relative to the shadow-contained map.
Address Dropdown Google Places (through Map service) + react-select/async The Google Places autocomplete suggestions drop-down (.pac-container) is appended to document.body, rendering outside the Shadow DOM. Users see suggestions appear behind or displaced from the input field. Google's pac-container div is injected into document.body and cannot target a Shadow DOM container.
Recaptcha react-google-recaptcha / Google reCAPTCHA API reCAPTCHA widget injects its iframe and badge into document.body. The invisible badge (data-badge) renders outside the shadow tree. Challenge pop-ups may fail to anchor correctly to the component. Google reCAPTCHA inserts challenge iframes and badges at document.body. Script loading also targets document.head.
Plaid (Plaid Link) Plaid Link SDK (external script) Plaid Link opens a modal overlay attached to document.body. In Shadow DOM, the overlay renders outside the embed container, lacks styling context, and may have z-index conflicts with the host page. Plaid.create() appends its modal iframe overlay to document.body.
Dataworkflow (Data Mapper v1) GoJS (window.go) GoJS appends measurement elements to document.body (measuring text widths, canvas sizing). Tooltips use go.HTMLInfo which creates DOM elements at document level. Inspector panel (datainspector.js) uses document.getElementById for its container. GoJS library code (go.js line 14070+) directly calls window.document.body.appendChild(). The inspector uses document.getElementById(divid) which doesn't find elements inside Shadow DOM.
Plugin (Integrator) Internal (XHR + DOM spinner) Adds/removes CSS classes on document.body (integrator-wait) to show/hide a global spinner overlay. Inside Shadow DOM, the integrator-wait class is applied to the host page <body>, not the embedded container. The spinner overlays the entire host page, not just the embedded module. Direct document.body.classList.add/remove('integrator-wait').
SweetAlert2 Modals SweetAlert2 (swal2) Used across various components for confirmation dialogs. SweetAlert2 creates modal overlays at document.body. The embed-loader injects swal2 CSS into document.head as a workaround, but the modal still renders outside the shadow root, breaking visual containment and focus trapping. SweetAlert2 appends its overlay container to document.body. CSS is injected to document head, but DOM remains external to shadow tree.

Components That Work Correctly in Both Modes

The following components work correctly in both embedding modes and do not require special handling or workarounds.

Component Notes
Textfield, Textarea, Number, Email, Password, PhoneNumber Standard input elements; no external DOM manipulation.
Button, Hidden, Calculator, Transformer Logic-only or simple DOM; no portaling.
Checkbox, Checkboxes, Radio Native form controls; work in any container.
FileInput Uses react-dropzone which operates on its container element. No global DOM access.
Signature Uses signature_pad on a <canvas> element in the component tree. No external DOM access.
Markdown Uses marked for parsing; renders sanitized HTML inline. No external DOM.
Image, Typography, HTMLElement Static rendering; no third-party DOM manipulation.
Progress, Timer Visual-only components using local state.

Iframe-Only Capabilities

The following tables document features, behaviors, and guarantees that are exclusively available or only work correctly in iframe mode.

Features Available Only in Iframe Mode

These features are exclusively available in iframe mode. They are not available in Web Component mode.

Feature Status in Iframe Why Web Component Cannot Provide This
Multiple modules on one page ✓ Natively supported Web component is limited to a single Angular bootstrap per page. Mounting a second module in web component mode produces undefined behavior. Each iframe is an independent Angular instance.
Complete JavaScript isolation ✓ Separate browsing context Web component shares the host window. Angular, jQuery, and Redux globals leak onto the host page, risking namespace collisions with host-page frameworks (React, Vue, another Angular instance, and so on).
Complete CSS isolation (no inheritance leakage) ✓ Browser-level Shadow DOM blocks direct style selectors but cannot block inherited CSS properties (font-family, color, line-height, cursor, and so on). These leak into the web component from the host page.
Host page crash protection ✓ Contained An uncaught exception in the module's Angular runtime terminates only the iframe's JavaScript context. In web component mode, uncaught errors propagate on the shared window and can crash host-page error boundaries or monitoring tools.
IframeAPI programmatic interface ✓ Full IframeAPI object returned Web component returns an HTMLElement; data access requires direct Angular scope or Redux store inspection on the shared window. The IframeAPI offers a typed, promise-based contract (getSubmissionData, getComponentValue, setComponentValue, getComponentDefinition, triggerSubmit).
Event subscription through api.on() submit, error, navigate, locationChange Web component relies on CustomEvent dispatches on the host window, which are more fragile and less discoverable. The IframeAPI.on() pattern provides explicit subscribe/unsubscribe with payload typing.
HMAC-signed message integrity ✓ SHA-256 signed postMessage Not applicable to web component (communication is direct DOM access; no message channel to sign). Iframe mode signs every sensitive message to prevent injection from rogue postMessage callers.
Input sanitization on data exchange ✓ Enforced automatically Web component provides raw access to Angular scope; no sanitization layer exists. Iframe mode truncates strings (10,000 chars), arrays (1,000 items), limits object keys (100) and depth (5), and strips <script> tags.
Rate limiting on communication ✓ 100 msg/s (configurable) Not applicable to web component. Iframe mode throttles incoming messages to prevent denial-of-service through postMessage flooding.
Configurable sandbox policy ✓ Through the sandbox option Web component has no equivalent; the module inherits full host-page privileges. Iframe sandbox allows granular permission control (allow-scripts, allow-same-origin, allow-forms, allow-popups).
Navigation confinement (no host-page redirect) allow-top-navigation intentionally omitted Web component uses a document-level click listener to intercept <a> navigations, but the click listener is a best-effort guard; programmatic window.location changes in Angular can still escape. The iframe sandbox physically prevents top-frame navigation.
Secure handshake before data exchange ✓ Three-step handshake protocol Not applicable to web component. The iframe establishes a verified channel (ready-for-initinit with secret → ready) before any API call is accepted.
Per-request timeout control ✓ Configurable per API call (default 20 s) Web component data access is synchronous (Angular scope), so there is no timeout concept. Iframe mode lets callers pass a timeout parameter to any IframeAPI method to control wait duration.

Behaviors That Work Correctly Only in Iframe Mode

Several behaviors that are unreliable in Web Component mode work correctly in iframe mode due to how browsers handle iframes versus Shadow DOM boundaries.

Behavior Iframe Correctness Web Component Issue
Third-party library conflict avoidance ✓ Guaranteed If the host page loads jQuery, Lodash, Moment, or any library that conflicts with the versions bundled in the Unqork SPA, the web component shares the same window and version collisions occur silently. The iframe loads its own dependency tree in isolation.
Content Security Policy (CSP) separation ✓ Independent CSP The web component inherits the host page's Content Security Policy (CSP). If the host has a restrictive CSP that doesn't allow unsafe-eval or inline styles, the Angular module may fail to bootstrap. The iframe loads from the Unqork origin and applies its own CSP headers.
Memory leak containment on destroy ✓ Full GC on iframe removal When the iframe is removed from the DOM, the browser garbage-collects the entire browsing context (all Angular scopes, watchers, jQuery caches, template caches). In web component mode, destroy() replaces the custom element, but leaked references on the shared window (global event listeners, timers, cached $http interceptors) may persist.
Cookie and storage partitioning ✓ Predictable In web component mode, localStorage and sessionStorage are shared with the host page; key collisions are possible. The iframe uses the same origin's storage but runs in a separate execution context, preventing accidental key overwrites between host logic and module logic.
window.location mutation safety ✓ Cannot affect host In web component mode, any code that reads or writes window.location (for example, query-param utilities or router guards) operates on the actual host page URL. In iframe mode, window.location inside the module is scoped to the iframe's browsing context.
Error monitoring isolation ✓ Separate window.onerror / unhandledrejection Host page error monitoring (Sentry, Datadog, and so on) attaches to window.onerror. In web component mode, module errors trigger host monitors, producing noise. In iframe mode, module errors stay inside the iframe's error handlers.
Focus and keyboard event scoping ✓ Scoped to iframe In web component mode, keyboard shortcuts registered by the module (for example, Angular hotkey services) capture events on the host document. In iframe mode, keyboard events are naturally scoped to the iframe's document.
setTimeout/setInterval isolation ✓ Scoped to iframe context Timers created by the module in web component mode run on the host window. If the host clears all timers (common in SPA route transitions), module timers are destroyed. Iframe timers are independent.

Changelog

Date Change
2026-06-10 Compliance pass: added period to "vs." in two headings; changed "shows" to "displays"; fixed British spelling "initialises" in diagram note; converted Auto-Resize numbered list to bullets and moved non-mechanism items to prose; fixed subject-verb agreement in Feature Comparison intro; replaced "payloads" (jargon) with "data inputs"; fixed "The following table" to "The following sections" in Centauri Compatibility intro; changed "appear" to "display" in DynamicGrid row; changed "in" to "in" in Signature row; fixed sentence starting with inline code in EmbedDisplayController section. Readability: simplified "message-passing protocol" to "messaging system"; replaced "implements defense-in-depth" with plain-language description; replaced "orchestrates" with "manages". Rewrote "Components with Known Limitations in Web Component Mode" intro to clearly state all listed components work in iframe mode; removed redundant "Works in Iframe" column from the table.
2026-06-09 Editorial pass: updated metadata title to match H1, rewrote introduction for 9th-grade readability, removed all HR dividers between sections, fixed heading em dashes (colon substitution), fixed British spellings throughout (defence → defense, behaviour/behaviours → behavior/behaviors, sanitisation → sanitization, initialising → initializing), replaced all "through" with "through" or equivalent, added why-descriptions to all sections lacking prose intros, expanded table descriptions for non-experts, renamed behavioral headings to American English, updated See Also links (architecture reference to published path, added landing page link).
2026-04-21 Initial publication.