Support inserting external CSS without transforming or linting the source
The problem
Emotion has no way to take a complete third-party stylesheet and insert it as-is.
<Global> always runs the string through serialize and Stylis. In development it also lints selectors such as :first-child and prints The pseudo class ":first-child" is potentially unsafe when doing server-side rendering. That warning is aimed at CSS-in-JS that Emotion itself authored. It is the wrong layer for a vendor .css file that the app must not edit.
A component library that ships another package's CSS (for example a playground that injects dialkit/styles.css) should not rewrite that file, should not sprinkle ignore comments through it, and should not minify it so that an undocumented comment flag happens to match. Doing any of that mixes ownership. The vendor owns the stylesheet. The consumer should only attach it.
Today the workarounds all leak:
- Per-rule ignore comments (
emotion-disable-server-rendering-unsafe-selector-warning-please-do-not-use-this-the-warning-exists-for-a-reasonfrom #1209) do not work on a full Global stylesheet. The alarm walks root children from the end, so later comments and line breaks stop the match. cache.compat = truemutes the whole cache and changes SSR insertion.- A dedicated cache for one
<Global>creates another style-tag group, which is the same class of ordering / SSR problem the warning exists to describe. - A hand-rolled
<style>skips Emotion's container, nonce, insertion point, re-render deduping, and unmount cleanup.
Related: #1105, #1178, #1209.
Proposed solution
Let the caller declare a CSS string as external stylesheet text. After that declaration, Emotion must not transform, prefix, nest-compile, or lint the CSS content. The bytes that go in are the bytes that end up in the style sheet text.
Emotion should still own the runtime concerns that a consumer cannot do well alone:
- insert at the cache's DOM location (container, nonce, insertion point)
- insert once across re-renders
- remove the insertion when the owner unmounts
- preserve relative order against other styles from the same cache
The exact API is secondary to that contract. A flag on <Global>, a dedicated component, or a cache helper such as insertExternalStyles would all work. What matters is an opt-in path that skips Stylis and the unsafe-selector development plugin, while still using the sheet.
Drawbacks. External CSS would not receive nesting or auto-prefixing. That is intended. @import can still fail under CSSOM insertRule for the same reason it fails in other CSS-in-JS libraries. Callers that need @import would keep using a link or a document-level style tag.
Alternative solutions
Keep telling people to change :first-child to :first-of-type, or to paste ignore comments next to every unsafe selector. That asks consumers to edit CSS they do not own, and the comment matcher is unreliable on a whole file.
Keep pointing at stylisPlugins: []. That only drops the prefixer. The unsafe-selector plugin is omnipresent, and the CSS still goes through Stylis.
Keep pointing at cache.compat. That is a cache-wide SSR mode, not a per-stylesheet "do not touch this text" switch.
Leave people to mount a raw <style> themselves. They then lose nonce, insertion order, deduping, and cleanup, which is why they used Emotion in the first place.
Fela's renderStatic already treats a CSS string as third-party / legacy input and documents that plugins do not run. Emotion has no equivalent.
Additional context
The use case is embedding another package's published CSS inside an Emotion app (React, nonce, custom insertion point) without forking that CSS. Constructable stylesheets and a plain <style> element already take source text as sheet contents. A CSS-in-JS runtime that also manages style tags should offer the same contract when the caller says the string is external.
Source: emotion-js/emotion