Splash screen
@symbiote-native/splash-screen wraps
react-native-bootsplash so every
SymbioteNative adapter can hide the native launch screen and drive its fade-out — without
importing that library’s React hook body. Unlike the
slider wrapper, bootsplash has no native view to
register: it exposes only an imperative TurboModule (hide/isVisible/getConstants()), so
there is nothing for the engine’s ViewConfig path to reach — every adapter calls straight into
the same native module.
| OS platform | Support |
|---|---|
| iOS | ✅ live |
| Android | ✅ live |
| Framework adapter | Support |
|---|---|
| React | ✅ live |
| Vue | ✅ live |
| Angular | ✅ live |
| Svelte | ✅ live |
| Solid | ✅ live |
Installation
Section titled “Installation”npm install @symbiote-native/splash-screenScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --splash-screen (or
add --splash-screen in an existing app) installs and wires this for you — see
@symbiote-native/cli.
@symbiote-native/splash-screen itself is a workspace package (packages/splash-screen), not
yet published — add it as a workspace dependency the same way the examples do. It ships as the
sole autolinked native proxy for react-native-bootsplash (react-native.config.cjs +
symbiote-splash-screen.podspec), so an app never lists react-native-bootsplash directly.
The imperative case: hide()
Section titled “The imperative case: hide()”Once the native launch screen is wired up and shown at boot, call hide() after your JS tree
has mounted. hide()/isVisible() carry zero framework dependency — they are the same
react-native-bootsplash functions, re-exported verbatim by every adapter.
import { useEffect } from 'react';import { hide } from '@symbiote-native/splash-screen/react';
export default function App() { useEffect(() => { hide(); }, []);
// ...}<script setup lang="ts">import { onMounted } from 'vue';import { hide } from '@symbiote-native/splash-screen/vue';
onMounted(() => hide());</script>import { Component } from '@angular/core';import { hide } from '@symbiote-native/splash-screen/angular';
@Component({ standalone: true, template: `...` })export class AppRoot { constructor() { hide(); }}<script lang="ts"> import { hide } from '@symbiote-native/splash-screen/svelte';
$effect(() => { hide(); });</script>Bare hide() on mount, matching examples/svelte/App.svelte — this repo’s own canary calls
it exactly this way at the app root, inside an $effect with no dependency, since a Svelte
component’s <script> body itself runs only once and there is no ongoing state to react to.
import { onMount } from 'solid-js';import { hide } from '@symbiote-native/splash-screen';
export default function App() { onMount(() => { hide(); });
// ...}Bare hide() inside onMount, matching examples/solid/App.tsx - this repo’s own canary
calls it exactly this way at the app root.
The animated case: useHideAnimation
Section titled “The animated case: useHideAnimation”import { useState } from 'react';import { Animated } from '@symbiote-native/react';import { useHideAnimation } from '@symbiote-native/splash-screen/react';import manifest from '../assets/bootsplash/manifest.json';import logo from '../assets/bootsplash/logo.png';
export default function AnimatedSplash() { const [opacity] = useState(() => new Animated.Value(1)); const [ready, setReady] = useState(false);
const { container, logo: logoProps } = useHideAnimation({ manifest, logo, ready, animate: () => { Animated.timing(opacity, { toValue: 0, duration: 250, useNativeDriver: true }).start(); }, });
return ( <Animated.View {...container} style={[container.style, { opacity }]}> <image {...logoProps} /> </Animated.View> );}<script setup lang="ts">import { ref } from 'vue';import { useHideAnimation } from '@symbiote-native/splash-screen/vue';import manifest from '../assets/bootsplash/manifest.json';import logo from '../assets/bootsplash/logo.png';
const ready = ref(false);
const hideAnimation = useHideAnimation(() => ({ manifest, logo, ready: ready.value, animate: () => { /* your own fade-out */ },}));</script>
<template> <view v-bind="hideAnimation.container"> <image v-bind="hideAnimation.logo" /> </view></template>useHideAnimation takes a config getter, not a plain value — a Vue composable’s setup body
runs once, so the getter lets it keep reading whatever reactive refs it closes over.
import { Component, inject, signal } from '@angular/core';import { HideAnimationService } from '@symbiote-native/splash-screen/angular';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import manifest from '../assets/bootsplash/manifest.json';import logo from '../assets/bootsplash/logo.png';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <view [style]="hideAnimation().container.style" (layout)="hideAnimation().container.onLayout()" > <image [source]="hideAnimation().logo.source" (loadEnd)="hideAnimation().logo.onLoadEnd?.()" /> </view> `,})export class AnimatedSplash { readonly ready = signal(false);
readonly hideAnimation = inject(HideAnimationService).connect(() => ({ manifest, logo, ready: this.ready(), animate: () => { /* your own fade-out */ }, }));}HideAnimationService.connect() is Angular’s twin of the React hook / Vue composable — Angular
has no per-instance hook, so state and lifecycle live in DI instead; connect() also takes a
config getter and returns a Signal. Unlike React’s {...container} spread or Vue’s
v-bind="hideAnimation.container", Angular has no generic prop-spread — onLayout/onLoadEnd
must be wired explicitly through the real (layout)/(loadEnd) outputs View/Image expose,
calling the readiness callbacks container.onLayout/logo.onLoadEnd by hand; skipping them
means the readiness gate never completes and hide() never fires.
<script lang="ts"> import { Animated } from '@symbiote-native/svelte'; import { useHideAnimation } from '@symbiote-native/splash-screen/svelte'; import manifest from '../assets/bootsplash/manifest.json'; import logo from '../assets/bootsplash/logo.png';
const opacity = new Animated.Value(1); let ready = $state(false);
const hideAnimation = useHideAnimation(() => ({ manifest, logo, ready, animate: () => { Animated.timing(opacity, { toValue: 0, duration: 250, useNativeDriver: true }).start(); }, }));</script>
<Animated.View {...hideAnimation.current.container} style={[hideAnimation.current.container.style, { opacity }]}><image {...hideAnimation.current.logo} /></Animated.View>useHideAnimation takes a config getter, not a plain value — a Svelte component’s
<script> body runs once, like Vue’s setup, so the getter is what lets it keep reading
ready as it changes. The return value is a getter too (hideAnimation.current), the same
boxed-getter convention as this adapter’s other runes, since a raw $state doesn’t survive
being returned from a plain function.
import { createSignal } from 'solid-js';import { Animated } from '@symbiote-native/solid';import { createHideAnimation } from '@symbiote-native/splash-screen/solid';import manifest from '../assets/bootsplash/manifest.json';import logo from '../assets/bootsplash/logo.png';
export default function AnimatedSplash() { const opacity = new Animated.Value(1); const [ready, setReady] = createSignal(false);
const hideAnimation = createHideAnimation(() => ({ manifest, logo, ready: ready(), animate: () => { Animated.timing(opacity, { toValue: 0, duration: 250, useNativeDriver: true }).start(); }, }));
return ( <Animated.View {...hideAnimation().container} style={[hideAnimation().container.style, { opacity }]} > <image {...hideAnimation().logo} /> </Animated.View> );}createHideAnimation takes a config accessor, not a plain value - a Solid component body
runs once, so the accessor is what lets it keep reading ready as it changes. The return value
is an accessor too (hideAnimation()), Solid’s own primitive convention, the same shape as this
adapter’s other create* primitives.
hide() / isVisible()
Section titled “hide() / isVisible()”| Signature | Description |
|---|---|
hide(config?: { fade?: boolean }): Promise<void> |
Hides the native splash screen. fade: true fades the native view out before removing it; omitted/false removes it immediately — independent of the JS-side useHideAnimation fade below |
isVisible(): boolean |
Reads whether the native splash screen is currently shown. Synchronous — iOS exports it as a blocking sync method, Android returns a plain boolean |
useHideAnimation() config
Section titled “useHideAnimation() config”| Field | Type | Default | Description |
|---|---|---|---|
manifest |
IManifest |
(required) | The generated assets/bootsplash/manifest.json — background color plus logo size, optionally a dark background and/or brand |
logo / darkLogo |
IImageSourceProp |
— | The logo image to fade in; omit to skip the logo readiness gate entirely |
brand / darkBrand |
IImageSourceProp |
— | A secondary “powered by”-style image below the logo; only read when manifest.brand is present |
ready |
boolean |
true |
Your own extra readiness gate (e.g. “auth check done”) — animate waits for this to be true too |
animate |
() => void |
(required) | Called exactly once, the moment every readiness condition (layout committed + images loaded + ready) is met — put your own fade-out here |
statusBarTranslucent |
boolean |
false |
Android-only: skip the extra top margin compensation when your app already draws a translucent status bar |
navigationBarTranslucent |
boolean |
false |
Android-only: skip the extra bottom margin compensation when your app already draws a translucent navigation bar |
useHideAnimation() return value
Section titled “useHideAnimation() return value”| Field | Type | Description |
|---|---|---|
container |
{ style, onLayout } |
Bind onto your own View — style positions and colors the full-screen backdrop, onLayout reports layout readiness |
logo |
{ source, fadeDuration, resizeMode, style, onLoadEnd } |
Bind onto your own Image — onLoadEnd reports logo-load readiness |
brand |
{ source, fadeDuration, resizeMode, style, onLoadEnd } |
Same shape as logo, bound onto a second Image for the optional brand asset |
- Calling
hide()on your very first render is safe — it queues rather than failing. iOS holds the resolver until a 0.35 s timer started during native init reports that the system launch screen has faded out; Android re-posts the request every 100 ms while the module is still initializing or no activity is resumed. The promise settles late; it does not reject. useHideAnimationhides the native screen without a fade. It callshide({ fade: false })itself the moment the readiness gate closes, because youranimate()owns the fade-out —hide({ fade: true })only matters when you hide the splash by hand.animate()runs at most once per mount. The controller latches after the first call, and the logo/brand readiness flags are captured when it is constructed — a config that later drops itslogo/brandsource does not flip them back. Ifreadynever becomestrue, or a readiness callback is never wired,hide()never fires and the splash stays up.- Native constants are read once, on first render.
darkModeEnabledis what picksdarkBackground/darkLogo/darkBrand, so switching the system theme while the splash is still on screen does not re-pick the dark assets. - Only Android reports layout constants. iOS exports
darkModeEnabledalone;statusBarHeight,navigationBarHeightandlogoSizeRatioare Android-only, which is why the container’s negative margins and the logo scaling apply there and nowhere else. On Samsung One UI 4 deviceslogoSizeRatiois0.5, so the logo renders at half its manifest size by design.
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/splash-screen ships zero native metadata of its own — the native launch
screen is generated once by the bundled symbiote-splash-screen CLI (a thin passthrough to
react-native-bootsplash’s own generator) and wired into the app’s native entry points by hand,
see the native-setup how-to. The package’s own
code is a pure JS bridge:
packages/splash-screen/src/├── core/ # framework-agnostic: hide/isVisible re-export, getHideAnimationConstants│ # (reads RNBootSplash's TurboModule via getEnforcingNativeModule),│ # HideAnimationController (readiness state machine), computeHideAnimationStyles├── react/ # React lifecycle (hooks) over the core├── vue/ # Vue lifecycle (composables) over the core├── angular/ # Angular lifecycle (a DI service) over the core└── svelte/ # Svelte lifecycle (runes/use-hide-animation.svelte.ts) over the coreEvery adapter’s useHideAnimation constructs the same HideAnimationController once, reads
native constants once, and re-syncs the controller’s config through its own reactivity primitive
— a React effect with no dependency array, a Vue watchEffect, an Angular effect(), a Svelte
$effect. The style
computation itself (computeHideAnimationStyles) is a faithful, framework-agnostic port of
react-native-bootsplash’s own useHideAnimation useMemo body, so the container/logo/brand
prop bags are byte-for-byte the same shape upstream produces — the same logic/lifecycle split as
every other SymbioteNative component (see
how it works).