Skip to content

Stencil FOUC styles should use @layer to avoid cascade conflicts with React 19 #6649

@estevaolucas

Description

@estevaolucas

Summary

Stencil's FOUC prevention CSS (<style data-styles>) can override consumer CSS when used with React 19, because React 19 reorders <head> elements and changes the cascade order. Wrapping the FOUC styles in @layer would make them lowest-priority by default, fixing the conflict without requiring consumers to patch Stencil.

Current behavior

In bootstrap-lazy.ts, Stencil injects a <style data-styles> tag into <head> with:

market-component-a, market-component-b, ... { visibility: hidden }
[hydrated] { visibility: inherit }

It's inserted via:

head.insertBefore(dataStyles, metaCharset ? metaCharset.nextSibling : head.firstChild);

This relies on source order in <head> — the FOUC styles are placed early so that component-specific and consumer CSS loaded later will override them.

Problem with React 19

React 19 introduced "hoistable" style management — it moves <link rel="stylesheet"> tags to head.firstChild. This pushes <meta charset> down in the head, so when Stencil inserts after metaCharset.nextSibling, the FOUC styles end up after component CSS <link> tags instead of before them.

The result: [hydrated] { visibility: inherit } and consumer class selectors (e.g. .popover { visibility: hidden }) have equal specificity (0,1,0). With the FOUC style now later in source order, it wins — causing components to inherit visibility: hidden from ancestors even after hydration.

This caused a production incident at Square where Market web component popovers became invisible but still captured pointer events, blocking user interaction across the entire dashboard.

Proposed fix

Wrap the FOUC CSS in @layer:

// src/runtime/bootstrap-lazy.ts
- dataStyles.textContent += cmpTags + HYDRATED_CSS;
+ dataStyles.textContent += `@layer stencil-hydration{${cmpTags}${HYDRATED_CSS}}`;

Per the CSS cascade spec, layered styles always lose to unlayered styles, regardless of source order or specificity. This is semantically correct — Stencil's FOUC styles are intentionally low-priority defaults that should yield to any consumer CSS.

Why @layer is the right solution

  • Order-independent: Works regardless of where the <style> tag lands in <head>
  • Semantically correct: FOUC styles are defaults, not overrides
  • No consumer changes needed: Consumers don't need to patch Stencil
  • Framework-agnostic: Fixes the issue for React 19 and any future framework that reorders <head>
  • Browser support: @layer is supported in all modern browsers (Chrome 99+, Firefox 97+, Safari 15.4+). Older browsers silently discard @layer blocks — but the FOUC bug only manifests with React 19 which itself requires modern browsers

Workaround

We're currently working around this by patching Stencil's output at runtime with a MutationObserver that wraps <style data-styles> content in @layer stencil-hydration { ... } before MFEs load.

Environment

  • Stencil: 4.18.0
  • React: 19.x
  • Output target: dist-custom-elements (via @stencil/react-output-target)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions