Skip to content

Brightness

@symbiote-native/brightness wraps expo-brightness so every SymbioteNative adapter can read and set the screen brightness. Like battery and cellular, it’s built on expo-modules-core — a pure async-function + EventEmitter surface, no Fabric view involved. Its permission surface (getPermissionsAsync/ requestPermissionsAsync) shares the exact same usePermissions() hook/composable/service shape as @symbiote-native/cellular, so switching between the two packages needs no relearning; what’s unique to brightness is an Android-only system-brightness-mode surface and an iOS-only change listener.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Solid ✅ live
Terminal window
npm install @symbiote-native/brightness

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --brightness (or add --brightness in an existing app) installs and wires this for you — see @symbiote-native/cli.

expo-brightness 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).

There is no live-value hook for brightness itself (unlike battery’s useBatteryLevel) — seed from getBrightnessAsync() on mount, then subscribe to addBrightnessListener for live updates. The listener only ever fires on iOS; on Android the value only changes in response to your own setBrightnessAsync calls.

import { useEffect, useState } from 'react';
import { addBrightnessListener, getBrightnessAsync, setBrightnessAsync } from '@symbiote-native/brightness';
export default function BrightnessControl() {
const [brightness, setBrightness] = useState<number | null>(null);
useEffect(() => {
getBrightnessAsync().then(setBrightness);
const subscription = addBrightnessListener(event => setBrightness(event.brightness));
return () => subscription.remove();
}, []);
return (
<view>
<text>{brightness === null ? 'checking…' : `${Math.round(brightness * 100)}%`}</text>
<pressable onPress={() => setBrightnessAsync(0.5)}>
<text>Set to 50%</text>
</pressable>
</view>
);
}
import { usePermissions } from '@symbiote-native/brightness/react';
export default function BrightnessPermission() {
const [status, request, get, error] = usePermissions();
return (
<>
<text>{error ? `check failed: ${error.message}` : (status?.status ?? 'checking…')}</text>
<pressable onPress={() => (error ? get() : request())}>
<text>{error ? 'Retry check' : 'Request permission'}</text>
</pressable>
</>
);
}

status stays null both while the first fetch is in flight and after it fails, so error is what tells those two apart; get() re-runs the check without prompting the user.

Signature Description
isAvailableAsync(): Promise<boolean> Whether getBrightnessAsync/setBrightnessAsync exist on the native module
getBrightnessAsync(): Promise<number> Current screen brightness between 0 and 1, inclusive
setBrightnessAsync(value: number): Promise<void> Sets the screen brightness (0..1, clamped); iOS only affects the app while foregrounded, Android persists until changed again
getSystemBrightnessAsync(): Promise<number> Gets the system-wide brightness. Delegates to getBrightnessAsync on every platform except Android, since iOS has no separate system-level value
setSystemBrightnessAsync(value: number): Promise<void> Sets the system-wide brightness. Delegates to setBrightnessAsync on every platform except Android
restoreSystemBrightnessAsync(): Promise<void> Resets the system brightness to the value it had before this app started controlling it. No-op on every platform except Android
isUsingSystemBrightnessAsync(): Promise<boolean> Whether the activity’s window has no brightness override of its own, so the system-wide value is what the screen shows. It cannot distinguish “the app set the system value” from “the app never touched brightness” — it reports only that setBrightnessAsync is not overriding this window. Always false except on Android
getSystemBrightnessModeAsync(): Promise<BrightnessMode> Gets the system brightness mode. Always resolves BrightnessMode.UNKNOWN except on Android
setSystemBrightnessModeAsync(mode: BrightnessMode): Promise<void> Sets the system brightness mode. No-op except on Android, and also a no-op when passed BrightnessMode.UNKNOWN
getPermissionsAsync(): Promise<PermissionResponse> Checks the user’s permission for accessing the system brightness
requestPermissionsAsync(): Promise<PermissionResponse> Asks the user for permission to access the system brightness
addBrightnessListener(listener): EventSubscription Subscribes to brightness-change events. Only fires on iOS — never on Android; call .remove() on the returned subscription to unsubscribe
React (/react) Vue (/vue) Angular (/angular) Svelte (/svelte) Solid (/solid) Signature Returns
usePermissions usePermissions PermissionsService.connect() usePermissions createPermissions () React: [PermissionResponse | null, request, get, Error | null] tuple. Vue: { status: Ref<PermissionResponse | null>, error: Ref<Error | null>, request, get }. Angular: Signal<PermissionResponse | null> from connect(), plus error: Signal<Error | null> and request()/get() on the service. Svelte: { status, error, request, get }, status and error getters over PermissionResponse | null / Error | null. Solid: { status: Accessor<PermissionResponse | null>, error: Accessor<Error | null>, request, get }
Field Type Description
status PermissionResponse | null The permission status last read, null until the first fetch resolves
error Error | null Why the automatic fetch left status at null. Cleared by the next successful get()/request()
request () => Promise<PermissionResponse> Asks the user for the permission, then updates status and clears error. Rejects to its caller when the native call fails
get () => Promise<PermissionResponse> Re-reads the current status without prompting. Same update and rejection behavior as request

React hands those back positionally, as [status, request, get, error], so existing two- and three-element destructuring keeps working. Vue wraps status and error in refs; Angular exposes status through connect() and error as a separate readonly signal on the service; Svelte returns both as getters, read as permissions.status / permissions.error.

Read together, the two fields separate the three states:

status error Meaning
null null Not fetched yet
null Error The automatic fetch failed
PermissionResponse null Fetched

Every variant auto-fetches the current permission status once on mount/connect(), and updates again whenever request/get resolves. A failure in that automatic fetch lands in error instead of escaping as an unhandled rejection; get()/request() called by hand still reject to their caller. Angular’s auto-fetch is latched to at most one run per service instance, so a later connect() never re-fetches: call get() to retry.

Member Value Description
UNKNOWN 0 Returned when the brightness mode cannot be determined
AUTOMATIC 1 Automatic brightness mode, tracking the ambient light sensor
MANUAL 2 Manual brightness mode, set by the user or by setSystemBrightnessAsync
Field Type Description
brightness number The current brightness value between 0 and 1, inclusive
  • setSystemBrightnessAsync switches the device out of adaptive brightness. Before writing the value, the Android module puts SCREEN_BRIGHTNESS_MODE into MANUAL — the device stays manual afterwards until something sets the mode back, which is what setSystemBrightnessModeAsync is for.
  • The WRITE_SETTINGS grant is re-checked on every system write. setSystemBrightnessAsync and setSystemBrightnessModeAsync call Settings.System.canWrite each time and throw a permissions exception when it is false — a grant revoked after the fact surfaces as a rejected promise, not as a PermissionResponse, so keep a catch on those two even once requestPermissionsAsync has resolved granted.
  • Android quantizes the system value. 0..1 is mapped onto Android’s integer 1..255 range on write and back on read, so a written brightness round-trips approximately, never exactly. While the device is in AUTOMATIC mode, getSystemBrightnessAsync reads the auto-brightness adjustment setting instead, rescaled — not the brightness actually on screen.
  • restoreSystemBrightnessAsync clears an override rather than replaying a saved value. It puts the activity window’s brightness back to BRIGHTNESS_OVERRIDE_NONE, handing the screen back to the system setting; nothing this app wrote earlier is restored. The same override is why getBrightnessAsync reports the system brightness until your first setBrightnessAsync call installs one.
  • My brightness change disappears on iOS. setBrightnessAsync persists only until the device locks, then the user’s setting returns.
  • On Android it only works while my app is open. The value applies to the current activity and overrides the system brightness while the app is in the foreground. Call restoreSystemBrightnessAsync to go back to the system value.
  • How do I change the system brightness on Android? setSystemBrightnessAsync (experimental) needs the WRITE_SETTINGS permission, granted by the user in system settings.
  • Permission result is empty after granting WRITE_SETTINGS. Reported upstream; re-query the permission after returning from settings.
  • Brightness 0 on Android. Fixed in upstream 55.0.5 by mapping [0, 1] onto Android’s [1, 255].

Sources: Expo docs: Brightness, expo/expo#18967, expo-brightness changelog.

@symbiote-native/brightness ships zero React/Vue/Angular/Svelte/Solid logic in expo-brightness itself — that package’s own JS hard-imports PermissionResponse/PermissionStatus from the expo meta-package (which this project never installs), so its functions and types are hand-ported, verbatim, into this package’s own core/, changing only that one import line to pull from expo-modules-core instead:

packages/brightness/src/
├── core/ isAvailableAsync/getBrightnessAsync/setBrightnessAsync + the
│ Android system-brightness surface + getPermissionsAsync/
│ requestPermissionsAsync + addBrightnessListener; native-module.ts
│ resolves the single `ExpoBrightness` native module via
│ expo-modules-core's requireNativeModule
├── react/hooks/use-permissions @symbiote-native/brightness/react
├── vue/composables/use-permissions @symbiote-native/brightness/vue
├── svelte/runes/use-permissions @symbiote-native/brightness/svelte
├── solid/primitives/use-permissions @symbiote-native/brightness/solid
└── angular/services/permissions.service @symbiote-native/brightness/angular

usePermissions is the only stateful surface — auto-fetch on mount, expose get/request as imperative callbacks — written once as a shared pattern and reapplied identically in cellular; every other export is a stateless free function, re-exported verbatim by all five adapters. The native code itself is never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see the native setup guide).