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 |
Installation
Section titled “Installation”npm install @symbiote-native/splash-screen@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.
The animated case: useHideAnimation
Section titled “The animated case: useHideAnimation”import { useState } from 'react';import { View, Image, 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 { Image, View } from '@symbiote-native/angular';import manifest from '../assets/bootsplash/manifest.json';import logo from '../assets/bootsplash/logo.png';
@Component({ standalone: true, imports: [View, Image], 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 { View, Image, 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';
// Animated.View is dotted, so it can't be a template tag — aliased first, matching // examples/svelte's own AnimatedDemo.svelte convention. const AnimatedView = Animated.View; 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>
<AnimatedView {...hideAnimation.current.container} style={[hideAnimation.current.container.style, { opacity }]}><Image {...hideAnimation.current.logo} /></AnimatedView>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.
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).