# Host

Host-side SDK for embedding hyperfrontend features: shell factory, display modes, iframe utilities, and lifecycle.

```ts
import { builtInDisplayModes, createShell, DisplayMode } from '@hyperfrontend/features/host'

const shell = createShell({
  modes: builtInDisplayModes,
  url: 'https://features.example.com/clock',
  container: '#clock-slot',
  displayMode: DisplayMode.Embedded,
})

shell.on('open', () => console.log('feature connected'))
shell.on('tick', (time) => console.log('feature said', time))

shell.open()
shell.send('set-timezone', { tz: 'UTC' })
```

## API

| Export                                                                                                       | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createShell`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-createShell)                 | Build a shell handle from an explicit [`modes`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-CreateShellOptions-prop-modes) map: only the mounts you pass ship.                                                                                                                                                                                                                                                                                                                                                                |
| [`builtInDisplayModes`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-builtInDisplayModes) | The all-modes map, for hosts that want every mode available.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [`mountEmbedded`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-mountEmbedded) …           | The four mount functions ([`mountEmbedded`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-mountEmbedded), [`mountDialog`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-mountDialog), [`mountPopup`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-mountPopup), [`mountStandalone`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-mountStandalone)) for the [`modes`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-CreateShellOptions-prop-modes) map. |
| [`DisplayMode`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode)                 | The four built-in modes: [`Embedded`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode), [`Dialog`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode), [`Popup`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode), [`Standalone`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode).                                                                                                                                                 |
| [`ShellHandle`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)                 | Type of the handle returned by [`createShell`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-createShell).                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [`CreateShellOptions`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-CreateShellOptions)   | Options accepted by [`createShell`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-createShell) ([`ShellOptions`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions) plus the [`modes`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-CreateShellOptions-prop-modes) map).                                                                                                                                                                                                          |
| [`ExperiencePlugin`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ExperiencePlugin)       | Opt-in extension point for layering transitions/animations onto display modes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

The [`modes`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-CreateShellOptions-prop-modes) map is how unused mode code stays out of bundles: a generated shell passes exactly the modes its feature declared, and a hand-written host that only ever embeds can pass `{ embedded: mountEmbedded }` and ship one mode. Opening a mode outside the map throws, naming the supported set.

The shell wraps a [`@hyperfrontend/nexus`](https://www.hyperfrontend.dev/docs/libraries/nexus.md) broker: [`send`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) emits a contract action to the feature, and [`on`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) subscribes to feature messages and the [`open`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`closing`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`close`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`error`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`status`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`dirty-state`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`reopen`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle)/[`dismiss`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) lifecycle events. [`close`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) disconnects the channel politely (the feature gets a [`closing`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) flush window, and [`isDirty`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle-prop-isDirty) reports declared unsaved work first); [`destroy`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) also releases the DOM.

Opening is asynchronous: [`isOpen`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle-prop-isOpen) stays `false` and sends queue until the wire handshake with the feature completes, flushing on the [`open`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) event. If the feature never completes the handshake within [`openTimeoutMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-openTimeoutMs) (default 10 s), the shell tears the mount down and emits [`error`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) with `reason: 'open-timeout'`.

Every shell-originated [`error`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) payload carries a reason discriminator:

| reason               | Emitted when                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Payload beyond reason                                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `'open-timeout'`     | The handshake does not complete within [`openTimeoutMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-openTimeoutMs).                                                                                                                                                                                                                                                                                                           | [`elapsedMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle), [`displayMode`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode)                                                                                                                                                                                                                                         |
| `'open-failed'`      | The display mode cannot produce a feature window at all (e.g. a blocked popup).                                                                                                                                                                                                                                                                                                                                                                                      | [`displayMode`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode)                                                                                                                                                                                                                                                                                                                                     |
| `'target-lost'`      | A window the shell opened stops being reachable before the handshake completes.                                                                                                                                                                                                                                                                                                                                                                                      | [`elapsedMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle), [`displayMode`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode)                                                                                                                                                                                                                                         |
| `'unresponsive'`     | The heartbeat watchdog trips under the [`emit`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsivePolicy), [`unmount`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsivePolicy), or [`reopen`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsivePolicy) [`onUnresponsive`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-onUnresponsive) policy. | [`missedBeats`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsiveInfo-prop-missedBeats), [`lastBeatAt`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsiveInfo-prop-lastBeatAt), [`displayMode`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode), [`frame`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsiveFrame) |
| `'reopen-exhausted'` | The [`reopen`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsivePolicy) policy spent its [`attempts`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ReopenOptions-prop-attempts) and the feature is still silent; the shell has torn it down.                                                                                                                                                                           | [`attempts`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ReopenOptions-prop-attempts), [`displayMode`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode)                                                                                                                                                                                                                          |

`'target-lost'` has two possible causes and the browser gives the host no way to tell them apart: the visitor closed the window, or the feature document carried `Cross-Origin-Opener-Policy: same-origin` and the browser severed the opener as it loaded. The [`elapsedMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) is what separates them in practice. A severed opener is reported as the document commits, typically within a few hundred milliseconds of `open()`; a visitor reaching for the close control takes far longer. A short [`elapsedMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) on a popup or standalone session is worth reading as an isolation problem on the feature origin rather than a dismissal.

The watchdog cannot tell a frame that died from one that stalled: a renderer process the browser or the operating system killed (routine on phones) goes silent without any signal the embedding page can observe, exactly like a frame whose main thread is starved. The `'unresponsive'` error says what the host _can_ see through [`frame`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-UnresponsiveFrame): `'gone'` when the iframe left the page or the window was closed, `'present'` otherwise. Only time separates the two `'present'` cases, and the `'reopen'` policy spends that time for you:

```ts
const shell = createShell({
  modes: builtInDisplayModes,
  container: '#clock',
  url: 'https://clock.example.com',
  onUnresponsive: {
    reopen: { graceMs: 4000, backoff: 3, attempts: 3, stableMs: 60_000 },
  },
})
shell.on('reopen', (data) => console.info('reviving the clock', data))
```

A frame that beats again within [`graceMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ReopenOptions-prop-graceMs) was only stalled and keeps its session. One that stays silent has its mount (the browser's crash placeholder included) replaced by a fresh one opened with the same options, announced by a `'reopen'` event carrying `{ attempt, attempts, displayMode }`. Each further death in the episode waits [`backoff`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ReopenOptions-prop-backoff) times longer, and a reopened session that stays open for [`stableMs`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ReopenOptions-prop-stableMs) restores the whole budget. `'reopen'` alone takes every default shown above.

Nothing is reopened into a hidden page: the attempt waits for the visitor to return, then allows a fresh grace. A reopened session that never completes its handshake counts as another death. The policy stands down for a `'gone'` frame (the shell tears it down instead, since your page or the visitor removed it), when you call [`close`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle), [`destroy`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle), or [`open`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) yourself, and when the feature closes the session.

A frame whose renderer died is painted over by the browser's own crash placeholder, which your styles cannot reach. Set [`concealUnresponsive`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-concealUnresponsive) to hide the frame from the verdict until its next beat or the next session opens. The trade-off: a feature that merely stalls past the miss budget also disappears until it beats again.

A feature that reloads itself (a refresh, an in-frame navigation, a dev-server rebuild) ends its session but keeps its mount: [`close`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) fires with `{ reason: 'peer-reload' }`, then [`open`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle) fires again once the new document completes its own handshake, and the shell re-announces the presentation to it. Treat the pair as a session boundary: pending requests reject, [`isDirty`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellHandle-prop-isDirty) resets, and anything you sent the previous document needs sending again. To refuse the reload instead, `destroy()` on that reason.

## Display modes and sizing

The host owns presentation. It picks the display mode (from the set the feature's contract declares), announces it to the feature once per mount (the announcement already carries the frame's initial dimensions, so the feature lays itself out without waiting for a second message), and is the single authority on frame geometry: every dimension crosses the boundary as an exact pixel value, host to feature, never the other way.

Mounted is not displayed: the frame mounts hidden and is revealed only once the session opens, so the user never sees (or clicks into) a frame whose feature is not ready. A dialog pane cannot intercept the page while it is still connecting; an embedded frame reserves its box without painting.

**Embedded** mounts the frame inline in your [`container`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-container) and the frame fills the container's content box: measured before the frame is inserted, then observed with a `ResizeObserver`; every later change is reported to the feature. While the container has no measurable size (hidden, not yet laid out, or nothing gives it a height), the SDK applies a dynamic viewport-derived fallback so the embed is never invisible by accident; the fallback retires as soon as your layout takes over.

A feature with intrinsic dimensions can bake fixed [`embedWidth`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-embedWidth)/[`embedHeight`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-embedHeight) instead (or you can pass them), then the frame gets exactly those pixels and you place the container somewhere they fit; the SDK never distorts a fixed agreement.

**Dialog** is a full-viewport transparent pane layered above your page. The feature draws its dialog box (and any backdrop paint) inside the pane; [`dialogWidth`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-dialogWidth)/[`dialogHeight`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-dialogHeight) set the inner box (viewport-derived when unset) and [`dialogPosition`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-dialogPosition) places it: [`center`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-BoxPosition) by default, or any edge/corner (`top-left` … `bottom-right`). The feature detects backdrop clicks and in-frame Escape presses and reports them as a dismiss signal; [`dialogBackdrop`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-dialogBackdrop) decides what the shell does: [`close`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-BackdropBehavior) (default) starts the polite teardown, [`event`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-BackdropBehavior) emits a [`dismiss`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-BackdropBehavior) event for you to handle, [`none`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-BackdropBehavior) ignores it. [`closeOnEscape`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-closeOnEscape) covers Escape from both documents.

```ts
shell.open({
  displayMode: DisplayMode.Dialog,
  dialogWidth: 480,
  dialogPosition: 'top-center',
  dialogBackdrop: 'event',
})
shell.on('dismiss', ({ source }) => console.log('backdrop interaction', source))
```

**Popup** opens a separate window at [`popupWidth`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-popupWidth)/[`popupHeight`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-popupHeight) (viewport-derived when unset), placed on the screen per [`popupPosition`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-popupPosition) ([`center`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-BoxPosition) by default, or any edge/corner). After that the window belongs to the browser and the user: they move and resize it freely, and no frame geometry crosses the boundary. The window's title and chrome are not the host's to set: the title comes from the loaded document (the feature sets its own `document.title`), and browsers ignore chrome flags like resizability for `window.open`. **Standalone** is a plain new tab: the simplest mode, deliberately free of presentation coordination.

Both windowed modes depend on the opener relationship surviving the feature document's load, and a cross-origin isolated feature origin ends it. [`crossOriginIsolated`](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) (the precondition for `SharedArrayBuffer` and `performance.measureUserAgentSpecificMemory`) requires `Cross-Origin-Opener-Policy: same-origin`, and a window opened onto such an origin keeps its opener only when the opener is same-origin **and** itself isolated. Every other pairing puts the new window in its own browsing-context group: `window.opener` is `null` inside it, messages in both directions are discarded without error, and the session times out while the feature renders perfectly on screen.

Note that `same-origin-allow-popups` does not widen this, because it governs popups the document _opens_, not the opener it keeps when it _is_ opened. Declare [`isolation`](https://www.hyperfrontend.dev/docs/libraries/features/#api-FeatureConfig-prop-isolation) in `feature.config.*` and the toolchain composes only the modes the origin can serve; the type narrowing means declaring isolation alongside [`popup`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode) or [`standalone`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode) does not compile.

Transparency is on by default in both iframe modes ([`allowtransparency`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) plus a matched `color-scheme` pin on both sides; a mismatch would force an opaque canvas), so features can render non-rectangular designs, blend with your UI, and paint dialog backdrops. Want an opaque embed? Give your container a background.

### What the SDK deliberately does not do

- **Content-driven growth** (the embed grows with its content) is not built in: it would hand geometry authority to the feature, which is the inversion this design exists to avoid. The recipe is one contract action: have the feature emit its content height as product data, and set it on the container you own (`shell.on('contentHeight', (px) => setContainerHeight(px))`); the SDK's container observation propagates the change back down.
- **Clipping and scrolling** are not managed. The feature owns its document's overflow behaviour; the host owns the container's. Neither needs a protocol.
- **Draggable/resizable dialog boxes** are not built in, but the full-viewport pane makes them a pure feature-side concern: the pane never moves, so the feature can drag or resize its inner box with ordinary CSS/pointer code and nothing needs to cross the boundary. If your host UI needs to know, carry position as an ordinary contract action.

## Browser capabilities

Capability is one of the two axes in the [Security Model](https://www.hyperfrontend.dev/docs/core-concepts/security/): read it for where these options sit relative to what the browser enforces and what stays the operator's job.

Two [`ShellOptions`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions) fields govern what the feature frame may do with the browser around it, both applied before the frame loads (the only moment they take effect) and both scoped to the iframe modes: [`popup`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode) and [`standalone`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode) open top-level windows, which ask the user for permissions directly.

[`permissions`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-permissions) delegates Permissions-Policy features (camera, fullscreen, clipboard, …) to the frame via the iframe [`allow`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#allow) attribute. Browsers deny these to cross-origin frames by default, so a feature that needs one only works when the host delegates it. A generated shell bakes the needs the feature declared at build time (also disclosed in its README and `metadata.json`); a host-supplied list replaces the baked one entirely.

[`sandbox`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-ShellOptions-prop-sandbox) is the host's containment lever and is never baked by a build. `true` (or an opt-in object) starts the frame from the browser's deny-all sandbox, and the SDK manages the two hazardous tokens itself:

- `allow-scripts` is always granted, because the feature runtime is JavaScript.
- `allow-same-origin` is granted only to cross-origin feature URLs, since a same-origin frame holding both tokens could remove its own sandbox. A sandboxed same-origin feature therefore runs with an opaque origin (no cookies or storage) while the messaging protocol still connects.
- Everything else ([`forms`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-SandboxOptions-prop-forms), [`popups`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-SandboxOptions-prop-popups), [`modals`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-SandboxOptions-prop-modals), [`downloads`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-SandboxOptions-prop-downloads), [`topNavigationByUserActivation`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-SandboxOptions-prop-topNavigationByUserActivation)) is denied unless opted in.

Requesting a sandbox on [`popup`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode) or [`standalone`](https://www.hyperfrontend.dev/docs/libraries/features/host/#api-DisplayMode) throws, because no containment can apply to a top-level window.

```ts
shell.open({
  permissions: ['fullscreen', 'clipboard-write'],
  sandbox: { downloads: true },
})
```

---

Canonical page: https://www.hyperfrontend.dev/docs/libraries/features/host/
This file: https://www.hyperfrontend.dev/docs/libraries/features/host.md
Documentation index: https://www.hyperfrontend.dev/llms.txt
