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
| Strategy | Strength | Cost |
|---|---|---|
Shadow DOM (ShadowRemote) | Full isolation — styles cannot cross the boundary in either direction | Breaks 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 in | Cheap; works with any CSS pipeline |
CSS Modules / Tailwind @layer | Soft — discipline + tooling, not a runtime guarantee | Free 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.
import { ShadowRemote } from '@jorvel/runtime';
<ShadowRemote
css={remoteCss}
stylesheets={['https://cdn.mycorp.com/mfe/dashboard/styles.css']}
>
<RemoteDashboard />
</ShadowRemote>ShadowRemote props
| Prop | Type | Purpose |
|---|---|---|
css | string | Inline CSS injected into the shadow root. |
stylesheets | string[] | 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. |
delegateFocus | boolean | Forward 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.
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.
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.
/* 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.
/* 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
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
| Situation | Use |
|---|---|
| Trusted internal remotes that follow team conventions | CSS Modules + Tailwind @layer |
| Vendor-supplied or 3rd-party widgets | Shadow DOM — protects against destructive resets |
| One remote that needs to participate in document-level focus / portals | Scoped selectors |
| Multiple visual brands on the same page | Shadow DOM per brand + shared tokens |
