How to: refs and attachments in Svelte
Svelte has no React-style ref prop and no Vue template ref. Symbiote’s
primitives (view, text, pressable, …) are ordinary lowercase host tags, so
reaching the real native node from app code takes one of two mechanisms depending
on what you’re touching.
bind:this — direct, on any primitive tag
Section titled “bind:this — direct, on any primitive tag”bind:this on a primitive tag gives you back a real ShimElement directly — no
wrapper in the way. hostInstance() unwraps that into the imperative
host-instance API (measure, measureInWindow, setNativeProps, focus,
blur), and findNodeHandle() reads the committed native tag off it — the
tag only exists after the first commit, so read it from an $effect:
<script lang="ts"> import { findNodeHandle, hostInstance, type ShimElement, } from '@symbiote-native/svelte';
let box = $state.raw<ShimElement | null>(null); let tag = $state<number | null>(null);
$effect(() => { if (box === null) return; tag = findNodeHandle(box); });
function onMeasure(): void { const instance = hostInstance(box); if (instance === undefined) return; instance.measure((x, y, width, height, pageX, pageY) => { // real on-screen frame }); }</script>
<view testID="ref-box" bind:this={box}> ... </view>Source: examples/svelte/components/RefApiDemo.svelte — the port of React’s
RefApiDemo.tsx, backing measure/setNativeProps/findNodeHandle.
Some components expose their own imperative surface without any of this:
ScrollView reads its own bind:this internally and exports plain functions
— scrollTo/scrollToEnd/flashScrollIndicators/getScrollNode — the
Svelte-5 twin of React’s useImperativeHandle/Vue’s expose(). Check the
Components API for a component’s own exported
surface before reaching for the raw host tag.
{@attach} — the route for a component, and it works on a tag too
Section titled “{@attach} — the route for a component, and it works on a tag too”On a primitive tag, {@attach} is nothing but Svelte’s own compiler feature working
directly against the element — no adapter code involved, same as bind:this:
<script> const logLifecycle = node => { console.log('attached', node); return () => console.log('detached', node); };</script>
<view testID="target" {@attach logLifecycle} />Where it earns its keep is the handful of primitives that stay real components
(Modal, KeyboardAvoidingView, the list family) — the Svelte compiler rejects
use:/transition:/class:/style: on a component (“This type of directive is
not valid on components”), so {@attach fn} is the one directive-shaped construct
that still compiles there: it lands as a prop keyed by a real JS Symbol
(createAttachmentKey()), which rides through $props()/...rest untouched —
routeProp only ever walks string keys — and each of those components spreads
...rest onto its own host tag, forwarding it there for free. From there, the
adapter’s own createAttachmentsSync() wires it to the real committed node
(adapters/svelte/src/runes/attachments.ts).
Wrapping a third-party Svelte action
Section titled “Wrapping a third-party Svelte action”fromAction from svelte/attachments converts an ordinary Svelte action
(init/update/destroy) into an attachment, so a library’s action-based API
still works against a Symbiote primitive:
<script> import { fromAction } from 'svelte/attachments';
const action = (node, value) => ({ update: next => { /* react to a new value */ }, destroy: () => { /* teardown */ }, });</script>
<view testID="action-target" {@attach fromAction(action, () => someValue)} />Source: adapters/svelte/src/runes/attachments.smoke.test.ts, which
round-trips this exact shape end to end against a real compiled component.
Setting props on a <svelte:element> native leaf
Section titled “Setting props on a <svelte:element> native leaf”@symbiote-native/navigation‘s stack renders react-native-screens’ native
views (RNSScreen, RNSScreenStackHeaderConfig, RNSSearchBar, …) through
<svelte:element this={'RNSScreen'}>, because a literal <RNSScreen> in a
template would parse as a component reference, not an element. A dynamic
tag compiles through Svelte’s generic setAttribute codegen instead of the
custom-element p= property-set path, so p={bag} as a plain attribute
silently does nothing there. An attachment sidesteps that path entirely by
assigning the property from plain JS:
export function hostProps( props: Record<string, unknown>,): (node: unknown) => void { return node => { if (!isShimElement(node)) return; node.p = props; };}<svelte:element this={"RNSScreen"} {@attach hostProps(plan.screenProps)}> ...</svelte:element>Which one?
Section titled “Which one?”| You have… | Use |
|---|---|
A primitive tag (view, pressable, …) and you just need the node |
bind:this (or {@attach} — both are direct) |
A component with its own exported imperative functions (ScrollView.scrollTo, …) |
that component’s own bind:this |
A primitive that still stays a component (Modal, KeyboardAvoidingView, the list family) |
{@attach} |
| A third-party Svelte action | {@attach fromAction(action, () => arg)} |
A <svelte:element> native leaf (a capitalized, un-hyphenated native view name) |
{@attach} setting node.p |