alpaca0984.log

Change WebView's theme through JavaScript

alpaca0984

I added a WebView in an Android app and found it doesn't interoperate with the prefers-color-scheme CSS media feature as well as it's explained in the official doc:

WebView's behavior interoperates with the prefers-color-scheme and color-scheme web standards. Whenever possible, if you author the web content that you want your app to display in WebView, you should define a dark theme for your website and implement prefers-color-scheme so that WebView can match the web content's theme to your app's theme.

It works with Android 13 but not with lower versions like 12, 11, and 10 (is it only for me?).

For demonstration, I created a very simple HTML file. With the prefers-color-scheme, it changes the color scheme for <body>, <div> and <p> tags.

<!DOCTYPE html>
<html>
 
<head>
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    :root {
      color-scheme: light dark;
      --color-background: silver;
      --color-surface: white;
      --color-on-surface: black;
    }
 
    @media (prefers-color-scheme: dark) {
      :root {
        --color-background: gray;
        --color-surface: black;
        --color-on-surface: white;
      }
    }
 
    body {
      background-color: var(--color-background);
    }
 
    .container {
      background-color: var(--color-surface);
    }
 
    p {
      color: var(--color-on-surface);
    }
  </style>
</head>
 
<body>
  <div class="container mx-auto p-4">
    <p class="text-2xl">Lorem Ipsum</p>
 
    <p class="text-lg mt-2">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
      Duis quis tempus elit, vel consequat ante. Proin placerat interdum urna,
      eget condimentum ipsum laoreet non.
    </p>
  </div>
</body>
 
</html>

It works on a browser. When you open the developer console, go to Rendering and emulate the prefers-color-scheme, the colors are changed as expected.

Next, let's load it inside WebView. Place the above HTML file to app/src/main/assets/web_content.html. With JetpackCompose, we can inflate a WebView with the AndroidView composable function. Here, I used the Android 10 emulator.

AndroidView(
    factory = { context ->
        WebView(context).apply {
            settings.javaScriptEnabled = true
            loadUrl("file:///android_asset/web_content.html")
        }
    }
)

When I go to the system settings, enable dark them, and come back to the app, the color doesn't change at all although I used a DayNight theme. Also, algorithmic darkening doesn't work for real devices whereas it works for emulators.

I spent quite some time to figure it out but after all, I couldn't. I ultimately decided to apply a workaround that is hacky but not so dirty.


In the HTML's <style> tag, copy the code of (prefers-color-scheme: dark) to :root.dark-mode selector to make the contents dark when the root element has a dark-mode class.

:root.dark-mode {
  --color-background: gray;
  --color-surface: black;
  --color-on-surface: white;
}

From the Android side, when the page is loaded, check if the system is in the dark theme or not (I got the snippet from Jetpack Compose's _isSystemInDarkTheme() function). And if it's in the dark theme, add a "dark-mode" class to the root element. Otherwise, remove the "dark-mode" class from it.

AndroidView(
    factory = { context ->
        WebView(context).apply {
            settings.javaScriptEnabled = true
            webViewClient = object : WebViewClient() {
                override fun onPageFinished(view: WebView, url: String) {
                    super.onPageFinished(view, url)
 
                    val isSystemInDarkTheme =
                        (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
                                Configuration.UI_MODE_NIGHT_YES
                    val js = """
                        const classList = document.querySelector(":root").classList;
                        $isSystemInDarkTheme ? classList.add("dark-mode") : classList.remove("dark-mode");
                    """.trimIndent()
                    view.evaluateJavascript(js, null)
                }
            }
            loadUrl("file:///android_asset/web_content.html")
        }
    }
)

I confirmed that it works as intended.


It's still a mystery why I see the issue though. I use Android's DayNight theme and the latest version of webkit (1.6.0). If you know about darkening web content from Android, please let me know.