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) / Portal (Svelte, Solid)
Section titled “Same surface: createPortal (React) / Teleport (Vue) / *portal (Angular) / Portal (Svelte, Solid)”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. The primitive tags need
imports: [SYMBIOTE_ELEMENTS] on the component, same as any other Angular template:
<!-- Angular --><view portalOutlet #overlayHost="portalOutlet"></view>@if (toastVisible) { <view *portal="overlayHost"><text>Ported via *portal</text></view>}Solid spells it <Portal mount={...}>, not a createPortal() call, because JSX in Solid
evaluates eagerly at the position it’s written, so a createPortal(children, target) call
would build the toast’s nodes before anything decided whether to portal them. children is
the only lazily-evaluated shape Solid has, which is what a component prop gives you:
// Solidconst [overlay, setOverlay] = createSignal<ISymbioteNode | null>(null);// ...<view ref={setOverlay} />{overlay() ? <Portal mount={overlay()}><text>ported in</text></Portal> : null}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" />// Solid - 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>Same shape as React and Vue: Solid components are ordinary functions, so createTunnel() can
mint a fresh In/Out pair per call, closed over that tunnel’s own registry.
<!-- 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, @symbiote-native/solid). Angular exposes the
same two mechanisms as directives instead of components/hooks: *portal/*tunnelIn and
<tunnel-out>. Solid has both mechanisms too, spelled <Portal mount={...}> and createTunnel()’s
In/Out; same-surface portal now ships on every adapter except Svelte, which exposes only the
second mechanism, createTunnel, as TunnelIn/TunnelOut components taking an explicit tunnel
prop.