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 |
Installation
Section titled “Installation”npm install @symbiote-native/brightnessexpo-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).
Reading and setting brightness
Section titled “Reading and setting brightness”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 { Pressable, Text, View } from '@symbiote-native/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> );}<script setup lang="ts">import { onMounted, onUnmounted, ref } from 'vue';import { Pressable, Text, View } from '@symbiote-native/vue';import { addBrightnessListener, getBrightnessAsync, setBrightnessAsync } from '@symbiote-native/brightness';import type { EventSubscription } from '@symbiote-native/brightness';
const brightness = ref<number | null>(null);let subscription: EventSubscription | undefined;
onMounted(() => { void getBrightnessAsync().then(value => (brightness.value = value)); subscription = addBrightnessListener(event => (brightness.value = event.brightness));});
onUnmounted(() => subscription?.remove());</script>
<template> <View> <Text>{{ brightness === null ? 'checking…' : `${Math.round(brightness * 100)}%` }}</Text> <Pressable @press="setBrightnessAsync(0.5)"> <Text>Set to 50%</Text> </Pressable> </View></template>import { Component, OnDestroy, signal } from '@angular/core';import { Pressable, Text, View } from '@symbiote-native/angular';import { addBrightnessListener, getBrightnessAsync, setBrightnessAsync } from '@symbiote-native/brightness';import type { EventSubscription } from '@symbiote-native/brightness';
@Component({ standalone: true, imports: [Pressable, Text, View], template: ` <View> <Text>{{ brightness() === null ? 'checking…' : (brightness()! * 100 | number: '1.0-0') + '%' }}</Text> <Pressable (press)="setBrightnessAsync(0.5)"> <Text>Set to 50%</Text> </Pressable> </View> `,})export class BrightnessControl implements OnDestroy { readonly brightness = signal<number | null>(null); private readonly subscription: EventSubscription;
constructor() { getBrightnessAsync().then(value => this.brightness.set(value)); this.subscription = addBrightnessListener(event => this.brightness.set(event.brightness)); }
ngOnDestroy(): void { this.subscription.remove(); }}<script lang="ts"> import { Pressable, Text, View } from '@symbiote-native/svelte'; import { addBrightnessListener, getBrightnessAsync, setBrightnessAsync } from '@symbiote-native/brightness'; import type { EventSubscription } from '@symbiote-native/brightness';
let brightness = $state<number | null>(null);
$effect(() => { getBrightnessAsync().then(value => (brightness = value)); const subscription: EventSubscription = addBrightnessListener(event => (brightness = event.brightness)); return () => subscription.remove(); });</script>
<View> <Text>{brightness === null ? 'checking…' : `${Math.round(brightness * 100)}%`}</Text> <Pressable onPress={() => setBrightnessAsync(0.5)}> <Text>Set to 50%</Text> </Pressable></View>Permission
Section titled “Permission”import { Pressable, Text } from '@symbiote-native/react';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> </> );}<script setup lang="ts">import { usePermissions } from '@symbiote-native/brightness/vue';
const { status, error, request, get } = usePermissions();</script>
<template> <Text>{{ error ? `check failed: ${error.message}` : (status?.status ?? 'checking…') }}</Text> <Pressable @press="error ? get() : request()"> <Text>{{ error ? 'Retry check' : 'Request permission' }}</Text> </Pressable></template>import { Component, inject } from '@angular/core';import { Pressable, Text } from '@symbiote-native/angular';import { PermissionsService } from '@symbiote-native/brightness/angular';
@Component({ standalone: true, imports: [Pressable, Text], template: ` <Text>{{ error() ? 'check failed: ' + error()?.message : (status()?.status ?? 'checking…') }}</Text> <Pressable (press)="error() ? permissions.get() : permissions.request()"> <Text>{{ error() ? 'Retry check' : 'Request permission' }}</Text> </Pressable> `,})export class BrightnessPermission { readonly permissions = inject(PermissionsService); readonly status = this.permissions.connect(); readonly error = this.permissions.error;}<script lang="ts"> import { Pressable, Text } from '@symbiote-native/svelte'; import { usePermissions } from '@symbiote-native/brightness/svelte';
const permissions = usePermissions();</script>
<Text> {permissions.error ? `check failed: ${permissions.error.message}` : (permissions.status?.status ?? 'checking…')}</Text><Pressable onPress={() => (permissions.error ? permissions.get() : permissions.request())}> <Text>{permissions.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.
Functions
Section titled “Functions”| 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 |
usePermissions()
Section titled “usePermissions()”React (/react) |
Vue (/vue) |
Angular (/angular) |
Svelte (/svelte) |
Signature | Returns |
|---|---|---|---|---|---|
usePermissions |
usePermissions |
PermissionsService.connect() |
usePermissions |
() |
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 |
usePermissions() return value
Section titled “usePermissions() return value”| 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.
BrightnessMode
Section titled “BrightnessMode”| 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 |
BrightnessEvent
Section titled “BrightnessEvent”| Field | Type | Description |
|---|---|---|
brightness |
number |
The current brightness value between 0 and 1, inclusive |
setSystemBrightnessAsyncswitches the device out of adaptive brightness. Before writing the value, the Android module putsSCREEN_BRIGHTNESS_MODEintoMANUAL— the device stays manual afterwards until something sets the mode back, which is whatsetSystemBrightnessModeAsyncis for.- The
WRITE_SETTINGSgrant is re-checked on every system write.setSystemBrightnessAsyncandsetSystemBrightnessModeAsynccallSettings.System.canWriteeach time and throw a permissions exception when it isfalse— a grant revoked after the fact surfaces as a rejected promise, not as aPermissionResponse, so keep acatchon those two even oncerequestPermissionsAsynchas resolved granted. - Android quantizes the system value.
0..1is mapped onto Android’s integer1..255range on write and back on read, so a written brightness round-trips approximately, never exactly. While the device is inAUTOMATICmode,getSystemBrightnessAsyncreads the auto-brightness adjustment setting instead, rescaled — not the brightness actually on screen. restoreSystemBrightnessAsyncclears an override rather than replaying a saved value. It puts the activity window’s brightness back toBRIGHTNESS_OVERRIDE_NONE, handing the screen back to the system setting; nothing this app wrote earlier is restored. The same override is whygetBrightnessAsyncreports the system brightness until your firstsetBrightnessAsynccall installs one.
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/brightness ships zero React/Vue/Angular 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└── angular/services/permissions.service @symbiote-native/brightness/angularusePermissions 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 four 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).