This is the abridged developer documentation for SymbioteNative # What is SymbioteNative? > A framework-agnostic renderer that lets non-React frameworks drive real React Native Fabric views. SymbioteNative is a framework-agnostic renderer for real native iOS and Android apps. It keeps React Native’s native stack — Fabric, JSI, Yoga, Hermes, and the host platforms — and replaces only the JavaScript renderer that talks to Fabric. React is not privileged inside React Native’s renderer. Fabric exposes a JSI-bound mutation surface through `global.nativeFabricUIManager`. React Native’s React renderer is one client of that surface. SymbioteNative lets other framework adapters become clients too. ## The shortest version [Section titled “The shortest version”](#the-shortest-version) ```txt React · Vue · Angular · Svelte · Solid ↓ thin framework reconciler ↓ @symbiote-native/engine ↓ nativeFabricUIManager ↓ stock React Native Fabric ``` The output is still native Fabric views. SymbioteNative is not a WebView, not a React Native fork, and not a compatibility shim that pretends React components are framework-agnostic. ## Why this exists [Section titled “Why this exists”](#why-this-exists) React Native has an excellent native runtime, but most of the ecosystem can only use it through React. If you write Vue, Svelte, Solid, or Angular, your choices usually collapse to a WebView, a rewrite, or a separate native abstraction. SymbioteNative opens the renderer seam that already exists in Fabric and gives each UI framework a thin adapter over one shared native engine. ## What works today [Section titled “What works today”](#what-works-today) * React is the reference adapter and canary, live on the framework switcher. * Vue 3 proves the same engine can be driven by a non-React framework, also live on the switcher. * Angular is a real, tested adapter too, also live on the switcher — see the [status page](/docs/project/status/) for what’s still catching up. * Svelte is a real, tested adapter too — a DOM shim over stock compiled Svelte output, verified on-device on iOS and Android, also live on the switcher. * Solid is a real, tested adapter too, a Solid custom renderer over `solid-js/universal`’s own `createRenderer` with full component parity, also live on the switcher, verified on device like the other four adapters (see the [status page](/docs/project/status/)). * The engine owns mutation-to-Fabric persistence, event normalization, and commit batching. * The native core remains stock React Native. Start with the [quick start](/docs/quick-start/) or read [how it works](/docs/how-it-works/). # Use with an AI agent > Feed SymbioteNative's docs to Claude Code, Cursor, Codex or Copilot — llms.txt corpora, per-page Markdown, and a paste-ready rules block. Every coding agent was trained before SymbioteNative existed. Asked for a native screen it writes React Native: a `View` from `react-native`, `StyleSheet.create`, a `react-native-*` component library. All three are wrong here, and nothing in its training says so. Give it a corpus, the Markdown twin of the page it needs, and a rules file. ## Hand it the docs [Section titled “Hand it the docs”](#hand-it-the-docs) | URL | What it is | Size | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | -------- | | [`/llms.txt`](https://docs.symbiote-native.dev/llms.txt) | The index — every set below, described so an agent can pick | \~4 KB | | [`/llms-full.txt`](https://docs.symbiote-native.dev/llms-full.txt) | The entire documentation as one Markdown file | \~900 KB | | [`/llms-small.txt`](https://docs.symbiote-native.dev/llms-small.txt) | The same, with non-essential content stripped | \~800 KB | | [`/_llms-txt/react.txt`](https://docs.symbiote-native.dev/_llms-txt/react.txt) | One framework only — swap `react` for `vue`, `angular`, `svelte`, `solid` | \~260 KB | Prefer the single-framework set. Five adapters’ worth of near-identical examples in one context window is how an agent writes Vue’s `@press` into a React file. ```sh # Claude Code, Codex, Gemini CLI — anything that can read a URL > Read https://docs.symbiote-native.dev/_llms-txt/vue.txt before you touch this project. ``` ## Any page as Markdown [Section titled “Any page as Markdown”](#any-page-as-markdown) Append `.md` to any docs URL for that page’s source, no HTML chrome: ```plaintext https://docs.symbiote-native.dev/docs/learn/vue/ -> .../docs/learn/vue.md ``` The same thing sits under every page title: a Copy for agent button, a Markdown link, and a one-click hand-off to ChatGPT, Claude, Cursor or Copilot. ## Pin the rules [Section titled “Pin the rules”](#pin-the-rules) A corpus is read once; a rules file stays in front of the agent. Paste this into `AGENTS.md`, `CLAUDE.md`, or `.cursor/rules/symbiote.md` at your project root: ```md # SymbioteNative This app renders real native iOS/Android views through SymbioteNative, not through React Native's own renderer and not through a WebView. Docs: https://docs.symbiote-native.dev/llms.txt - Import from `@symbiote-native/` (`react` | `vue` | `angular` | `svelte` | `solid`). NEVER import from `react-native` in app code. - `react-native` (>=0.86) and `react` still stay top-level dependencies of the app — they are the runtime singleton and the Metro version anchor. Do not remove them, do not import from them. - Primitives are lowercase intrinsic tags, not imported components: `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`, `sticky-header`. React/Vue/Svelte/Solid need no import; Angular needs `imports: [SYMBIOTE_ELEMENTS]` on the component. - Events follow the framework's own idiom: `onPress` (React, Svelte, Solid), `@press` (Vue), `(press)` (Angular). - `pressable` has no render-prop/slot child. Track pressed state yourself with `onPressIn`/`onPressOut` (Angular: `(pressIn)`/`(pressOut)`). - `FlatList`, `SectionList`, `VirtualizedList`, `VirtualizedSectionList` and `KeyboardAvoidingView` ARE imported from the adapter package — their render item returns a framework element, which a tag cannot express. - Style with a CSS class (`className` on React, `class`/`[class]` elsewhere, or an SFC ` ``` `:global(.name)` escapes scoping the same way it does in real Svelte. Svelte’s own scoping deliberately doesn’t reach into a child component’s own markup — scope a class where it’s declared, not where it’s used. `@media`/`@keyframes`/ pseudo-class selectors are dropped with a diagnostic; React Native has no such concept. See the [Styling guide](/docs/learn/styling/) for the full compiler pipeline shared by every adapter. ## Runtime modules [Section titled “Runtime modules”](#runtime-modules) `@symbiote-native/svelte` re-exports the same runtime utilities as the other adapters, so app code keeps one import root: `Platform`, `StyleSheet`, `PlatformColor`, `DynamicColorIOS`, `PixelRatio`, `Alert`, `Share`, `Linking`, `Keyboard`, `Vibration`, `ActionSheetIOS`, `BackHandler`, `ToastAndroid`, `PermissionsAndroid`, `AccessibilityInfo`, `I18nManager`, `Settings`, `LayoutAnimation`, `InteractionManager`, `StatusBar`, `AppState`, `findNodeHandle`, and `dlog`/`isDebug` for diagnostic logging. Two reactive reads are **runes** instead of hooks/composables — Svelte’s own term, and this adapter’s lifecycle-helper bucket is named `runes/` to match: ```svelte {dimensions.current.width}×{dimensions.current.height} · {scheme.current} ``` Each returns a **boxed getter** (`{ get current() { ... } }`), not a bare `$state` value — Svelte 5 reactivity is lexically scoped to the module that declares it, so a raw `$state` returned from a function loses its reactivity for the caller; read `.current` the same way you’d unwrap a Vue `Ref.value`. On Android, `Keyboard` and `Settings` do nothing until you also install [`@symbiote-native/android`](/docs/packages/android/): `keyboardDidShow`/`keyboardDidHide` never fire, because Android’s only stock keyboard-event source is `ReactRootView`’s layout listener and the bridgeless surface SymbioteNative mounts never triggers it, and `Settings` reads back `null`, because RN’s `Settings` has no stock Android implementation at all. ## Animations and gestures [Section titled “Animations and gestures”](#animations-and-gestures) An animated value dropped straight into a primitive’s prop resolves in the engine — `` just works, no wrapper component needed. `Animated` (both the JS and native driver) and `PanResponder` are re-exported from `@symbiote-native/svelte` — see the [Animations guide](/docs/learn/animations/) for the full surface. `Animated.FlatList`/`Animated.SectionList` have no Svelte wrapper yet. ## Cross-surface content (`createTunnel`) [Section titled “Cross-surface content (createTunnel)”](#cross-surface-content-createtunnel) ```svelte {#if toastVisible} Toast {/if} ``` React’s `createPortal` (and Vue’s `Teleport`) have no Svelte twin at all — `createPortal` is react-reconciler’s own Fiber-level `HostPortal` primitive with no equivalent in a framework with no reconciler, and Svelte has no built-in `Teleport`-shaped relocation either — so `createTunnel` is the only cross-surface primitive here, for two independently `mount()`-ed surfaces that share no Fabric tree at all. `TunnelIn`/`TunnelOut` take an explicit `tunnel` prop rather than `tunnel.In`/`tunnel.Out` member access like React/Vue: a `.svelte` file compiles to one fixed, top-level component, so there’s no runtime factory to construct a fresh pair per `createTunnel()` call the way Vue’s `defineComponent` can. ## App entry point [Section titled “App entry point”](#app-entry-point) index.js ```js import { createApp } from '@symbiote-native/svelte/bootstrap'; import App from './App.svelte'; createApp(App).mount('MyApp'); ``` Mirrors React/Vue’s `createApp(App).mount(appName)` two-step idiom. The lower-level `AppRegistry`/`setHostRegistrar` entry point described in the [Core API](/docs/api/core/#app-entry-point-appregistry) is also re-exported directly, for app code that wants it. `AppRegistry.setWrapperComponentProvider` is re-exported but currently **ignored** on this adapter: Vue composes a wrapper via `h(wrapper, ...)` at call time, but a compiled `.svelte` file has no equivalent runtime composition of two independently-authored components — there’s no working wrapper-component example to show here yet. ## Error boundaries (``) [Section titled “Error boundaries (\)”](#error-boundaries-svelteboundary) Svelte 5’s own `` is currently **Svelte-exclusive** among the five adapters — React’s `componentDidCatch` boundary and Vue’s `onErrorCaptured` aren’t documented patterns here yet: ```svelte console.error(error)}> {#snippet failed(error, reset)} {error.message} {/snippet} ``` Full behavior, including the `{@const}`-doesn’t-capture-`reset()` gotcha: [How to: catch render errors with ``](/docs/howtos/error-boundaries/). ## Boundary [Section titled “Boundary”](#boundary) Do not pass React component packages to the Svelte adapter. A third-party React Native package that ships a JavaScript React component still uses the React dispatcher internally. Non-React adapters need native-view wrappers instead — see the [Slider package](/docs/packages/slider/) for the reference implementation, shipping on React, Vue, Angular, Svelte, and Solid alike. # Vue API > The Vue adapter public contract. Primitives (`view`, `text`, `pressable`, `text-input`, `scroll-view`, …) are plain lowercase intrinsic tags — write them directly in a template or JSX, no import needed. Import only what’s still a real component or module from `@symbiote-native/vue`: ```ts import { StyleSheet } from '@symbiote-native/vue'; ``` Vue drives the same engine as React, but its public API follows Vue conventions. That matters most for events, children, and refs. ## Installation [Section titled “Installation”](#installation) ```sh pnpm add @symbiote-native/vue react-native vue ``` `react-native` and `vue` stay your app’s own top-level dependencies — this package replaces only the JS renderer that drives them, and `react-native` is the Metro version anchor, so it has to be pinned at the app root. `@symbiote-native/engine` is a peer dependency and installs alongside. TSX needs nothing beyond this; SFC additionally needs the Metro transformer for `.vue` files. ## Component shape [Section titled “Component shape”](#component-shape) * Events: attrs listeners like `@press` and `@value-change`, routed straight to the tag — same as any native DOM event in Vue. * Children: default slots. * Render children: list components only (`FlatList`, `SectionList`, …) still take a scoped slot; a primitive tag like `pressable` has no component body to call one against — see [Slots](#slots) below for the pressed-state workaround. * Refs: template refs to host nodes or exposed component handles. * Styles: `:style="styles.root"` with React Native-style objects. Most visual components — not just `View`/`Text` anymore — also accept `class`/`:class` for a registered class name, resolved through the same style registry as a Vue SFC ` ``` * Angular ```ts import { Component } from '@angular/core'; import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular'; import './app.css'; @Component({ selector: 'app-root', standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` Count is {{ count }} `, }) export class App { count = 0; } ``` app.css ```css .root { flex: 1; align-items: center; justify-content: center; } .text { font-size: 24px; } ``` * Svelte ```svelte (count += 1) }}> Count is {count} ``` * Solid ```tsx import './App.css'; import { createSignal } from 'solid-js'; export function App() { const [count, setCount] = createSignal(0); return ( setCount(value => value + 1)}> Count is {count()} ); } ``` App.css ```css .root { flex: 1; align-items: center; justify-content: center; } .text { font-size: 24px; } ``` ## What changed [Section titled “What changed”](#what-changed) | Concern | React | Vue | Angular | Svelte | Solid | | ----------- | ------------------------------ | ------------------------------ | ------------------------------ | ------------------------------------- | ---------------------------- | | State | `useState()` | `ref()` | plain class field | `$state()` rune | `createSignal()` | | Press event | `onPress` directly on `view` | `@press` directly on `view` | `(press)` on a `pressable` tag | `onPress` via a `p={{ onPress }}` bag | `onPress` directly on `view` | | Styling | `className="root"` + `App.css` | `class="root"` + SFC ` ``` * Angular ```ts import { Component } from '@angular/core'; import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular'; import './pressable.css'; @Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` {{ pressed ? 'Release' : 'Press me' }} `, }) export class PressableLabel { pressed = false; onPress(): void { console.log('pressed'); } } ``` pressable.css ```css .up { opacity: 1; } .down { opacity: 0.55; } ``` * Svelte ```svelte console.log('pressed'), onPressIn: () => (pressed = true), onPressOut: () => (pressed = false), }} > {pressed ? 'Release' : 'Press me'} ``` * Solid ```tsx import './Pressable.css'; import { createSignal } from 'solid-js'; export function PressableLabel() { const [pressed, setPressed] = createSignal(false); return ( console.log('pressed')} onPressIn={() => setPressed(true)} onPressOut={() => setPressed(false)} > {pressed() ? 'Release' : 'Press me'} ); } ``` Pressable.css ```css .up { opacity: 1; } .down { opacity: 0.55; } ``` ## API takeaway [Section titled “API takeaway”](#api-takeaway) React is the only adapter where `pressable` still exposes state through a children render function, `children: (state) => ReactNode`. The `pressable` tag on Vue, Angular, Svelte, and Solid has no render-prop channel for its children, so each one instead reads `onPressIn`/`onPressOut` directly and mirrors `pressed` into its own local reactive state, then derives a class or style from that: Vue’s `@press-in`/`@press-out`, Angular’s `(pressIn)`/`(pressOut)` `@Output()`s, Solid’s `onPressIn`/`onPressOut` props setting a signal, and Svelte’s `onPressIn`/`onPressOut` inside the `p={{ … }}` bag every event handler on a bare host tag needs. All five still read from the same shared press state machine underneath. `StyleSheet.create` works identically to a CSS class if you’d rather keep style objects inline — see the [Styling guide](/docs/learn/styling/). # TextInput example > Controlled text input in React, Vue, Angular, Svelte, and Solid. `text-input` is controlled through a `value` prop and a text-change event. The controlled write handshake is shared; the public event name is framework-shaped. * React ```tsx import './NameInput.css'; import { useState } from 'react'; export function NameInput() { const [name, setName] = useState('Symbiote'); return ( setName(event.text)} className="input" /> Hello, {name} ); } ``` NameInput.css ```css .root { gap: 12px; } .input { min-width: 220px; padding: 12px; border-width: 1px; } ``` * Vue ```vue ``` `v-model="name"` is sugar over the same `:value`/`@value-change` pair — write that explicit form instead if you also need the raw native event: `@value-change="event => setName(event.text)"`. The event handed to `@value-change` IS the native change event, with `text` merged onto it — there is no separate second argument. * Angular ```ts import { Component } from '@angular/core'; import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular'; import './name-input.css'; @Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` Hello, {{ name }} `, }) export class NameInput { name = 'Symbiote'; } ``` name-input.css ```css .root { gap: 12px; } .input { min-width: 220px; padding: 12px; border-width: 1px; } ``` `[(value)]="name"` is Angular’s banana-in-a-box two-way binding, sugar over the same `[value]`/`(valueChange)` pair — write that explicit form instead (`[value]="name" (valueChange)="setName($event)"`) if you also need the raw native event, which arrives on the separate `(change)` output: an `EventEmitter` only carries one value, so `(valueChange)` stays text-only to keep `[(value)]` working, and `(change)` exists purely for the raw event. * Svelte ```svelte (name = event.text)} class="input" > Hello, {name} ``` Svelte’s own element `bind:value` is explicitly unsupported by this adapter’s DOM shim — there is no sugar over `value`/`onValueChange` here, same as React. Unlike a plain `onPress`, `onValueChange` is safe to write as an individual attribute directly: it is always called with a single change-event object, so it never trips the crash an individual callback prop can otherwise hit on a bare host tag. * Solid ```tsx import './NameInput.css'; import { createSignal } from 'solid-js'; export function NameInput() { const [name, setName] = createSignal('Symbiote'); return ( setName(event.text)} class="input" /> Hello, {name()} ); } ``` NameInput.css ```css .root { gap: 12px; } .input { min-width: 220px; padding: 12px; border-width: 1px; } ``` No `v-model`-style sugar here either: `value`/`onValueChange` is the whole contract, same as React and Svelte. ## API takeaway [Section titled “API takeaway”](#api-takeaway) | Concern | React | Vue | Angular | Svelte | Solid | | ------------ | ---------------------------- | -------------------------------- | ---------------------------------------------- | ---------------------------- | ---------------------------- | | Text prop | `value` | `:value` or `v-model` | `[value]` or `[(value)]` | `value` | `value` | | Text change | `onValueChange={event => …}` | `@value-change` or `v-model` | `(valueChange)="handler($event)"` (text only) | `onValueChange={event => …}` | `onValueChange={event => …}` | | Native event | *(same event object)* | *(same emit, same event object)* | `(change)="handler($event)"` (separate output) | *(same event object)* | *(same event object)* | All five now share one contract: `onValueChange` (or its per-adapter emit/output spelling) is called with a **single** change-event object, `event.text` carries the current text, and `event` itself is the native change event — there is no separate second argument anywhere. Angular is the one exception, since its `(valueChange)` output is deliberately text-only to keep `[(value)]` working, with `(change)` carrying the full event instead. `StyleSheet.create` works identically to a CSS class if you’d rather keep style objects inline — see the [Styling guide](/docs/learn/styling/). # How it works > The SymbioteNative render pipeline from framework adapters to the stock React Native Fabric host. SymbioteNative is built around one architectural fact: Fabric already exposes a framework-agnostic JavaScript seam. React Native’s React renderer talks to this seam from its Fabric host config. SymbioteNative talks to the same seam from its own engine. ## Render pipeline [Section titled “Render pipeline”](#render-pipeline) ```txt Framework adapter React · Vue · Angular · Svelte · Solid ↓ Mutation API insert · remove · setProp · commit ↓ @symbiote-native/engine (JavaScript) builds a command buffer — no retained tree on this side ↓ one JSI crossing per commit SymbioteTree (C++) retained tree · clone-on-write · tag-keyed platform rules · event normalization ↓ Fabric's UIManager createNode · cloneNodeWithNewProps · appendChildToSet · completeRoot ↓ stock React Native Fabric C++ · JSI · Yoga · native views ``` ## Why the engine exists [Section titled “Why the engine exists”](#why-the-engine-exists) Most UI frameworks express host updates as mutations: set a prop, insert a child, remove a child. Fabric is persistent: you do not mutate an existing node; you clone nodes with new props or children and atomically commit a new child set. The engine is the translation layer between those worlds. Its JavaScript half turns adapter mutations into a buffer of ops; its C++ half — `SymbioteTree` — owns the retained tree, applies that buffer, and turns it into Fabric child set commits, one JSI crossing per commit instead of one per mutation. Platform behavior that React Native’s own components carry in JavaScript — a `Pressable`’s `disabled` folding into its accessibility state, a `Switch`’s different prop names per platform, a `Text`’s default `ellipsizeMode` — is folded into the same C++ layer, keyed off the Fabric tag a node was created with. Every adapter gets it once, instead of porting it per framework. That means each framework adapter stays thin. A persistence bug is fixed once in the engine, not once per framework. ## What stays stock [Section titled “What stays stock”](#what-stays-stock) SymbioteNative does not fork React Native native code. These stay ordinary React Native runtime pieces: * Fabric C++ shadow tree * Yoga layout * JSI and Hermes * iOS/Android host surfaces * native modules The only replaced layer is the JavaScript renderer that drives Fabric. ## Events [Section titled “Events”](#events) Fabric returns events to the same handle passed at node creation time. In React, that handle is a Fiber. In SymbioteNative, it is a retained-tree node owned by the engine. The adapter maps framework event syntax onto engine listeners, and the engine normalizes native events back into those listeners. ## Deeper architecture [Section titled “Deeper architecture”](#deeper-architecture) The public site should explain the architecture directly instead of pointing readers at local repository notes. Deeper design rationale can be promoted into standalone public pages as the docs grow. # How-tos > Task-oriented recipes for common SymbioteNative problems. Short, direct answers to “how do I do X” — each page is a lookup, not a tutorial. For the concept behind an answer, follow the link into [Learn](/docs/learn/react/) or [API](/docs/api/); for a full copyable app, see [Examples](/docs/examples/). ## Available how-tos [Section titled “Available how-tos”](#available-how-tos) * [Style a component](/docs/howtos/styling/) — `StyleSheet.create` vs an inline ` ``` * **iOS** — a `BootSplash.storyboard` file alongside the app’s existing storyboards. Re-run the same command (with `--brand`/`--dark-*` if you have a generator license key) any time the logo or colors change — it’s idempotent, not a one-shot scaffold. ## 3. Wire the generated theme into your native entry points [Section titled “3. Wire the generated theme into your native entry points”](#3-wire-the-generated-theme-into-your-native-entry-points) The generator does **not** do this part — it writes the theme/storyboard, but your app’s own `MainActivity`/`AppDelegate`/`Info.plist` still have to reference them. ### Android — `MainActivity.kt` [Section titled “Android — MainActivity.kt”](#android--mainactivitykt) Call `RNBootSplash.init` in `onCreate`, **before** `super.onCreate`: ```kotlin import android.os.Bundle import com.zoontek.rnbootsplash.RNBootSplash class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { RNBootSplash.init(this, R.style.BootTheme) super.onCreate(savedInstanceState) } // ... } ``` ### iOS — `AppDelegate.swift` + `Info.plist` [Section titled “iOS — AppDelegate.swift + Info.plist”](#ios--appdelegateswift--infoplist) Call `RNBootSplash.initWithStoryboard` from `customize(_:)`: ```swift import RNBootSplash class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { override func customize(_ rootView: RCTRootView) { super.customize(rootView) RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView) } } ``` Then point `Info.plist`’s launch-screen key at the generated storyboard: ```diff UILaunchStoryboardName LaunchScreen BootSplash ``` Run `pod install` inside `ios/` after adding the dependency — `RNBootSplash`’s native pod only links once CocoaPods has resolved it. Verify the native binary is actually wired Skipping the Android `RNBootSplash.init` call is **not** silent in JS: calling `hide()` / `isVisible()` before it runs throws `Error: react-native-bootsplash has not been initialized` (a real, frequently-reported setup mistake — see [zoontek/react-native-bootsplash#227](https://github.com/zoontek/react-native-bootsplash/issues/227)). A missed iOS storyboard/`Info.plist` step, by contrast, IS silent — the screen just shows the OS default (blank) or the wrong storyboard. Rebuild and run on a real device/simulator after this step and confirm your logo/background shows at cold launch before moving on to step 4. ## 4. Hide it from JS once your app is ready [Section titled “4. Hide it from JS once your app is ready”](#4-hide-it-from-js-once-your-app-is-ready) With the native binary wired, `hide()` is the simple case — call it once your JS tree has mounted: * React ```tsx import { useEffect } from 'react'; import { hide } from '@symbiote-native/splash-screen/react'; useEffect(() => { hide(); }, []); ``` * Vue ```vue ``` * Angular Called once from the root component’s `ngOnInit`: ```ts import { Component, OnInit } from '@angular/core'; import { hide } from '@symbiote-native/splash-screen/angular'; @Component({ /* ... */ }) export class App implements OnInit { ngOnInit(): void { hide(); } } ``` * Svelte ```svelte ``` * Solid ```tsx import { onMount } from 'solid-js'; import { hide } from '@symbiote-native/splash-screen/solid'; onMount(() => { hide(); }); ``` If you want a fade transition gated on real readiness (layout committed + logo/brand images loaded + your own `ready` flag) instead of an immediate cut, reach for `useHideAnimation` — covered with full React/Vue/Angular/Svelte/Solid examples on the [package page](/docs/packages/splash-screen/#the-animated-case-usehideanimation). ## Known upstream gotchas [Section titled “Known upstream gotchas”](#known-upstream-gotchas) These are `react-native-bootsplash` behaviors this wrapper inherits as-is — worth knowing before you file a bug against `@symbiote-native/splash-screen` itself: * **Dark mode follows the OS appearance setting, not an in-app theme toggle.** `darkBackground`/ `darkLogo` in the manifest are picked based on `getConstants().darkModeEnabled` — the *system* dark-mode flag read before JS runs, at native paint time. If your app has its own theme switcher independent of the OS setting, the native splash can briefly flash the *other* color scheme for a frame before your JS-driven UI repaints ([zoontek/react-native-bootsplash#743](https://github.com/zoontek/react-native-bootsplash/issues/743)). There is no JS-side fix — it’s a property of native paint happening before any JS runs. * **Android: relaunching via a notification can re-show, and occasionally get stuck on, the boot theme**, with `isVisible()` reporting `false` and `hide()` having no visible effect in that stuck state — an open upstream edge case, not something this wrapper can route around ([zoontek/react-native-bootsplash#736](https://github.com/zoontek/react-native-bootsplash/issues/736)). If you see a splash reappear only on notification-triggered launches, this is why. ## Recap [Section titled “Recap”](#recap) | Step | What | Where | | ---- | ------------------------------------------------------------ | ---------------------------------------------------- | | 1 | Install the wrapper | `package.json` | | 2 | Generate the binary (assets + native config) | `npx symbiote-splash-screen generate` | | 3 | Wire the generated theme/storyboard into native entry points | `MainActivity.kt`, `AppDelegate.swift`, `Info.plist` | | 4 | Hide it from JS | `hide()` / `useHideAnimation` in app code | Steps 1–3 are native, one-time setup per app; step 4 is the only part that lives in your framework code, and it’s identical in shape across React, Vue, Angular, Svelte, and Solid. # How to: style a component > Styling with a CSS class or CSS Modules (preferred), or StyleSheet.create. You need to style a native view and want the shortest path for your framework. ## Any adapter — a plain CSS class [Section titled “Any adapter — a plain CSS class”](#any-adapter--a-plain-css-class) A standalone `.css`/`.module.css` file import (`className` on React, `class`/`[class]` on Vue/Angular/Svelte/Solid) works identically on every adapter — this is what every current example app (`examples/react`, `examples/vue-sfc`, `examples/vue-tsx`, `examples/angular`, `examples/svelte`, `examples/solid`) actually does: ```tsx import './App.css'; Native surface ; ``` `@symbiote-native/css-parser` compiles the rule at build time; `className="card"` resolves it back at render time through a runtime class registry shared by every adapter. Plain CSS, CSS Modules, plus optional SCSS/Sass, Less, and Stylus preprocessing (` ``` ```svelte Native surface ``` ## The alternative — `StyleSheet.create` [Section titled “The alternative — StyleSheet.create”](#the-alternative--stylesheetcreate) Every adapter re-exports `StyleSheet` from `@symbiote-native/engine`. It’s still fully supported and needs no build-time CSS step — reach for it for a value that’s genuinely computed at runtime, or if you’d rather skip CSS entirely: ```tsx import { StyleSheet } from '@symbiote-native/react'; const styles = StyleSheet.create({ card: { padding: 16, borderRadius: 12, backgroundColor: '#111827' }, }); Native surface ; ``` `StyleSheet.create()` is identity at runtime — the engine flattens plain objects too. Its value is preserving literal types and giving the file one predictable style block at the bottom, not a different rendering path from CSS. ## Want typo-safe `.module.css` keys [Section titled “Want typo-safe .module.css keys”](#want-typo-safe-modulecss-keys) A bare `.module.css` import type-checks as `Record` unless you wire two pieces from `@symbiote-native/css-parser` — add it as a devDependency: ```sh pnpm add -D @symbiote-native/css-parser ``` package.json ```json { "scripts": { "pretypecheck": "css-dts ." } } ``` tsconfig.json ```json { "compilerOptions": { "plugins": [{ "name": "@symbiote-native/css-parser/typescript-plugin" }] } } ``` `pretypecheck`’s `css-dts .` writes a real, narrowed `.d.ts` next to every `.module.css` file so a typo fails `tsc`; the `typescript-plugin` gives you matching autocomplete live in the editor, no watch process needed. See the [Styling guide](/docs/learn/styling/#real-key-narrowing-css-dts-and-the-typescript-plugin) for what each piece covers. ## Choose based on your adapter [Section titled “Choose based on your adapter”](#choose-based-on-your-adapter) * Want CSS syntax → a standalone `.css`/`.module.css` file import works on every adapter; Vue and Svelte additionally support an inline ` ``` `:style`/`:class` composition and cascade precedence (explicit `:style` always wins over class-derived style) work the same as any other Vue app. `scoped` works the same as it does for real Vue: opt in with the attribute and every class in that block is suffixed to a per-component scope, so the same class name in two components never collides. `:global(...)` is the escape hatch back out — for a utility class meant to apply anywhere, exactly like real Vue’s scoped-CSS semantics, minus the DOM underneath. See the `.row`/`.flex1` utility classes in [`examples/vue-sfc/App.vue`](https://github.com/OneEyed1366/symbiote-native/blob/master/examples/vue-sfc/App.vue) for a real `scoped` stylesheet with a `:global()` escape running on device. `box-shadow`, `transform`, `filter`, `transform-origin`, and `background-image` (gradients) all map onto Fabric’s own native style props and compile like any other property: ```css .gradient-card { height: 64px; border-radius: 12px; background-image: linear-gradient(to right, #2b6cb0, #f6ad55); } ``` A property with genuinely no RN equivalent (`animation`, pseudo-classes, media queries) is dropped with a build warning instead — see “What is not CSS” below. ## Svelte’s own ` ``` Every class in the block is suffixed to a per-component scope, exactly like real Svelte’s own scoped-CSS semantics minus the DOM underneath — the same class name in two components never collides. `:global(...)` is the escape hatch back out, for a utility class meant to apply anywhere. See [`examples/svelte/screens/StyleShowcaseScreen.svelte`](https://github.com/OneEyed1366/symbiote-native/blob/master/examples/svelte/screens/StyleShowcaseScreen.svelte) for a real component using both a scoped block and a `:global()` mark running on device. Svelte has no inline ` ``` `$style` is the default binding name (or whatever `module="name"` sets); it’s a closed-over `const` holding the compiled name→scopedName map, usable from both the template and ` ``` ```ts import { Component } from '@angular/core'; import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular'; import styles from './card.module.css'; @Component({ selector: 'app-card', standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` Native surface `, }) export class CardComponent { readonly styles = styles; } ``` A plain `.css` import has no ambient type declaration needed (it’s side-effect only), but `.module.css` needs one so TypeScript resolves the import at all — a loose fallback works with no extra setup: css.d.ts ```ts declare module '*.module.css' { const classes: Record; export default classes; } ``` That types `styles.card` as `string`, not a literal key — a typo (`styles.crad`) still type-checks and silently resolves to `undefined` at runtime instead of failing the build. ### Real key narrowing — `css-dts` and the TypeScript plugin [Section titled “Real key narrowing — css-dts and the TypeScript plugin”](#real-key-narrowing--css-dts-and-the-typescript-plugin) `@symbiote-native/css-parser` ships two more pieces that close that gap for a standalone `.module.css`/`.module.scss`/`.module.less`/`.module.styl` file — add it as a direct `devDependency` of your app, alongside the ambient fallback above (it still covers any file the generator hasn’t reached yet): ```sh pnpm add -D @symbiote-native/css-parser ``` package.json ```json { "scripts": { "pretypecheck": "css-dts ." } } ``` * **`css-dts` (the `generate-dts-cli` bin)** walks the given paths and writes a real `Card.module.css.d.ts` next to each CSS Modules file it finds, with the actual exported keys as literal properties — no index signature, so `styles.crad` is a genuine `error TS2339` under `tsc`/`vue-tsc`. Wire it as a `pretypecheck` script so it runs before every typecheck, local or CI, with no dependency on Metro or a dev server; `css-dts --watch ` regenerates on save for a long-running local loop instead. * **`@symbiote-native/css-parser/typescript-plugin`** is a TypeScript language service plugin for live in-editor autocomplete on `.module.css` (plain CSS Modules only — SCSS/Less/Stylus fall back to the loose ambient type in the editor, since a language-service plugin must resolve synchronously and those preprocessors don’t offer a sync compile). Register it in `tsconfig.json`: ```json { "compilerOptions": { "plugins": [{ "name": "@symbiote-native/css-parser/typescript-plugin" }] } } ``` It recomputes on every keystroke inside the editor’s own `tsserver`, so there’s no watch process to keep running just for autocomplete. It only extracts simple `.foo { }` selectors — a compound (`.btn.primary`) or descendant (`.card .title`) selector still resolves correctly at build time through `css-dts`/the runtime registry, just without a matching in-editor suggestion. `css-dts` is the CI/`tsc`-time correctness guarantee, the plugin is the in-editor convenience — use both together. Vue’s inline ` ``` ```tsx import styles from './Card.module.scss'; ``` `sass`, `less`, and `stylus` are lazy, optional dependencies — install whichever one you actually use (`npm i -D sass`, `npm i -D less`, or `npm i -D stylus`); a project that never authors `.scss`/`.less`/`.styl` never needs any of the three. ## What is not CSS [Section titled “What is not CSS”](#what-is-not-css) There is no DOM, so there is nothing for a browser selector to match — pseudo-classes (`:hover`, `:focus`, `:nth-child`), media queries, and `animation` have no RN target and are dropped at build time, not silently misapplied. `:active` is the single exception, and it is not a general opening of CSS state: a press is the one state the engine itself tracks, so `.btn:active` compiles to a token beside `.btn` and wins the cascade exactly as its specificity says. Nothing else in that family follows — there is no hover or focus for it to mirror. Prefer `onPressIn`/`onPressOut` in the examples you write; reach for `:active` when the pressed look is purely visual and you want it resolved without a render. Tailwind CSS is not supported. Everything else — plain CSS (including `box-shadow`, `transform`, `filter`, `transform-origin`, and `background-image`), CSS Modules, and the SCSS/Less/Stylus preprocessors above — is a stable, build-time-only compile step: no CSS engine, no selector matching, and no extra runtime cost ships in the app bundle. It works the same way across React, Vue, Angular, Svelte, and Solid, including Svelte’s own default-scoped ` ``` `view`, `pressable`, and `text` are intrinsic tags — no import needed. Only a composed component (`FlatList`, `SectionList`, `VirtualizedList`, `VirtualizedSectionList`, `KeyboardAvoidingView`, …) still comes from `@symbiote-native/svelte`. A component’s own ` ``` `view`, `pressable`, and `text` are intrinsic tags — no import needed. Only a composed component (`FlatList`, `SectionList`, `VirtualizedList`, `VirtualizedSectionList`, `KeyboardAvoidingView`, …) still comes from `@symbiote-native/vue`. `StyleSheet.create` works identically if you’d rather keep style objects inline instead of an SFC `