RORK LABJP
IOS27 — iOS 27 and iPadOS 27 land on September 14. For apps assembled with no-code or AI tooling, the week a new OS ships is when the ground moves mostDUO — Apple's first foldable, the iPhone Duo, starts at $1,999. A new screen shape is also the first place a generated layout tends to breakSPLIT — Standard Rork produces React Native through Expo. Rork Max generates native Swift. Neither will adapt to a folding screen at the same pace, or fail in the same waySUBMIT — Automated submission absorbs every change Apple makes to the process. An article praising the convenience owes its readers that caveatRATING — Answering the age rating questionnaire became mandatory in September. Apps built without code are no exceptionSILICON — The A20 Pro in the iPhone 18 Pro is reported to be the first high-volume smartphone processor built on TSMC's 2nm nodeIOS27 — iOS 27 and iPadOS 27 land on September 14. For apps assembled with no-code or AI tooling, the week a new OS ships is when the ground moves mostDUO — Apple's first foldable, the iPhone Duo, starts at $1,999. A new screen shape is also the first place a generated layout tends to breakSPLIT — Standard Rork produces React Native through Expo. Rork Max generates native Swift. Neither will adapt to a folding screen at the same pace, or fail in the same waySUBMIT — Automated submission absorbs every change Apple makes to the process. An article praising the convenience owes its readers that caveatRATING — Answering the age rating questionnaire became mandatory in September. Apps built without code are no exceptionSILICON — The A20 Pro in the iPhone 18 Pro is reported to be the first high-volume smartphone processor built on TSMC's 2nm node
Articles/Dev Tools
Dev Tools/2026-09-10Intermediate

I Added a Dim Screen Button, and iPhones Stayed Dark After the App Was Closed

In expo-brightness, setBrightnessAsync applies only to the current activity on Android, but changes the device brightness itself on iOS. Here is why the cleanup burden falls on one platform only, and a hook that restores brightness through AppState.

expo-brightnessReact Native237Expo205iOS114Android49

I added a dim screen button to an ambient sound app that people mostly open at bedtime. I tested it on the Android phone sitting on my desk, watched the screen darken, pressed home, watched it come back, and went to sleep pleased with myself.

A few days later someone on iPhone wrote in. The app was closed, they said, and the screen was still dark. They had opened Settings and pushed the slider back up by hand.

I had written exactly one line. That one line was touching two different things on the two platforms.

The same setBrightnessAsync reaches different places

The expo-brightness documentation states this plainly. I was the one who read past it.

QuestioniOSAndroid
What setBrightnessAsync changesThe screen brightness itselfAn override for the current activity
When you leave the appIt staysIt reverts to the system value
What brings it backLocking the deviceLeaving the foreground
Permission neededNoneNone, for app-level brightness

On Android, the value you set overrides the system brightness only while your activity is in the foreground. Press home and it comes back on its own. My reassuring desk test had simply been riding on that automatic cleanup.

iOS has no per-app brightness layer. The value you write becomes the device brightness, and it stays until the device locks. Quitting the app does not undo it.

So the cleanup is only needed on one side — and that side was the one I had not written.

If the app dimmed the screen, the app gives the brightness back. That sentence is the one I now recall before adding anything that touches a system default.

Read the original value once, before you dim

To restore something you need somewhere to restore it to. getBrightnessAsync() gives you that, but there is a trap sitting right next to it.

If you read again after dimming, you get back the value you set. If your restore variable is overwritten on every call, the second call saves the dim value as your backup, and restoring quietly does nothing.

Capture it once and only once.

import { useRef } from 'react';
import * as Brightness from 'expo-brightness';
 
const originalRef = useRef<number | null>(null);
 
async function rememberOnce() {
  // Already captured: do not re-read, or we grab the value we just set
  if (originalRef.current !== null) return;
  originalRef.current = await Brightness.getBrightnessAsync();
}

It is worth calling isAvailableAsync() alongside it so you can hide the control entirely where the API is unavailable. A button that visibly does nothing is the least kind outcome.

Restore on app state, not on unmount

At first I put the restore in a useEffect cleanup and left it there. Closing the screen restores the brightness, I thought, and that is enough. It was not.

People leave an app in more ways than closing a screen. They press home, they swipe it away from the app switcher, they tap a notification and land somewhere else. In those paths your cleanup may never run.

So I tied dimming and restoring to AppState instead.

import { useCallback, useEffect, useRef } from 'react';
import { AppState } from 'react-native';
import * as Brightness from 'expo-brightness';
 
export function useDimScreen(target: number) {
  const originalRef = useRef<number | null>(null);
  const dimmingRef = useRef(false);
 
  const applyDim = useCallback(async () => {
    if (originalRef.current === null) {
      originalRef.current = await Brightness.getBrightnessAsync();
    }
    await Brightness.setBrightnessAsync(target);
  }, [target]);
 
  const restore = useCallback(async () => {
    const original = originalRef.current;
    if (original === null) return;
    await Brightness.setBrightnessAsync(original);
  }, []);
 
  const start = useCallback(async () => {
    if (!(await Brightness.isAvailableAsync())) return;
    dimmingRef.current = true;
    await applyDim();
  }, [applyDim]);
 
  const stop = useCallback(async () => {
    dimmingRef.current = false;
    await restore();
    originalRef.current = null;
  }, [restore]);
 
  useEffect(() => {
    const sub = AppState.addEventListener('change', next => {
      if (!dimmingRef.current) return;
      if (next === 'active') {
        applyDim();
      } else {
        // Give it back on background and inactive. iOS will not do it for you
        restore();
      }
    });
    return () => {
      sub.remove();
      if (dimmingRef.current) restore();
    };
  }, [applyDim, restore]);
 
  return { start, stop };
}

I catch inactive as well, because pulling down Notification Center or opening Control Center on iOS can stop at inactive without ever reaching background. Miss that and you leave a half-dimmed screen behind.

On Android the restore is close to a no-op, and that is fine. Keeping one path is easier to reread months later than maintaining two branches that must stay in sync.

Why I left the Android system functions alone

expo-brightness also ships a group of functions with System in the name, and this is where I hesitated longest.

FunctionPlatformNote
getSystemBrightnessAsyncAndroid onlyReads the device-wide brightness
setSystemBrightnessAsyncAndroid only, experimentalNeeds a permission and switches the mode to MANUAL
restoreSystemBrightnessAsyncAndroid onlyDrops the activity-level override
setSystemBrightnessModeAsyncAndroid onlyTurns automatic adjustment on or off
addBrightnessListeneriOS onlyNever fires on Android or web

I wanted Android to dim as deeply as iOS did, and I nearly reached for the system side to get there. But those calls require WRITE_SETTINGS, and setSystemBrightnessAsync rewrites the device brightness and switches the phone to manual mode. An ambient sound app would be silently turning off someone's automatic brightness.

Adding a permission also means explaining that permission in the store listing. For something that only needs to look dim inside my own app, that price was not worth paying.

What I settled on is to keep the effect inside the time the app is in front. Restore it myself on iOS, let the OS restore it on Android. Following each platform's own habit leaves less mess on someone's phone.

Dimming and keeping the screen awake belong together

People dim a screen because they want to keep looking at it while falling asleep. Dimming alone does not stop the OS from turning the display off a little later, which is where the "it disappeared after thirty seconds" reports come from.

So I disable the auto-lock while dimming and release it once the fade has finished. Holding the screen awake will drain a battery, so I decide the release condition before I add the feature, not after.

There is one more piece on iOS: addBrightnessListener. If someone raises the brightness themselves from Control Center, the value you captured is already stale. Re-capturing when the listener fires keeps the restore from feeling wrong. Audio has the same shape of problem, which I wrote up in Three Minutes After the Screen Locked, My expo-audio Ambient Sound Went Silent. Anything that runs after the screen goes dark needs its "after you leave" behavior decided first.

What you can check tonight

With two devices this takes about ten minutes. On the iPhone, tap dim, press home, quit the app, then open Settings and look at the brightness slider. If it is still down, your restore never ran. On Android, the brightness should come back the moment you reach the home screen.

iOS 27 and iPadOS 27 ship on September 14. Anything that writes directly to an OS default, brightness included, is worth exercising once before the update lands; I described how I do that in Put Your Live Rork App on a Spare iPhone Before iOS 27 Ships.

Start with one thing: leave the screen you are currently dimming, and watch what happens. That is where I found mine.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-04-02
Adding Native Share Functionality to Your Rork App — A Complete Share Sheet Guide
Learn how to implement native Share Sheet functionality in your Rork app. From sharing text and URLs to images and deep links, this beginner-friendly guide walks you through real code examples for iOS and Android.
Dev Tools2026-08-31
Three Minutes After the Screen Locked, My expo-audio Ambient Sound Went Silent
Why expo-audio stops playing when the screen locks, diagnosed as three separate layers: build config, audio session, and lock screen integration. The three-minute stop on Android is documented behavior.
Dev Tools2026-07-27
When to Raise Your Minimum iOS Version — Count Leftover Branches, Not User Percentages
Judging a minimum OS bump by usage share produces the same answer every year, so the decision never happens. Here is the annotation convention, the sweep script that counts how many iOS and Android branches each candidate floor would retire, and what to watch for 30 days after.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links