Nucleus
OS integration

Dark mode detector

Detect the operating system's dark mode from Kotlin and recompose your Compose UI when the theme changes.

darkmode-detector reports whether the operating system is in dark mode and notifies you when that changes. It provides an isSystemInDarkMode() composable that recomposes on theme changes, plus an imperative IDarkModeDetector for use outside Compose. macOS, Windows, and Linux are supported.

Add the dependency

build.gradle.kts
dependencies {
    implementation("dev.nucleusframework:nucleus.darkmode-detector:2.0.7")
}

Detect dark mode in Compose

Call isSystemInDarkMode() and use the result to select your color scheme. The composable recomposes when the system theme changes:

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import dev.nucleusframework.darkmodedetector.isSystemInDarkMode

@Composable
fun App() {
    val isDark = isSystemInDarkMode()
    MaterialTheme(
        colorScheme = if (isDark) darkColorScheme() else lightColorScheme(),
    ) {
        // Content recomposes when the OS theme flips.
    }
}

Compose's own isSystemInDarkTheme() reads the OS theme once and does not update when the user switches themes at runtime. isSystemInDarkMode() returns the current value and registers a listener that triggers recomposition on every change.

Inside a @Preview (when LocalInspectionMode is true), isSystemInDarkMode() delegates to Compose's isSystemInDarkTheme() so previews render without a running detector.

How it works

Each platform reads the system theme through a native JNI bridge and watches the OS for changes. No polling is involved; the detector reacts to change notifications from the OS itself.

  • macOS: reads the AppleInterfaceStyle system preference and registers an observer through NSDistributedNotificationCenter.
  • Windows: reads the registry value HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme and watches it with RegNotifyChangeKeyValue on a native background thread.
  • Linux: reads the XDG Desktop Portal color-scheme preference over the org.freedesktop.portal.Settings D-Bus interface; a value of 1 means prefer-dark.

On unsupported platforms, getPlatformDarkModeDetector() returns NoopDarkModeDetector, which always reports light and ignores listeners.

API reference

Read the current theme

getPlatformDarkModeDetector() returns the IDarkModeDetector for the current platform. Call isDark() for a one-shot read:

import dev.nucleusframework.darkmodedetector.getPlatformDarkModeDetector

val detector = getPlatformDarkModeDetector()
val dark: Boolean = detector.isDark()

Listen for changes

Register a java.util.function.Consumer<Boolean> to be notified when the theme changes, and remove it when you are done:

import dev.nucleusframework.darkmodedetector.getPlatformDarkModeDetector
import java.util.function.Consumer

val detector = getPlatformDarkModeDetector()

val listener = Consumer<Boolean> { isDark ->
    println("dark mode: $isDark")
}
detector.registerListener(listener)

// Later, stop receiving updates.
detector.removeListener(listener)

The full interface:

interface IDarkModeDetector {
    fun isDark(): Boolean
    fun registerListener(listener: Consumer<Boolean>)
    fun removeListener(listener: Consumer<Boolean>)
}

Use a no-op detector

NoopDarkModeDetector implements IDarkModeDetector without touching the OS. It always returns false from isDark() and ignores listeners — useful in tests or headless environments:

import dev.nucleusframework.darkmodedetector.IDarkModeDetector
import dev.nucleusframework.darkmodedetector.NoopDarkModeDetector

val detector: IDarkModeDetector = NoopDarkModeDetector

What's next

  • System colors — read the system accent color and high-contrast state.
  • System info — query the OS, CPU, memory, and battery from Kotlin.
  • OS integration — the rest of the desktop OS APIs.