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
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
AppleInterfaceStylesystem preference and registers an observer throughNSDistributedNotificationCenter. - Windows: reads the registry value
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightThemeand watches it withRegNotifyChangeKeyValueon a native background thread. - Linux: reads the XDG Desktop Portal
color-schemepreference over theorg.freedesktop.portal.SettingsD-Bus interface; a value of1means 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 = NoopDarkModeDetectorWhat'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.