JORVEL

CSS isolation

Remotes ship CSS that can leak into the host — a remote that resets * { margin: 0 }can hose the host's typography. JORVEL offers two runtime isolation strategies depending on how strict you need to be, plus a couple of bundle-level conventions that head off most problems before they reach production.

Strategy comparison

StrategyStrengthCost
Shadow DOM (ShadowRemote)Full isolation — styles cannot cross the boundary in either directionBreaks document.querySelector across the boundary; some CSS-in-JS libraries need adapter config
Scoped selectors (scopeCss)One-way — remote styles namespaced under an attribute; host CSS still leaks inCheap; works with any CSS pipeline
CSS Modules / Tailwind @layerSoft — discipline + tooling, not a runtime guaranteeFree at runtime; relies on remote teams following conventions

Shadow DOM mount

ShadowRemote attaches a shadow root, mounts a React subtree inside, and injects stylesheets. Styles cannot cross the shadow boundary. This is the only option that protects against a remote with a destructive reset rule.

tsx
import { ShadowRemote } from '@jorvel/runtime';

<ShadowRemote
  css={remoteCss}
  stylesheets={['https://cdn.mycorp.com/mfe/dashboard/styles.css']}
>
  <RemoteDashboard />
</ShadowRemote>

ShadowRemote props

PropTypePurpose
cssstringInline CSS injected into the shadow root.
stylesheetsstring[]URLs fetched and inlined as <link rel="stylesheet"> inside the shadow.
mode'open' | 'closed'Default 'open'. Closed roots block external script access and form autofill.
delegateFocusbooleanForward focus to the first focusable child. Pair with autofocus on inputs.

Scoped selectors

Lighter weight — the remote's CSS is rewritten so every selector is prefixed with an attribute. Doesn't protect against the host's rules leaking in (they still hit the remote), but it does keep the remote's rules from escaping.

tsx
import { scopeCss } from '@jorvel/runtime';

const scoped = scopeCss(rawCss, '[data-remote="dashboard"]');
injectStyle(scoped);

<div data-remote="dashboard">
  <RemoteDashboard />
</div>

CSS Modules

Bundler-level isolation. Every class name is hashed (.btn .btn__7sJ2k) so accidental collisions are impossible. Rspack supports CSS Modules out of the box; the generator wires them in any app that opts in via --css-modules.

tsx
import styles from './Button.module.css';

export function Button(props: ButtonProps) {
  return <button className={styles.btn} {...props} />;
}

Tailwind @layer

When both host and remote ship Tailwind, the second-loaded preflight reset can clobber the first. Force every team to wrap their custom rules in @layer components / @layer utilities — the layer order is then predictable and the reset only applies once.

css
/* remote.css */
@layer components {
  .product-card {
    /* … */
  }
}
@layer utilities {
  .text-shadow {
    /* … */
  }
}

Caveats

  • Shadow DOM breaks global document.querySelector — isolate by design.
  • CSS-in-JS libraries may need the shadow root as style target — check their SSR adapter.
  • Design tokens still propagate via CSS custom properties (they inherit).
  • Focus traps and portals need to mount into the shadow root, not document.body.
  • Forms inside a closed shadow root cannot be discovered by browser autofill — use mode="open" when forms matter.
  • Print stylesheets do not inherit into shadow roots — duplicate them per shadow.
  • DevTools shows shadow roots collapsed by default — toggle "Show user agent shadow DOM" to inspect them.

Sharing design tokens across the boundary

CSS custom properties inherit through shadow roots. Define your tokens on :rootin the host and the remote inherits them automatically. This is the canonical way to keep the same brand colors without the remote depending on the host's build pipeline.

css
/* host global.css */
:root {
  --brand-primary: #4f46e5;
  --brand-radius: 8px;
}

/* remote.css (inside shadow root) */
.button {
  background: var(--brand-primary);   /* resolves from the host */
  border-radius: var(--brand-radius);
}

Tailwind users

Tailwind's preflight resets target the global document. Inside a ShadowRemote you'll need to ship the compiled Tailwind CSS via the css prop. The runtime injects it into the shadow root so utilities still resolve.

Picking a strategy

SituationUse
Trusted internal remotes that follow team conventionsCSS Modules + Tailwind @layer
Vendor-supplied or 3rd-party widgetsShadow DOM — protects against destructive resets
One remote that needs to participate in document-level focus / portalsScoped selectors
Multiple visual brands on the same pageShadow DOM per brand + shared tokens