Skip to content

Add StyleSheetManager / target option to inject styles into another document (Electron child windows, iframes, shadow DOM) #64

@beautyfree

Description

@beautyfree

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 BrowserWindow rendered via <Portal mount={childWin.document.body}> — the child document never receives the goober sheet, so all styled components render unstyled there.
  • iframe content 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions