fix: stop plugin frames from painting a white backdrop in dark themes

This commit is contained in:
Linus Rath
2026-10-14 14:02:40 +02:00
parent 2ddc2bf00e
commit 9d7dc4fc4e
3 changed files with 32 additions and 0 deletions
@@ -95,6 +95,10 @@ export function PluginIframeSlot({ pluginId, slot, extraProps }: Props) {
const activeThemeId = useThemeStore((s) => s.activeThemeId);
useEffect(() => {
instanceRef.current?.setTheme(snapshotHostTheme());
// The store may apply the new theme to the page after this render: read
// it again a frame later, so the frame never keeps the old values.
const frame = requestAnimationFrame(() => instanceRef.current?.setTheme(snapshotHostTheme()));
return () => cancelAnimationFrame(frame);
}, [resolvedTheme, activeThemeId]);
if (show !== true) return null;
@@ -162,3 +162,25 @@ describe('plugin frame permissions', () => {
}
});
});
describe('plugin frame color scheme', () => {
// A frame whose document says dark while the iframe element says light gets
// an opaque white canvas from the browser: dark text tokens on white.
it('gives the iframe element the color-scheme the frame declares, at start and on a theme change', () => {
document.documentElement.classList.add('dark');
try {
const inst = createSlotInstance({
plugin: plugin(), slot: 'plugin-dialog', code: '', locale: 'en', tier: 'untrusted', extraProps: {},
hostContainer: document.body, onResize: () => {},
});
instances.push(inst);
expect(inst.iframe.style.colorScheme).toBe('dark');
inst.setTheme({ dark: false, fontFamily: 'sans-serif', vars: {} });
expect(inst.iframe.style.colorScheme).toBe('light');
inst.setTheme({ dark: true, fontFamily: 'sans-serif', vars: {} });
expect(inst.iframe.style.colorScheme).toBe('dark');
} finally {
document.documentElement.classList.remove('dark');
}
});
});
+6
View File
@@ -172,6 +172,11 @@ export class SandboxInstance {
this.iframe.setAttribute('allow', 'clipboard-write');
this.iframe.title = `plugin-${plugin.id}-${initPayload.mode}`;
this.iframe.style.border = 'none';
// The frame's document declares the theme's color-scheme (themeSnapshotToCSS).
// When the iframe element's own color-scheme differs, the browser paints
// an opaque canvas behind the frame (white in a dark theme), so the two
// are always set together.
if (initPayload.mode === 'slot') this.iframe.style.colorScheme = initPayload.theme.dark ? 'dark' : 'light';
this.iframe.style.display = 'block';
if (initPayload.mode === 'background') {
this.iframe.style.position = 'absolute';
@@ -377,6 +382,7 @@ export class SandboxInstance {
/** Push a new resolved theme so the slot iframe re-injects its theme CSS. */
setTheme(theme: ThemeSnapshot): void {
if (this.destroyed) return;
this.iframe.style.colorScheme = theme.dark ? 'dark' : 'light';
this.send({ type: 'theme-change', theme });
}