<!-- GENERATED by scripts/build-llms.mjs from llms/media.md — do not edit this file. -->

# `lr-zoomable-frame`

- **Import** `import '@aceshooting/lyra-ui/components/lr-zoomable-frame.js';` (stable tag alias; registers the tag)
- **Class** `LyraZoomableFrame`, also available unregistered from `@aceshooting/lyra-ui/components/media/zoomable-frame/zoomable-frame.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 4 parts, 2 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-zoomable-frame`

Sandboxed iframe preview that mirrors Web Awesome's zoomable-frame contract. It scales a real
`<iframe>` through discrete controls without changing the document's own viewport, and fills its
allocated inline size with a 16:9 aspect ratio by default (override `aspect-ratio` on the host).

Focus on either zoom control does not enter the iframe browsing context, and `blur()` does not blur
a focused zoom control.

**Properties:**

- `src: string = ''` — iframe URL. Relative, `http:`, `https:`, `blob:`, and exact `about:blank`
  values are accepted; active `data:`/`javascript:` and non-embeddable schemes are omitted.
- `srcdoc: string = ''` — inline iframe document. A present `srcdoc` wins over `src`, including an
  explicitly empty `srcdoc` attribute.
- `allowfullscreen: boolean = false`, `loading: LyraZoomableFrameLoading = 'eager'`
  (`'eager' | 'lazy'`),
  `referrerpolicy: string = ''`, and `sandbox: string = 'allow-same-origin'` forward the native
  iframe controls after validation. Invalid loading becomes `eager`; an invalid non-empty referrer
  policy becomes `no-referrer`.
- `zoom: number = 1` (reflected) — current scale. Finite programmatic values do not have to occur
  in `zoomLevels`; unsafe/non-finite layout values render as a finite positive fallback.
- `zoomLevels: string = '25% 50% 75% 100% 125% 150% 175% 200%'` (attribute `zoom-levels`) —
  decimal/percentage stops used by the controls. The cached projection reads at most the first
  16,384 UTF-16 code units and 256 whitespace-delimited tokens before deduplicating and sorting;
  a token cut by the source ceiling is ignored, and later source text cannot affect the controls.
- `withoutControls: boolean = false`, `withoutInteraction: boolean = false`, and
  `withThemeSync: boolean = false` (reflected attributes `without-controls`,
  `without-interaction`, `with-theme-sync`) — respectively remove the zoom controls, remove pointer and
  sequential-keyboard iframe interaction by making the browsing context native `inert`, and opt
  into best-effort same-origin theme sync. The inert frame also refuses programmatic focus/click
  and carries no unsupported `aria-disabled` claim.
- `accessibleLabel: string | null` (attribute `aria-label`) — a declarative attribute remains on
  the host while the iframe gets its localized purpose title, avoiding a duplicate name on two
  semantic owners. A property-only value names the iframe and updates reactively without creating a host attribute. Explicit empty host naming is preserved
  as an empty iframe title rather than replaced through truthiness.
- readonly `iframe?: HTMLIFrameElement`, `contentWindow: Window | null`, and
  `contentDocument: Document | null`. Both content accessors return `null` while detached;
  `contentDocument` also returns `null` across an origin boundary.

**Authoring type:** `LyraZoomableFrameLoading`. The former unprefixed
`ZoomableFrameLoading` name is removed in v9 rather than retained as an alias.
The former deep-class-module implementation exports `DEFAULT_ZOOM_LEVELS`,
`DEFAULT_IFRAME_SANDBOX`, `safeZoomableFrameSrc()`, and `safeZoomableFrameSandbox()` are also
removed in v9. They were never part of the registration, root, or documented component surface;
configure the corresponding public properties instead of depending on sink-policy internals.

**Methods:** `zoomIn()` selects the nearest configured level above the current value;
`zoomOut()` selects the nearest below it. The control group also accepts `+`/`=` and `-`/`_` while one
of its controls has focus. `focus(options?)`, `blur()`, and `click()` forward to the internal
iframe — the component's primary interactive surface — only while the component is connected and
interaction is enabled. Under `without-interaction` they are deliberate no-ops rather than an
escape around native `inert`; the two-button zoom-control group has no single primary action. Both
buttons are ordinary independent Tab stops inside a labelled `role="group"`; the container does not
claim the roving-arrow-key contract of an ARIA toolbar.

**Slots:** `zoom-in-icon` and `zoom-out-icon` replace the decorative control glyphs. Their
flattened subtrees are always inert and hidden from assistive technology, so use an SVG or glyph
rather than a second interactive control; the native zoom buttons remain the sole focus and pointer
actions.

**Events:** internal `focus`/`blur` from the iframe are relayed exactly once as owner-realm native
`FocusEvent`s (bubbling and composed, preserving external `relatedTarget`; a transition to or from an internal zoom control uses `null` because retargeting that control to the host would suppress the relay);
native `load` and `error` are relayed exactly once from the current iframe
generation as non-bubbling, non-composed `Event` instances. Navigation/source-policy changes
replace the iframe, so a late event from an earlier document is ignored; detached frames do not
notify.

**CSS parts:** `iframe`, `controls`, `zoom-in-button`, and `zoom-out-button`. Real focus entry into
the browsing context (Tab, pointer, or `focus()`) exposes `data-frame-focused` on the host and paints
the shared focus ring around the iframe boundary; blur, navigation rekey, disablement, and removal
clear it. Moving between the iframe and a zoom control clears or restores the marker and emits one matching native blur or focus relay; focusing a zoom control alone leaves the marker absent.

**CSS custom properties:** read-only `--lr-zoomable-frame-zoom`, resolved from the `zoom`
property and applied to the internal iframe scale; and `--lr-zoomable-frame-control-hover-background`
(default `var(--lr-color-brand-quiet)`), which colors a zoom control on hover and supplies the base
for its active color.

**RTL behavior:** the scaled iframe is a physical canvas and remains pinned to physical top-left in
both directions. Its zoom controls remain logical interface chrome, so RTL places the control group at
inline-end (the physical left edge).

**Security and theme sync:** the iframe always keeps a `sandbox` attribute. The secure Lyra default
allows same-origin access but not scripts, forms, popups, downloads, or top navigation. Supplied
sandbox tokens are allowlisted; if both `allow-scripts` and `allow-same-origin` are requested, the
latter is dropped so framed script cannot escape a same-origin sandbox. `with-theme-sync` never
widens those permissions: when the document is accessible it copies only Lyra theme-selector
classes, theme attributes, computed `--lr-theme-*` inputs, and `color-scheme`; cross-origin
documents remain untouched. Turning `with-theme-sync` off restores only the iframe classes,
attributes, and inline properties Lyra changed, preserving any later iframe-owned edits. Changing
a watched host-page theme attribute syncs again.

```js
import "@aceshooting/lyra-ui/components/media/zoomable-frame/zoomable-frame.js";
```

```html
<lr-zoomable-frame
  aria-label="Component preview"
  srcdoc="<!doctype html><html><body><h1>Preview</h1></body></html>"
  zoom="0.75"
  with-theme-sync
></lr-zoomable-frame>
```

---
