Skip to content

Components API

SymbioteNative’s component goal is structural parity: reusable state, render, and native plumbing live in shared layers; adapters provide lifecycle and framework syntax.

Terminal window
pnpm add @symbiote-native/components

Nearly every consumer gets this transitively: @symbiote-native/react, @symbiote-native/vue, @symbiote-native/angular, @symbiote-native/svelte, and @symbiote-native/solid each depend on it and re-export the parts an app touches. Install it directly only when building a new adapter or a native-view wrapper. @symbiote-native/engine and react-native (>=0.86) are peer dependencies and stay top-level dependencies of the app.

view, text, pressable, image, image-background, scroll-view, text-input, switch, activity-indicator, safe-area-view, modal, refresh-control, input-accessory-view, button, touchable-opacity, touchable-native-feedback, touchable-without-feedback, touchable-highlight, and sticky-header are lowercase tags an app writes directly — React, Vue, Svelte, and Solid resolve them with no import; Angular resolves them once its imports array carries SYMBIOTE_ELEMENTS. There is no per-adapter wrapper component left for these, so the framework differences that used to live in each adapter’s own render bridge (descriptorToReact, descriptorToVue, a hand-authored Svelte host tag, Solid’s descriptorToSolid) mostly don’t apply to them anymore — a pressable on React and a pressable on Solid commit the identical Fabric tree, built by the same shared host behavior in @symbiote-native/components/@symbiote-native/engine.

What still differs by framework:

  • Event syntax — onPress (React/Svelte/Solid), @press (Vue), (press) (Angular). See Event naming examples below.
  • pressable’s pressed state no longer arrives as a render-prop child, a Vue scoped slot, or a Svelte snippet — those relied on a wrapper component body to call a child function, and a plain tag has no such body. Wire onPressIn/onPressOut into local state instead (each framework’s own API page shows the idiom); Angular already used real (pressIn)/(pressOut) outputs for this and is unaffected.
  • Lists (FlatList, SectionList, VirtualizedList, VirtualizedSectionList) and KeyboardAvoidingView are the one family still a genuine per-framework component: renderItem/the cell content returns a framework element (ReactNode, a Vue slot, a Svelte snippet, a Solid render function), which is exactly the shape a tag cannot express — see each framework’s own API page for the render-item signature.
  • Animated.* wrapping stays per-adapter — see the Animations guide for how each framework hands an animated value into a tag’s style prop and what createAnimatedComponent still does where a genuine component (the lists) is being animated.
  • PanResponder — an engine-level gesture-recognition module, not a component; shared across every adapter unchanged.

Shared when the prop is plain native data:

  • booleans, strings, numbers;
  • native style objects;
  • accessibility and ARIA aliases;
  • platform constants and native payload shapes.

Framework-specific when the prop contains framework values:

  • children or slots;
  • render callbacks returning framework elements;
  • refs and imperative handles;
  • framework-level event conventions.
Tag React Vue Angular Svelte Solid Payload
pressable onPress @press (press) onPress onPress ISymbioteEvent
pressable onLongPress @long-press (longPress) onLongPress onLongPress ISymbioteEvent
switch onValueChange @value-change
or v-model
(valueChange) onValueChange onValueChange boolean
text-input onValueChange @value-change
or v-model
(valueChange) onValueChange onValueChange string
view onLayout @layout (layout) onLayout onLayout ISymbioteEvent

text-input’s React/Vue callback fires as (text, event) — one value merged from what used to be two separate callbacks. Angular can’t do that in one EventEmitter, so it keeps a second, separate (change) output for the raw event alongside text-only (valueChange). Svelte’s onValueChange fires the identical (text, event) shape React’s does — the adapter is a flat object-bag adapter (every prop, handlers included, rides one object handed to the shim element’s p setter), so there is no per-event translation layer to begin with: the callback prop name is RN’s own name, unchanged. Solid’s onValueChange fires the same (text, event) shape too, as an ordinary callback prop: Solid JSX props reach the component as plain getters, with no compiler-level event translation.

Angular’s names ((press), (longPress), (valueChange), (change), (layout)) are all real @Output() EventEmitters, bound like (press)="handler($event)". The one permanent exception is the scroll-family events (onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd) on scroll-view and the list components, which stay plain callbacks bound as an @Input() — [onScroll]="handler" — because they can carry an Animated.event(...) marker for native-driven scroll, and @Output() only binds a template listener expression, never an arbitrary value. Svelte has no @Output() equivalent at all — every event on every component, scroll-family included, is a plain callback prop, so this Angular-only exception has no Svelte counterpart to carve out. Solid has no @Output() equivalent either, for the same reason.

Svelte’s own two-way-binding primitive, the nearest equivalent to Vue’s v-model, is $bindable() (let { value = $bindable() } = $props(), wired with bind:value={x} at the call site). text-input/switch (and Slider, from the separate @symbiote-native/slider package) declare value this way, so both styles work: <text-input bind:value={x} /> and the explicit <text-input value={x} onValueChange={setX} /> that Svelte shares with React. Supplying onValueChange on the same instance as bind:value takes over the echo — the component can’t detect binding from the inside, so pass one or the other, not both, on one control. See How to: two-way bind a value for the full picture. Solid has no bind:value/v-model sugar of its own on these components: value and onValueChange are always explicit, the same pair React uses without the sugar.

// React — callback props, no import needed for the tags
<pressable onPress={event => console.log(event)} />
<switch value={enabled} onValueChange={setEnabled} />

App code reaches these through an adapter. The exports below are the seam an adapter (or a native-view wrapper package) drives directly.

A render function returns a Descriptor tree — a tiny framework-agnostic node description. For a primitive tag (view, switch, modal, …), the engine’s own IHostBehavior.buildStructure consumes this directly at attach time — no adapter is involved. The descriptorToReact/descriptorToVue/descriptorToSolid bridges (and Svelte’s equivalent host-tag wiring) still exist for the one family that remains a genuine per-framework component — the lists (FlatList, SectionList, VirtualizedList, VirtualizedSectionList) and KeyboardAvoidingView — since a list’s cell content is a real framework element, not a Descriptor.

Export Signature Description
el el(type, props?, children?, key?): IDescriptor Build a host-element descriptor of any type; type is an open string, so a component can paint a raw Fabric view name as well as a primitive tag name
txt txt(props?, children?): IDescriptor Shorthand for a text descriptor
IDescriptor { type, props, children, key? } The node every render function returns
IDescriptorType string The host component to paint — kept open, since components register their own host element names with the engine
IDescriptorProps Record<string, unknown> Open prop bag (style, events, accessibility, native props), forwarded onto the framework element verbatim
IDescriptorChild IDescriptor | string A nested descriptor or a raw text child
Export Signature Description
descriptorFor descriptorFor(type: string): IComponentDescriptor Resolve an intrinsic to its Fabric component name for the current platform. An unknown intrinsic throws (a typo in our own code); any other string passes through as a raw Fabric view name from a library’s codegen component
COMPONENT_DESCRIPTORS Readonly<Record<string, IComponentDescriptor>> The platform-selected intrinsic → Fabric name map. The tables are Metro-split (.ios/.android filename selects, no Platform.OS read)
buildDescriptors buildDescriptors(names): Readonly<Record<string, IComponentDescriptor>> Assemble the descriptor map a platform name table exports, pairing each name with its platform-invariant isText flag
makeDescriptorFor makeDescriptorFor(descriptors): (type: string) => IComponentDescriptor Bind the resolver above to one descriptor map; each platform file uses it to produce its own descriptorFor
ISymbioteIntrinsic union of 'view', 'text', 'image', … Every intrinsic tag an app may write. A name table must cover exactly these keys, so a missing primitive is a compile error rather than a runtime gap
IComponentDescriptor { component: string; isText: boolean } The resolved Fabric name plus whether it lays text (drives the RCTText/RCTVirtualText nesting choice)

Pure viewProps → Descriptor. Visual state enters only through arguments; no framework, no lifecycle, no events.

Function Signature Description
renderSwitch (view: ISwitchViewProps, platform: ISwitchPlatform) => IDescriptor Paint the switch, mapping the track/thumb color props onto the platform’s native names
renderImage (view: IImageViewProps) => IDescriptor Resolve the source, fold the width/height aliases into style, paint image
renderInputAccessoryView (view: IInputAccessoryViewViewProps) => IDescriptor Assemble the nativeID/backgroundColor host element
renderModal (view: IModalViewProps) => IDescriptor Paint the modal host, applying the transparent/backdrop overrides on top of the generic style prop
renderTextInput (view: ITextInputViewProps) => IDescriptor Pick the single-line or multiline intrinsic and map the resolved native props

These are called by the primitive’s own IHostBehavior.buildStructure in the engine now, not by an adapter — that’s what lets a bare <modal>/<switch>/… tag commit the same tree a wrapper used to. ActivityIndicator’s and ImageBackground’s painting logic moved into their behaviors directly (behaviors/activity-indicator/, behaviors/image-background.ts) with no separate render* function left. The list family still builds its cell content from the framework’s own elements, so the shared half there is the state and math below, not a descriptor.

Pure reducers and folds. The adapter supplies the lifecycle cell (React useReducer, Vue ref/watch, Angular signals, Svelte $state/$effect runes, Solid createSignal/createEffect) and executes the effects.

Export Signature Description
createInitialSwitchState () => ISwitchState Initial Switch state — no native report seen yet
switchReducer (state: ISwitchState, action: ISwitchAction) => ISwitchState Fold a native change report into the last-reported value; always returns a fresh object so the snap-back effect re-fires on every report
shouldSnapBack (state: ISwitchState, fabricValue: boolean) => boolean Whether native reported a value the JS-held value rejects, so the switch must be commanded back
valueFromChange (event: ISymbioteEvent) => boolean | undefined Read the boolean out of a native Switch change payload
createInitialModalState (isVisible: boolean) => IModalState Initial Modal state, seeded from the first visible prop
modalReducer (state: IModalState, action: IModalAction) => IModalState Gate the iOS keep-alive frame across show/hide
shouldRenderModal (isVisible: boolean, state: IModalState) => boolean Whether the modal host should be in the tree this render
createPressRuntime () => IPressRuntime Mutable per-instance press bookkeeping (timers, origin, suppression flags) the handlers write through
createPressHandlers (config: IPressMachineConfig, runtime: IPressRuntime, host: IPressHost) => IPressHandlers The whole press lifecycle — delay, long-press timer, drift test against the responder region, termination
buildPressableListeners (handlers: IPressHandlers, options: { disabled?, cancelable? }) => Record<string, unknown> Turn those handlers into the responder listener bag a host element takes; returns {} when disabled
createTouchableFeedbackRuntime () => ITouchableFeedbackRuntime Per-instance timing state for the Touchable* feedback animation
createTouchableFeedbackHandlers (config, runtime, callbacks) => ITouchableFeedbackHandlers Press handlers for the Touchable* family; the adapter supplies the Animated animation through callbacks
computePressOutWait (heldFor: number, minPressDuration: number, delayPressOut: number) => number The deactivation floor — how long to keep the pressed visual after release
resolveTextInputProps (input: ITextInputFoldInput) => IFoldedTextInputProps Fold inputMode/autoComplete/submitBehavior and friends into the native prop set
foldText (value?: string, defaultValue?: string) => string | undefined Resolve the controlled value against defaultValue
textFromChange (event: ISymbioteEvent) => string | undefined Read the text out of a native TextInput change payload
eventCountFromChange (event: ISymbioteEvent) => number | undefined Read the native event count, the ordering token behind the controlled-write handshake
shouldCommandText (lastNativeText: string | undefined, value: string | undefined) => value is string Whether JS must command text back onto the native input because the two have diverged
createInitialListState <ItemT>() => IListState<ItemT> Initial VirtualizedList state — no offsets measured, empty committed window
reduceList <ItemT>(state, action: IListAction<ItemT>, inputs: IListReducerInputs<ItemT>) => IListReduceResult<ItemT> The list orchestration reducer: window recompute → edge reached → viewability → initial scroll → maintainVisibleContentPosition, returned as state plus an effect list for the adapter to run
listEffectSignature <ItemT>(state: IListState<ItemT>) => string Stable key over the state fields an effect depends on, so an adapter can skip re-running unchanged effects
createInitialStickyState () => IStickyHeaderState Initial sticky-header state — unmeasured, untranslated
reduceSticky (state, action: IStickyAction, inputs: IStickyReducerInputs) => IStickyReduceResult The per-header sticky machine: the zero-swallow gate, the debounce-delay pick, and the rebuild-on-input-change decision
stickyEffectSignature (state: IStickyHeaderState) => string The same skip-unchanged key for the sticky interpolation effect

Platform-invariant math and plumbing with no state machine of their own.

Export Signature Description
resolveDecelerationRate (rate: 'normal' | 'fast' | number) => number Map RN’s named deceleration rates to the numeric native value
selectScrollIntrinsics (isHorizontal: boolean, contentContainerStyle) => IScrollIntrinsics Pick the scroll host/content intrinsics — horizontal scroll is a separate native ViewManager on Android
resolveScrollForwarding (inputs: IScrollForwardingInputs) => IScrollForwarding The scroll-forwarding decisions: which onScroll path to build (IScrollForwardMode — 'plain', 'sticky-native', 'sticky-js'), the resolved scrollEventThrottle, whether onLayout must capture the viewport height, and whether content cells stay un-flattened for maintainVisibleContentPosition/snap on Android. It returns decisions, not built handlers — those must stay framework-owned for identity reasons
buildScrollViewHandle (getNode: () => ISymbioteNode | null) => IScrollViewHandle The imperative handle (scrollTo, scrollToEnd, flashScrollIndicators, …) over a lazily-read host node
splitLayoutProps (style) => { outer, inner } RN’s key partition: layout keys (margin*, flex, …) go to the outer box, visual keys (background*, padding*, border*, …) stay on the inner view, for cases like the Android RefreshControl wrap
forwardScrollEvent (handler, args: readonly unknown[]) => void Forward a native scroll event to a user handler, dropping non-event arguments
isSymbioteEvent (value: unknown) => boolean Runtime guard for a normalized engine event
attachStickyScroll (node: ISymbioteNode, value: AnimatedValue) => () => void Attach a native-driven onScroll → Animated.Value binding; returns the detach function
computeStickyInterpolation (params: IStickyInterpolationParams) => { inputRange, outputRange } The sticky header’s interpolation ranges, including the inverted-list case
nextStickyHeaderY (stickyHeaderIndices, indexOfIndex, headerLayoutYs) => number | undefined The following sticky header’s Y, which bounds how far the current one travels
buildOffsets (count, measured, fixedLayout, averageLength) => … Cumulative cell offsets from measured cells, a fixed-layout function, or the running average
computeWindow (count, offsets, lengths, scrollOffset, …) => … The render window for the current scroll position
buildListPlan (params: IListPlanParams) => IListPlan The full cell plan for a pass — which cells render, plus the leading/trailing spacer extents
computeViewableSet <ItemT>(params: IViewableSetParams<ItemT>) => { tokens, map } The currently viewable items, as tokens plus a key-indexed map
diffViewable <ItemT>(previous, current, currentTokens) => { changed, hasChanged } Diff two viewable sets into the payload onViewableItemsChanged expects
resolveItemKey <ItemT>(item, index, keyExtractor?) => string The cell key, falling back to the index when no keyExtractor is given
chunkIntoRows <ItemT>(data: readonly ItemT[], columns: number) => IRow<ItemT>[] Group flat data into numColumns rows for FlatList
rowKeyExtractor <ItemT>(row: IRow<ItemT>) => string Stable key for such a row
flattenSections <ItemT>(sections, withSeparators) => { entries, headerIndices } Flatten SectionList sections into one entry list plus the sticky header indices
unwrapEntryItem <ItemT>(entry?: ISectionEntry<ItemT>) => ItemT | undefined Read the item out of a flattened entry, or undefined for a header/separator entry
sectionEntryKey <ItemT>(entry, index, keyExtractor?) => string Key for a flattened section entry
scrollLocationToFlatIndex (headerIndices, sectionIndex, itemIndex) => number Translate a scrollToLocation section/item pair into the flat index the windowing math uses
resolveKeyboardAvoidingLayout (params: IResolveKeyboardAvoidingLayoutParams) => IKeyboardAvoidingLayout The resolved style/height for KeyboardAvoidingView’s current behavior
computeInset (frame, keyboard, verticalOffset) => number The overlap between the measured frame and the keyboard frame
readKeyboardFrame (payload: unknown) => IKeyboardFrame | undefined Guard-read a native keyboard event payload
readLayoutFrame (layout: unknown) => IMeasuredFrame | undefined Guard-read an onLayout payload
buttonTextStyle ITextStyle The base Button label style
resolveButtonTextStyle (color?: string, disabled?: boolean) => ITextStyle Fold color/disabled into that base style
BUTTON_ACCESSIBILITY_ROLE 'button' The role every adapter’s Button sets

Constants with the same shared-math role are exported alongside them — DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_PRESS_RECT_OFFSETS, DEFAULT_ACTIVE_OPACITY, DEFAULT_VERTICAL_OFFSET, DEFAULT_WINDOW_SIZE, DEFAULT_INITIAL_NUM_TO_RENDER, SCROLL_VIEW_BASE_VERTICAL, STICKY_HEADER_Z_INDEX, INITIAL_EVENT_COUNT, among others.

Export Signature Description
resolveAccessibilityProps <T extends IAccessibilityProps & IAriaProps>(props: T) => T Fold the web aliases (role, aria-*) into the canonical accessibility* props. Returns props untouched when no alias is present, so every adapter folds identically
IAccessibilityProps type The canonical accessibility* prop set
IAriaProps type The role/aria-* aliases that fold into it
IAccessibilityRole, IRole type The native role union and its web-alias twin
IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo type State, value, and custom-action payload shapes

IResponderProps, IActivityIndicatorProps, ISwitchProps, and IButtonProps are fully framework-agnostic at their core, so every adapter’s own prop type layers only its own class-styling field on top of the shared base rather than redeclaring the whole shape — React adds className?: string, Vue, Svelte and Solid all add class?: (Svelte’s is ISvelteClassValue, a wider type that additionally accepts a clsx-style object/array, resolved by the adapter itself before it reaches the engine; Solid’s is the plain IClassNameValue the engine’s style registry resolves, matching Vue’s). IImageProps, ITextInputProps, ITextInputHandle, IScrollViewHandle, IVirtualizedListHandle, and IVirtualizedSectionListHandle ship here too; a prop type carrying framework children, refs, or render callbacks stays per-adapter by design.

The *ViewProps types (IActivityIndicatorViewProps, ISwitchViewProps, IImageViewProps, IModalViewProps, ITextInputViewProps, IImageBackgroundViewProps, IInputAccessoryViewViewProps) are the inputs of the matching render* function — already resolved, adapter-facing, not app-facing.

imageStatics and setImageSourceResolver (plus IImageStatics, IImageSize, IImageCacheStatus) originate in @symbiote-native/engine — they touch the native bridge, so they stay out of the pure view layer — and are re-exported here so the public Image.* surface an adapter assembles is unbroken.