How to: share content across surfaces
You want to render content somewhere other than its own JSX/template position — a toast, an overlay — and need to know which mechanism applies.
Same surface: createPortal (React) / Teleport (Vue) / *portal (Angular)
Section titled “Same surface: createPortal (React) / Teleport (Vue) / *portal (Angular)”Both move content to an already-mounted node within the same mounted surface as the call site:
// Reactconst [overlay, setOverlay] = useState<IHostInstance | null>(null);// ...<View ref={setOverlay} />{overlay ? createPortal(<Text>ported in</Text>, overlay) : null}<!-- Vue --><Teleport v-if="toastVisible && overlayHost" :to="overlayHost"> <Text>Ported via Teleport</Text></Teleport><View ref="overlayHost" />Angular has no runtime component synthesis (no JIT under Metro/AOT), so createPortal
isn’t a factory here — *portal is a structural directive instead, the same idiom as
*ngIf, paired with a portalOutlet marking the destination:
<!-- Angular --><View portalOutlet #overlayHost="portalOutlet"></View>@if (toastVisible) { <View *portal="overlayHost"><Text>Ported via *portal</Text></View>}Svelte has no same-surface portal primitive at all — not even a partial one.
createPortal is react-reconciler’s own Fiber-level HostPortal, and Vue’s Teleport is a
separate Vue-runtime relocation feature; neither has anything for a framework with no
reconciler to hook into, and there’s no *portal-style directive to reach for either. Skip
straight to createTunnel below — it’s the only cross-content primitive this adapter has,
and it happens to work same-surface too.
Use a state/ref callback (useState, not useRef, on React) so the target
resolves once it actually commits — a plain ref is null on the entire first
render.
Cross surface: createTunnel
Section titled “Cross surface: createTunnel”For content that must reach a genuinely different, separately-mounted
surface, use createTunnel() instead — a shared store, not a node reference:
// React — module-level singleton, importable from any surfaceexport const overlayTunnel = createTunnel();
// wherever it should paint:<overlayTunnel.Out />// wherever the content originates, any surface:<overlayTunnel.In><ToastCard /></overlayTunnel.In><!-- Vue --><tunnel.Out /><tunnel.In><Text>Ported via createTunnel</Text></tunnel.In>Same no-JIT constraint as *portal above — TunnelInDirective/TunnelOut are one static,
pre-authored pair parameterized by the store createTunnel() returns, not a per-call
factory output:
// Angular — module-level singleton, importable from any componentexport const overlayTunnel = createTunnel();@if (toastVisible) { <View *tunnelIn="overlayTunnel"><Text>Ported via createTunnel</Text></View>}<tunnel-out [tunnel]="overlayTunnel" /><!-- Svelte — module-level singleton, importable from any surface --><script lang="ts" module> import { createTunnel } from '@symbiote-native/svelte';
export const overlayTunnel = createTunnel();</script><!-- wherever it should paint: --><TunnelOut tunnel={overlayTunnel} />
<!-- wherever the content originates, any surface: -->{#if toastVisible} <TunnelIn tunnel={overlayTunnel}> {#snippet children()}<ToastCard />{/snippet} </TunnelIn>{/if}TunnelIn/TunnelOut take the tunnel as an explicit tunnel prop instead of
tunnel.In/tunnel.Out — a .svelte file compiles to one fixed, top-level component, so
there’s no per-call factory createTunnel() could bake a registry into via closure the way
Vue’s defineComponent can.
In/Out are components, not hooks/composables — an earlier hook-based
React version caused a genuine infinite render loop (the shared store’s
notify() re-rendering the same component that also called the write side).
As separate components, Out’s forced re-render never bounces back into
In, even when they’re siblings.
Picking one
Section titled “Picking one”- Overlay host lives in the same tree you’re already rendering →
createPortal/Teleport. - Content needs to reach a different
mount()root entirely (split-screen, an always-on-top system surface) →createTunnel.
Import both from your adapter’s package (@symbiote-native/react, @symbiote-native/vue,
@symbiote-native/angular, @symbiote-native/svelte). Angular exposes the same two mechanisms as
directives instead of components/hooks: *portal/*tunnelIn and
<tunnel-out>. Svelte exposes only the second mechanism — createTunnel, as TunnelIn/
TunnelOut components taking an explicit tunnel prop — it has no same-surface portal
primitive at all.