Skip to content

Patterns · Technical infrastructure

EmbeddedAppFrame

Embed a page of another Rook app: sandboxed iframe, ready handshake, auto-height and focus handoff with origin checks.

@rookcom/monarch-react/shellPattern

An embedded settings page (ready, auto-height)

Usage examples

A page that never answers

Purpose

Rook ID’s App Settings hub shows each app’s settings pages inside its own layout. EmbeddedAppFrame is the host side of that embed contract (v1), and useEmbedReady is the embedded side, so every app speaks the same protocol instead of hand-writing postMessage code.

Use when

  • Showing a page that belongs to another Rook app inside this one (App Settings, Integrations).
  • In the embedded app: call useEmbedReady in the layout of pages that may be framed.

Avoid when

  • Embedding Rook Chat: use ChatDock, which speaks the rook-chat-embed protocol.
  • Third-party content you do not control: keep the default sandbox and add nothing to allow, or link out instead.

Design principles

  • Closed by default: a minimal sandbox (scripts, its own session, forms), no permissions, and messages accepted only from the frame’s own window at an allowed origin.
  • Never silent: a page that does not say ready within 12 seconds of visible time shows an error with Try again and an open-in-app link instead of an endless spinner.
  • The frame fits its content (auto-height), so the host page scrolls once instead of a box inside a box.
  • Focus can cross the boundary on purpose: into the page when it is ready, and back out when the page hands it back.

API reference

import { APP_EMBED_PROTOCOL, APP_EMBED_VERSION, DEFAULT_EMBED_SANDBOX, EmbeddedAppFrame, MAX_EMBED_HEIGHT, decodeAppEmbedChildMessage, decodeAppEmbedHostMessage, encodeAppEmbedMessage, useEmbedReady } from '@rookcom/monarch-react/shell';
ExportKindRenderingRoot class
APP_EMBED_PROTOCOLutilityserver-safe–
APP_EMBED_VERSIONutilityserver-safe–
DEFAULT_EMBED_SANDBOXutilityclient–
EmbeddedAppFramecomponentclient.mn-embedded-app-frame
MAX_EMBED_HEIGHTutilityserver-safe–
decodeAppEmbedChildMessageutilityserver-safe–
decodeAppEmbedHostMessageutilityserver-safe–
encodeAppEmbedMessageutilityserver-safe–
useEmbedReadyhookclient–

Forwards its ref to the wrapper; iframeRef reaches the iframe.

Also exported: useEmbedReady (embedded side), APP_EMBED_PROTOCOL, APP_EMBED_VERSION, DEFAULT_EMBED_SANDBOX, encodeAppEmbedMessage, decodeAppEmbedChildMessage, decodeAppEmbedHostMessage.

Generated from the TypeScript declarations of packages/react/src/patterns/technical/embedded-app-frame.

Properties

allowedOrigins defaults to the origin of src. Entries that are not a bare origin (*, a path, null) are dropped and never match.

sandbox takes tokens, not a string; allow-top-navigation is not accepted.

readyTimeout={0} waits forever (use only when the page shows its own errors).

EmbeddedAppFrame

Embeds a page of another Rook app (App Settings embed contract v1): an iframe with a minimal sandbox, a ready handshake, auto-height and focus handoff over postMessage, and loading and error states with Try again. Only messages from the frame's own window at an allowed origin are read, and nothing is ever posted to `*`.

PropTypeDescription
src requiredstringThe page to embed (absolute URL, or relative to this page).
title requiredstringThe iframe's accessible name, e.g. "Rook Tickets: Email settings". Required.
allowreadonly string[]Permissions policy features, e.g. `['clipboard-write']`. Default none.
allowedOriginsreadonly string[]Exact origins whose messages are accepted. Default: the origin of `src` only. Entries that are not a bare origin (`*`, paths) are ignored.
autoHeightbooleanFollow the page's `resize` messages. Default true.
classNamestring
focusOnReadybooleanMove focus into the page once it is ready. Default false.
heightstring | numberHeight before the first `resize` message, or the fixed height without `autoHeight` (a number of pixels or any CSS length). Default 480.
idstring
iframeRefRef<HTMLIFrameElement>The iframe element.
loadingContentReactNodeReplaces the loading content.
maxHeightnumberLargest auto height in pixels; the page scrolls inside the frame beyond it.
messagesPartial<EmbeddedAppFrameMessages>Interface text, for translation.
minHeightnumberSmallest auto height in pixels. Default 120.
onFocusExit((direction: EmbedFocusDirection) => void)The page handed focus back (`focus-exit`). Default: focus moves to the control after (forward) or before (backward) the frame.
onReady(() => void)
onStatusChange((status: EmbeddedAppFrameStatus) => void)
openHrefstringLink offered with the error (the page in its own app). Opens in a new tab.
protocolstringProtocol name in every message. Default `rook-app-settings` (App Settings contract v1).
readyTimeoutnumberGive up after this much visible time without `ready`. Default 12000 ms; 0 waits forever.
referrerPolicyimport("/home/runner/work/monarch/monarch/node_modules/@types/react/index").HTMLAttributeReferrerPolicyDefault `strict-origin-when-cross-origin`.
sandboxreadonly EmbedSandboxToken[]Sandbox tokens. Default `DEFAULT_EMBED_SANDBOX` (scripts, same-origin, forms).

Variants

PropValuesDescription
autoHeight'true''false'Follow resize messages between minHeight and maxHeight, or keep the fixed height.
protocol'rook-app-settings''custom'The protocol name in every message, for sibling contracts.

States

StateExposed asBehaviour
Loadingdata-status="loading"A status with a spinner covers the frame until the page says ready.
Readydata-status="ready"The page is shown; onReady runs once.
Errordata-status="error"No ready in time: an alert with Try again (reloads the frame) and the open-in-app link. The frame is hidden from assistive technology.

Interaction behaviour

  • On every load the host posts ping, so a page that announced ready before the host listened says it again.
  • Try again remounts the iframe and keeps focus on the loading status instead of losing it to the page.
  • The countdown pauses while the tab is hidden, since background tabs throttle the embedded page.
  • A focus-exit message moves focus to the control after (forward) or before (backward) the frame, or calls onFocusExit.

Keyboard support

KeysAction
TabMoves into and through the embedded page, then on to the host page.
Enter / SpaceTry again and the open link, in the error state.

Keys inside the embedded page belong to that page. With useEmbedReady, the page can call exitFocus (for example on Escape) to hand focus back.

Accessibility

Role
iframe (document) in a wrapper; status while loading, alert on error
Accessible name
title names the iframe, e.g. "Rook Tickets: Email settings".
  • The loading overlay is a status; the error is an alert so it is announced when it replaces the spinner.
  • The open link says it opens in a new tab.
  • Security: event.origin must be in allowedOrigins (exact match) and event.source must be this frame’s window; each message is validated against its exact shape (ready has exactly three keys, resize a finite height under 100000), and anything else is dropped.
  • Security: the host posts only to the origin of src, and only when it is allowed; nothing is posted to *. useEmbedReady posts only to an allowed parent origin (from ancestorOrigins or the referrer) and reads messages only from window.parent at that origin.
  • Threat model: a hostile page in another frame or tab cannot spoof ready or resize the frame (origin and source checks); a hijacked embedded page cannot navigate the host (no top navigation), open popups, download files or use camera, microphone or clipboard unless the host opts in. allow-scripts together with allow-same-origin is only safe for cross-origin pages: never point the frame at a page on the host’s own origin with untrusted content.

Responsive behaviour

  • The frame is as wide as its container; its height follows the page within minHeight and maxHeight.
  • Loading and error content is centred and wraps at 320px.

Theme support

  • Surface, border and text tokens for the wrapper and overlays. The embedded page draws itself; Rook apps share the theme cookie, so both sides match.
  • The height transition is removed under reduced motion.

Do and don't

Do

  • List exact origins in allowedOrigins, from configuration, per environment.
  • Announce ready only when the embedded page can actually be used (signed in, workspace resolved, or the no-access screen).
  • Give every frame a specific title.

Don't

  • Do not add allow-top-navigation, allow-popups or permissions unless the embedded page needs them.
  • Do not post to * or accept * as an origin, on either side.
  • Do not put secrets or tokens in the frame URL.
  • ChatDock: The Rook Chat frame, with its own protocol.
  • SupportPanel: Support chat beside the rail.
  • NoAppAccess: What the embedded page shows (and announces ready for) without access.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsButton, Spinner, VisuallyHidden

Testing coverage

  • Titled iframe with the default sandbox, referrer policy and no permissions; loading status.
  • Ready from the frame’s window at an allowed origin; onReady once.
  • Drops other origins, look-alike origins, other windows, extra keys, other versions and protocols.
  • * and paths in allowedOrigins never match.
  • Resize within min and max; fixed height without autoHeight.
  • Timeout error with Try again (new frame, focus kept) and the new-tab open link.
  • Ping on load to the page’s own origin; focus on ready; focus-exit forward, backward and onFocusExit.
  • Unknown sandbox tokens and bad permission names dropped; readyTimeout 0 waits.
  • Protocol encode/decode both ways; useEmbedReady: inert when not framed, ready to an allowed parent only, ping answered, exitFocus.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/embedded-app-frame; Cross-browser axe: /components/embedded-app-frame

Tests: packages/react/src/patterns/technical/embedded-app-frame