Updated

Transparent Android Status Bar in Expo

Update

An updated method of achieving edge-to-edge is now possible with zoontek/react-native-edge-to-edge for a much cleaner solution.

Introduction

So you've got your app to be edge-to-edge but still have that ugly status bar background scrim that refuses to go away? Here's the actual solution.

A Broken Solution

If you've looked this issue up in the past then you've likely stumbled upon answers such as setting FLAG_LAYOUT_NO_LIMITS, but this breaks Samsung One UI's virtual navbars as they have no block height for safe area view's to reference.

// In Activity's onCreate() for instance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    Window w = getWindow();
    w.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
A continue button being overlapped with the Android virtual navigation buttons

The Solution

  • Tell your app to draw behind the system bars within the Activity onCreate: WindowCompat.setDecorFitsSystemWindows(getWindow(), false)
  • Set the system bar colours to be transparent <item name=”android:statusBarColor”>@android:color/transparent</item>
  • Set androidStatusBar.translucent to false in app.config.js/app.config.js Translucent does not include transparent in Android

Obviously this isn't very Expo friendly, we're editing native Android files here, so let's turn it into a config plugin.

import { ConfigPlugin, withMainActivity, withAndroidStyles, AndroidConfig } from '@expo/config-plugins'
import { addImports } from '@expo/config-plugins/build/android/codeMod'
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'
import { ExpoConfig } from '@expo/config-types'

const TRANSPARENT_WINDOW_ID = 'transparent-window-mainActivity-onCreate'

export const withAndroidTransparentWindow: ConfigPlugin = (config) => {
    config = withAndroidStyles(config, (config) => {
        config.modResults = AndroidConfig.Styles.assignStylesValue(config.modResults, {
            add: true,
            parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(),
            name: 'android:statusBarColor',
            value: '@android:color/transparent',
        })

        return config
    })

    return withMainActivity(config, (config) => {
        const isJava = config.modResults.language === 'java'
        const LE = isJava ? ';' : ''

        config.modResults.contents = addImports(
            config.modResults.contents,
            ['androidx.core.view.WindowCompat', 'android.os.Build'],
            isJava,
        )

        config.modResults.contents = mergeContents({
            src: config.modResults.contents,
            anchor: /(?<=^.*super\.onCreate.*$)/m,
            offset: 1,
            comment: '//',
            tag: TRANSPARENT_WINDOW_ID,
            newSrc: `
                if (Build.VERSION.SDK_INT > 28) {
                    WindowCompat.setDecorFitsSystemWindows(getWindow(), false)${LE}
                }
            `,
        }).contents

        return config
    })
}

export default withAndroidTransparentWindow

This will modify our android folder when running expo prebuild so that we're not in a bare workflow; this config plugin has been written in TypeScript and will therefore need compiling before referencing in the app config.