Repository navigation
Add StyleSheetManager / target option to inject styles into another document (Electron child windows, iframes, shadow DOM) #64
Description
Summary
There is no public API to make styled / css / createGlobalStyles inject their generated <style> into a document other than the main document.head. styled-components solves this with <StyleSheetManager target={node}>. solid-styled-components has no equivalent, which breaks any multi-document rendering:
- Electron child
BrowserWindowrendered via<Portal mount={childWin.document.body}>— the child document never receives the goober sheet, so allstyledcomponents render unstyled there. iframecontent portals.- Shadow DOM roots.
- Pop-out windows opened with
window.open.
Why this is almost already supported
The internals already thread a target through to goober:
// src/index.js — makeStyled
function makeStyled(tag) {
let _ctx = this || {};
return (...args) => {
const Styled = props => {
...
let className = css.apply(
{ target: _ctx.target, o: append, p: withTheme, g: _ctx.g },
args
);
...goober's css/glob honor this.target and write the sheet into that node's root (document.head or a shadow/other-document root). But styled is exported as:
export const styled = new Proxy(makeStyled, {
get(target, tag) { return target(tag); } // makeStyled called with `this === undefined`
});so _ctx.target is always undefined → everything lands in the main document.head. There is no way for a consumer to set it.
Reproduction
import { render, Portal } from "solid-js/web";
import { styled } from "solid-styled-components";
const Box = styled.div`background: tomato; width: 100px; height: 100px;`;
// open a second window/document
const win = window.open("about:blank")!;
render(() => (
<Portal mount={win.document.body}>
<Box /> {/* renders, but the .go<hash> rule is injected into the MAIN
document's <style>, never into win.document → unstyled */}
</Portal>
), document.getElementById("root")!);The <div class="go…"> appears in win.document, but the matching CSS rule is only in the opener document's <head>, so the box is unstyled in the child window.
keyframes and glob/createGlobalStyles have the same limitation (they also inject into the default sheet).
Proposed API
Mirror styled-components:
import { StyleSheetManager } from "solid-styled-components";
<Portal mount={win.document.body}>
<StyleSheetManager target={win.document.head}>
<App />
</StyleSheetManager>
</Portal>All styled / css.class / createGlobalStyles rendered inside the provider inject into target instead of document.head.
Sketch
A context carrying the target, read inside Styled (and the createGlobalStyles component), passed through to the existing css.apply({ target, … }) call:
const StyleTargetContext = createContext();
export function StyleSheetManager(props) {
return createComponent(StyleTargetContext.Provider, {
value: props.target,
get children() { return props.children; }
});
}
// inside Styled():
const target = useContext(StyleTargetContext) ?? _ctx.target;
let className = css.apply({ target, o: append, p: withTheme, g: _ctx.g }, args);Because goober already routes by target, this is a small, backward-compatible change (no behavior change when no provider is present).
Workaround today
We currently mirror every <style>/<link> from the opener document.head into the child window's head with a MutationObserver, and additionally shim keyframes so @keyframes bodies aren't dropped. It works but is fragile and duplicates the whole sheet per window.
Offer
Happy to send a PR implementing StyleSheetManager (context-threaded target) plus keyframes/glob target support, with tests. Filing this first to confirm the API shape (<StyleSheetManager target> vs a setup-level option) you'd prefer.
Activity