Skip to content

feat(plugins): inject host theme variables into plugin iframes#204

Open
Vinyl-Davyl wants to merge 1 commit intoNetflix:masterfrom
Vinyl-Davyl:feat/plugin-iframe-css-variables
Open

feat(plugins): inject host theme variables into plugin iframes#204
Vinyl-Davyl wants to merge 1 commit intoNetflix:masterfrom
Vinyl-Davyl:feat/plugin-iframe-css-variables

Conversation

@Vinyl-Davyl
Copy link
Copy Markdown

@Vinyl-Davyl Vinyl-Davyl commented Apr 13, 2026

Requirements for a pull request

  • Unit tests related to the change have been updated
  • Documentation related to the change has been updated

Description of the Change

Resolves the // TODO: ADD VARIABLES comment in src/components/Plugins/PluginSlot.tsx.

When a plugin sets useApplicationStyles: true, the existing implementation injects PLUGIN_STYLESHEET (the bundled theme.css text) into the iframe's <head>. However:

  1. Runtime theme overrides never reach the iframe. The injected stylesheet contains only the source-defined values. If the host applies a class on :root that overrides --color-bg-primary (the pattern that the in-flight dark-mode work in feat: theming, UI polish and dark mode integrations (#157) #190 will use), the iframe still sees the light default.
  2. Theme changes don't propagate. Once the iframe is rendered, the host can switch themes freely but the plugin stays stuck on the snapshot taken at register time.

This PR is a standalone fix for the // TODO: ADD VARIABLES gap and does not depend on #190 being merged — but it is the missing piece that lets future runtime theme switches (including dark mode, once #190 lands) actually reach plugin iframes.

Changes

File Purpose
src/components/Plugins/PluginSlot.tsx Adds an inline getThemeCSSVariables helper that walks accessible stylesheets, collects every --* custom property declared on :root/html, and resolves each to its currently computed value via getComputedStyle(document.documentElement) — returning a :root { ... } CSS string. Cross-origin stylesheets are silently skipped. Recurses into @media and @supports rule groups. PluginSlot then injects a second <style id="metaflow-plugin-theme-vars"> block containing the resolved variables alongside the existing PLUGIN_STYLESHEET. A MutationObserver watches <html> for class, style, and data-theme attribute changes; on any mutation, the iframe's variables <style> element has its textContent rewritten with fresh values. The observer is disconnected on unmount.

The helper is kept inline as a private module-scope function since it has a single call site and no other component needs it.

Why resolve at injection time instead of injecting raw CSS?

document.styleSheets only gives access to source declarations. To capture the value the host is actually rendering with (after cascade, runtime overrides, inline <html style>, etc.), we have to read getComputedStyle(document.documentElement).getPropertyValue(...). This is the only way to honor runtime theme state.

Alternate Designs

  • postMessage-based theme push. Would require defining a new plugin protocol message (THEME_UPDATE) and updating every plugin to listen for it. Rejected because plugins already opt-in to host styles via useApplicationStyles: true; they should not need plugin-side code changes.
  • Re-render the entire iframe on theme change. Simpler but causes flicker, lost plugin state, and re-runs the plugin's bootstrap code on every theme toggle. The targeted <style> rewrite is non-destructive.
  • Inject only a hand-picked allowlist of variables. Rejected because the design token set in theme.css evolves; an allowlist would silently drift out of sync. Walking the actual stylesheets stays correct automatically.
  • Extracting the helper to its own file. Considered, but since the function is only used by PluginSlot.tsx and is self-contained, keeping it inline reduces file count and keeps the related logic colocated.

Possible Drawbacks

  • Walking document.styleSheets and computing variable values at registration time is O(rules + custom-props). In practice this is a few hundred properties resolved once per plugin mount and once per host theme change — well below any noticeable threshold.
  • The MutationObserver is scoped to document.documentElement with a narrow attributeFilter (class, style, data-theme), so it does not fire on unrelated DOM mutations.
  • Cross-origin stylesheets throw SecurityError when accessed via cssRules. The helper catches this silently — those sheets are skipped, which matches the existing behavior since the bundled theme.css is same-origin.

Verification Process

  1. Ran npx tsc --noEmit — zero TypeScript errors.
  2. Ran npx prettier --write and npx eslint on the modified file — clean.
  3. Manually tested with the example plugin under plugin-api/:
    • Started the dev server with yarn dev:plugin.
    • Confirmed the plugin iframe's <head> now contains two <style> elements: the existing PLUGIN_STYLESHEET and the new metaflow-plugin-theme-vars block.
    • Inspected the second <style> element via DevTools — confirmed it contains :root { --color-bg-primary: ...; --color-text-primary: ...; ... } with values matching getComputedStyle(document.documentElement) on the host.
  4. Simulated a runtime theme change by manually toggling a class on <html> in DevTools and overriding a variable — confirmed the iframe's <style id="metaflow-plugin-theme-vars"> element updates immediately, with no iframe reload and no lost plugin state.

Quick Navigation Summary

Test Location What to look for
Variables injected Plugin iframe <head> in DevTools New <style id="metaflow-plugin-theme-vars"> element
Live sync works Edit <html class> in host DevTools Iframe <style> updates immediately
TODO resolved src/components/Plugins/PluginSlot.tsx No more // TODO: ADD VARIABLES
No regression for non-themed Plugin without useApplicationStyles Iframe <head> unchanged

Release Notes

Plugins using useApplicationStyles: true now receive the host's resolved CSS theme variables and stay in sync when the host theme changes at runtime — enabling plugins to participate in future theme switches (including the dark mode being added in #190) without per-plugin code changes.

@Vinyl-Davyl
Copy link
Copy Markdown
Author

cc: @saikonen @romain-intel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant