RORK LABJP
DEADLINE — From August 31, every new app and update on Google Play must target Android 16 (API level 36). Seven days to goEXTENSION — If you qualify, you can request an extension through November 1 using a form in Play Console, but the request itself has to be filed before the deadlineMAX — Rork Max generates native Swift rather than React Native and compiles on a cloud Mac fleet, covering iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageCOMPANION — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer account, so design, build, and test can all happen in a browserDEVTOOLS — React Native DevTools in Expo SDK 57 can emulate light and dark mode, letting you check both appearances without touching device settingsIOS27 — iOS 27 ships next month. Beta 6 landed on August 17, and RCS Universal Profile 3.0 support means you can finally reply to a specific message received from AndroidDEADLINE — From August 31, every new app and update on Google Play must target Android 16 (API level 36). Seven days to goEXTENSION — If you qualify, you can request an extension through November 1 using a form in Play Console, but the request itself has to be filed before the deadlineMAX — Rork Max generates native Swift rather than React Native and compiles on a cloud Mac fleet, covering iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageCOMPANION — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer account, so design, build, and test can all happen in a browserDEVTOOLS — React Native DevTools in Expo SDK 57 can emulate light and dark mode, letting you check both appearances without touching device settingsIOS27 — iOS 27 ships next month. Beta 6 landed on August 17, and RCS Universal Profile 3.0 support means you can finally reply to a specific message received from Android
Articles/Dev Tools
Dev Tools/2026-08-24Intermediate

I Moved My Dark Mode Checks Into DevTools, and Only Four Places Still Need a Real Device

Expo SDK 57 added light and dark emulation to React Native DevTools, which ended my trips to the Settings app. What it swaps is the value JavaScript reports, so a few places still need a real toggle. Here is how to find that boundary in your own project.

Expo182SDK 57Dark ModeReact Native231DevTools

I used to lose half an hour every morning walking back and forth to the Settings app.

Darken the list screen, check it, switch back to light. Darken the detail screen, check it, switch back again. In a wallpaper app where most of the screen is filled by an image, very few surfaces actually change color — but you still have to look at every screen to be sure. Ten seconds per toggle across twenty screens is close to seven minutes of pure walking around.

After upgrading to Expo SDK 57, I noticed that React Native DevTools now has a switch that emulates light and dark mode. One toggle, no device settings.

My first thought was that this solved the whole problem. It did not. But drawing a clear line around what it does not cover made my pre-release routine much shorter, and that line is what I want to write down here.

What the toggle swaps is the value JavaScript reports

Understanding the mechanism makes every later decision faster.

React Native ships an Appearance module, and Appearance.setColorScheme() overrides the color scheme that the JavaScript side reports. The DevTools emulation makes sense as a way to drive that override from the tooling instead of from your own code.

So anything that decides its colors by reading useColorScheme() follows the toggle immediately.

import { useColorScheme, View, Text } from 'react-native';
 
export function Card({ title }: { title: string }) {
  const scheme = useColorScheme(); // 'light' | 'dark' | null
  const isDark = scheme === 'dark';
 
  return (
    <View style={{ backgroundColor: isDark ? '#16181d' : '#ffffff' }}>
      <Text style={{ color: isDark ? '#e8eaed' : '#1f2328' }}>{title}</Text>
    </View>
  );
}

NativeWind's dark: modifier behaves the same way, since it reads from the same source underneath.

The flip side is the part worth remembering: anything that reads the OS setting natively does not follow along. JavaScript can report "we are dark now," and UIKit or the Android resource resolver will happily keep using whatever the device is actually set to.

That is the boundary. The reason it confused me at first is that both kinds of surface tend to sit on the same screen.

Put the sources side by side and confirm the boundary yourself

Rather than trusting a description of the mechanism, it is faster to confirm it inside your own project and your own version. I built a small diagnostic screen that displays three color scheme sources at once.

import { useEffect, useState } from 'react';
import { Appearance, useColorScheme, View, Text, Platform } from 'react-native';
import { WebView } from 'react-native-webview';
 
const PROBE_HTML = `
<html><body style="margin:0;font:16px -apple-system,system-ui">
  <div id="out" style="padding:12px"></div>
  <script>
    const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    document.getElementById('out').textContent = 'WebView: ' + (dark ? 'dark' : 'light');
    document.body.style.background = dark ? '#16181d' : '#ffffff';
    document.body.style.color = dark ? '#e8eaed' : '#1f2328';
  </script>
</body></html>`;
 
export function ColorSchemeProbe() {
  const hookScheme = useColorScheme();
  const [snapshot, setSnapshot] = useState(Appearance.getColorScheme());
 
  useEffect(() => {
    const sub = Appearance.addChangeListener(({ colorScheme }) => {
      setSnapshot(colorScheme);
    });
    return () => sub.remove();
  }, []);
 
  return (
    <View style={{ gap: 8, padding: 16 }}>
      <Text>useColorScheme(): {String(hookScheme)}</Text>
      <Text>Appearance.getColorScheme(): {String(snapshot)}</Text>
      <Text>Platform: {Platform.OS}</Text>
      <WebView source={{ html: PROBE_HTML }} style={{ height: 64 }} />
    </View>
  );
}

Keep that screen open and flip the DevTools toggle. If the first two lines change and the WebView line stays put, the boundary is where I expect it to be. If the WebView follows along too, then my assumption is out of date for your version, and you should re-check the four places below on your own terms.

The check takes about a minute, and it is worth re-running on every SDK bump. A single screen in your own project beats secondhand claims about version differences.

The four places the emulation does not reach

With the boundary confirmed, here is what still requires flipping the device setting.

SurfaceHow its colors are decidedHow to check it
Splash screen Drawn by the OS from native resources before your app runs Flip the device setting and cold start
Native alerts and keyboard Follow the UIKit / Android trait Flip the device setting and open that screen
prefers-color-scheme inside a WebView The WebView reads the OS setting itself Flip the device setting, or inject the scheme explicitly
Android system bar backgrounds styles.xml plus your edge-to-edge configuration Flip the device setting and look at a real device

Of those, the WebView is the one I would simply take control of. Leaving it to the OS produces the mismatch where your app is dark and the embedded page is bright white — very visible in apps that show terms of service or help content in a WebView.

const scheme = useColorScheme() ?? 'light';
 
<WebView
  source={{ uri: 'https://example.com/help' }}
  injectedJavaScriptBeforeContentLoaded={`
    document.documentElement.dataset.scheme = ${JSON.stringify(scheme)};
    true;
  `}
/>

If the receiving CSS keys off [data-scheme="dark"] instead of prefers-color-scheme, the whole app resolves colors from a single source. It also starts following the DevTools toggle, which removes one of the four blind spots outright.

The native keyboard is partially fixable. If you pass keyboardAppearance on TextInput from your color source, at least the iOS keyboard follows the JavaScript decision.

const scheme = useColorScheme() ?? 'light';
 
<TextInput
  placeholder="Search"
  keyboardAppearance={scheme === 'dark' ? 'dark' : 'light'}
/>

In an app with a search field, a mismatch here is noticeable every single time someone types. Native dialogs such as Alert.alert() offer no equivalent knob, so those genuinely have to be looked at on a device. Splitting the list into "things I can fix" and "things I can only look at" makes it much easier to decide where the checking time goes.

For the splash screen, not having a light and dark variant at all is a perfectly reasonable choice. A single brand color removes it from the list of things to verify. That is what I do in my wallpaper apps, and I no longer spend time chasing flashes during launch.

Android system bars deserve one look on a real device right after upgrading, since SDK 57 includes edge-to-edge fixes. If you have hand-edited native configuration, it helps to work through Auditing the native changes prebuild will erase before you upgrade to Expo SDK 57 first, so you are not debugging two things at once.

The pre-release routine I rewrote

Once the boundary was clear, the routine collapsed into three steps.

Sweep every screen in DevTools, toggling once

Leave the app running, flip the toggle to dark, and walk the whole app. At this stage you are only looking at surfaces that read the JavaScript color source: text contrast, how shadows read, dividers dissolving into the background. In my case roughly eight out of ten dark mode defects showed up right here.

Flip the device setting and check four places

Then set the device itself to dark and check exactly four things: a cold start, one native alert, one screen containing a WebView, and on Android one screen with visible system bars. Compared with walking every screen twice, this part now takes a couple of minutes.

Watch one re-render immediately after switching

Both the emulation and the real toggle trigger a re-render. If a white frame appears or state gets dropped at that moment, that is a separate problem. If your setup restarts the process on theme change, Fixing the white screen on theme switch without recreate() covers closely related ground.

Shorter checks get run more often

What I have described here is less about a new feature and more about friction going down.

A seven-minute check invites the thought "nothing changed since last time, let me skip it." A one-minute check gets run every time. Since the frequency went up, the number of dark mode defects reaching production has clearly dropped.

As a next step, drop the diagnostic screen into one project. Five minutes will tell you where the boundary sits in your version, and from there you can shorten your own routine by hand.

Accessibility settings share the same "you only really know on a device" quality. I collected what I do for VoiceOver and Dynamic Type in Bringing VoiceOver and Dynamic Type up to production quality in Rork, and handling both at once keeps all the settings-driven checks in one pass.

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-08-23
Predictive back on Android 16: what to fix before August 31, and what can wait
Targeting API 36 turns predictive back on by default and stops onBackPressed from firing. Here is how I split the work that the deadline actually requires from the work that does not, and how I kept the opt-out from disappearing on the next prebuild.
Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
Dev Tools2026-08-17
When an Expo UI drop-in swap actually removes a dependency
Expo UI went stable in SDK 56 with drop-in replacements for eight community packages. Swapping one import does not always shrink your dependency list. Here is how to decide which swaps actually pay off, straight from the dependency graph.
📚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
See all →