System UI
@symbiote-native/system-ui wraps
expo-system-ui — setting and
reading the root view’s background color — so every SymbioteNative adapter can reach it, not
just React. Like device and local auth,
both exports here are one-shot async calls with no per-instance state, so there is no
hook/composable/service to wrap — the React, Vue, Angular, Svelte, and Solid entry points are
plain re-exports of the same core.
| 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/system-uiScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --system-ui (or
add --system-ui in an existing app) installs and wires this for you — see
@symbiote-native/cli.
expo-system-ui and expo-modules-core come along as regular dependencies, pinned to exact
versions — never install either yourself, and never add the expo meta-package to your project
(it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).
expo-system-ui needs no runtime permission on either platform — it only sets/reads a stored
background color, nothing gated by a permission prompt.
All five adapters — React, Vue, Angular, Svelte, Solid — re-export the exact same two functions; there is no per-adapter hook/composable/service to reach for, since nothing here holds live state or a subscription.
import { useEffect } from 'react';import { setBackgroundColorAsync } from '@symbiote-native/system-ui/react';
export default function RootScreen() { useEffect(() => { void setBackgroundColorAsync('black'); }, []);
return ( <view> <text>Root background set to black</text> </view> );}<script setup lang="ts">import { onMounted } from 'vue';import { setBackgroundColorAsync } from '@symbiote-native/system-ui/vue';
onMounted(() => { void setBackgroundColorAsync('black');});</script>
<template> <view> <text>Root background set to black</text> </view></template>import { Component } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { setBackgroundColorAsync } from '@symbiote-native/system-ui/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <view> <text>Root background set to black</text> </view> `,})export class RootScreen { constructor() { void setBackgroundColorAsync('black'); }}There’s no per-instance service to inject() here — both functions are plain exports off the
core package, called straight in the constructor or wherever the app’s root is set up.
<script lang="ts"> import { setBackgroundColorAsync } from '@symbiote-native/system-ui/svelte';
$effect(() => { void setBackgroundColorAsync('black'); });</script>
<view><text>Root background set to black</text></view>import { onMount } from 'solid-js';import { setBackgroundColorAsync } from '@symbiote-native/system-ui/solid';
export function RootScreen() { onMount(() => { void setBackgroundColorAsync('black'); });
return ( <view> <text>Root background set to black</text> </view> );}Functions
Section titled “Functions”| Signature | Description |
|---|---|
setBackgroundColorAsync(color: ColorValue | null): Promise<void> |
Changes the root view background color. Call this outside of your component tree (e.g. in the root file), since it affects the whole app. null clears the override rather than setting an actual color |
getBackgroundColorAsync(): Promise<ColorValue | null> |
Gets the current root view background color, in hex format. null if the background color is not set |
setBackgroundColorAsync(null)clears the override rather than setting an actual color — it’s passed straight through to the native module without runningprocessColor.- On web, the raw
ColorValueis passed through untouched instead of being run through RN’sprocessColor— matching upstream, sinceprocessColoris a native-color-parsing step that doesn’t apply on that platform. - On iOS/Android, a non-null color is run through RN’s own
processColorbefore reaching the native module — the same conversion RN’s style pipeline uses internally.
Common questions
Section titled “Common questions”- Where do I call
setBackgroundColorAsync? In the root file, outside any component, so the color is set before the first frame and no flash shows during transitions. - How do I follow dark and light mode? Read the color scheme and call it again when it changes.
getBackgroundColorAsyncreturnsnull. No root background color has been set yet.- Can I set
userInterfaceStyleat runtime? No. The Android style and iOS background color are native build settings and need a new binary.
Sources: Expo docs: SystemUI, Evan Bacon: stack-based root background component.
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/system-ui ships zero React/Vue/Angular/Svelte/Solid logic in expo-system-ui
itself — its
two functions are hand-ported, verbatim, into this package’s own core/, resolving the native
module through expo-modules-core’s requireNativeModule rather than the expo meta-package
this project never installs:
packages/system-ui/src/├── core/ # framework-agnostic: setBackgroundColorAsync/getBackgroundColorAsync;│ # native-module.ts resolves the native module via expo-modules-core's│ # requireNativeModule├── react/ # @symbiote-native/system-ui/react — export * from '../core'├── vue/ # @symbiote-native/system-ui/vue — export * from '../core'├── angular/ # @symbiote-native/system-ui/angular — export * from '../core'├── svelte/ # @symbiote-native/system-ui/svelte — export * from '../core'└── solid/ # @symbiote-native/system-ui/solid — export * from '../core'Same shape as device’s and local auth’s
adapter entries: single-file re-exports with no lifecycle code at all, since every export
here is a stateless one-shot async call — there is nothing for a hook, composable, or service to
subscribe to or clean up. The native code itself is never vendored or copied —
expo-modules-autolinking resolves it straight out of node_modules (see the native setup
guide).