diff --git a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
index d232df24b3aec05b7b6c8ab85bb0ef96b6631017..b92286f58558e975a63d312bc67621fabf0faa44 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
@@ -52,6 +52,8 @@ import org.briarproject.bramble.util.OsUtils.isMac
 import org.briarproject.briar.attachment.AttachmentModule
 import org.briarproject.briar.desktop.attachment.media.ImageCompressor
 import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl
+import org.briarproject.briar.desktop.settings.Settings
+import org.briarproject.briar.desktop.settings.SettingsImpl
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
 import org.briarproject.briar.desktop.threading.UiExecutor
@@ -100,6 +102,10 @@ internal class DesktopModule(
         return DesktopDatabaseConfig(dbDir, keyDir)
     }
 
+    @Provides
+    @Singleton
+    fun provideSettings(settings: SettingsImpl): Settings = settings
+
     @Provides
     @Singleton
     @EventExecutor
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
index 62c9ec49a3a463adefd6a420a27277ef51a79925..99e4c6ccbdce531df7b3e63a9e29ae1482026f17 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
@@ -229,7 +229,7 @@ fun ConversationList(
         }
     }
 
-    onMessageAddedToBottom.react { type ->
+    onMessageAddedToBottom.reactInCoroutine { type ->
         // scroll to bottom for new *outgoing* message or if scroll position was at last message before
         if (type == ConversationViewModel.MessageAddedType.OUTGOING || scrollState.isScrolledToPenultimate()) {
             scope.launch {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt
new file mode 100644
index 0000000000000000000000000000000000000000..59fa618dda1bd33037100d2ce8c8d9103e954307
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/Settings.kt
@@ -0,0 +1,30 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.settings
+
+import org.briarproject.briar.desktop.viewmodel.SingleStateEvent
+
+interface Settings {
+
+    enum class Theme { AUTO, LIGHT, DARK }
+
+    var theme: Theme
+
+    val invalidateScreen: SingleStateEvent<Unit>
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d70e0f4df32d61d44696094859d9d5dcb4d085cd
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsImpl.kt
@@ -0,0 +1,43 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.settings
+
+import org.briarproject.briar.desktop.settings.Settings.Theme
+import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO
+import org.briarproject.briar.desktop.viewmodel.SingleStateEvent
+import java.util.prefs.Preferences
+import javax.inject.Inject
+
+const val PREF_THEME = "theme"
+
+class SettingsImpl @Inject internal constructor() : Settings {
+
+    // used for unencrypted settings, namely theme and language
+    private val prefs = Preferences.userNodeForPackage(this::class.java)
+
+    override val invalidateScreen = SingleStateEvent<Unit>()
+
+    override var theme: Theme
+        get() = Theme.valueOf(prefs.get(PREF_THEME, AUTO.name))
+        set(value) {
+            prefs.put(PREF_THEME, value.name)
+            prefs.flush() // write preferences to disk
+            invalidateScreen.emit(Unit)
+        }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt
index 7d35ba60cb751f0d3ff0d5e9482685bcec99c970..3c3aec2b22b10f5bb049fb2c76a815b279bc25f7 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt
@@ -19,6 +19,8 @@
 package org.briarproject.briar.desktop.settings
 
 import androidx.compose.runtime.mutableStateOf
+import org.briarproject.briar.desktop.settings.Settings.Theme.DARK
+import org.briarproject.briar.desktop.settings.Settings.Theme.LIGHT
 import org.briarproject.briar.desktop.viewmodel.ViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
 import javax.inject.Inject
@@ -34,18 +36,21 @@ enum class SettingCategory {
 
 class SettingsViewModel
 @Inject
-constructor() : ViewModel {
+constructor(
+    private val settings: Settings,
+) : ViewModel {
     private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY)
     val selectedSetting = _selectedSetting.asState()
 
-    private val _isDarkMode = mutableStateOf(true)
+    private val _isDarkMode = mutableStateOf(settings.theme == DARK)
     val isDarkMode = _isDarkMode.asState()
 
     fun selectSetting(selectedOption: SettingCategory) {
         _selectedSetting.value = selectedOption
     }
 
-    fun toggleTheme() {
-        _isDarkMode.value = !isDarkMode.value
+    fun toggleTheme() { // todo: set theme instead
+        settings.theme = if (settings.theme == DARK) LIGHT else DARK
+        _isDarkMode.value = settings.theme == DARK
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 06cc0ba857e72aca9dcc0ad8c106ef3269912b88..659f8ed26344d2ae0a6c98644b79300517176ade 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -18,6 +18,7 @@
 
 package org.briarproject.briar.desktop.ui
 
+import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
@@ -45,6 +46,9 @@ import org.briarproject.briar.desktop.DesktopFeatureFlags
 import org.briarproject.briar.desktop.expiration.ExpirationBanner
 import org.briarproject.briar.desktop.login.ErrorScreen
 import org.briarproject.briar.desktop.login.StartupScreen
+import org.briarproject.briar.desktop.settings.Settings
+import org.briarproject.briar.desktop.settings.Settings.Theme.AUTO
+import org.briarproject.briar.desktop.settings.Settings.Theme.DARK
 import org.briarproject.briar.desktop.settings.SettingsViewModel
 import org.briarproject.briar.desktop.theme.BriarTheme
 import org.briarproject.briar.desktop.ui.Screen.EXPIRED
@@ -85,6 +89,7 @@ constructor(
     private val lifecycleManager: LifecycleManager,
     private val eventBus: EventBus,
     private val viewModelProvider: ViewModelProvider,
+    private val settings: Settings,
     private val featureFlags: FeatureFlags,
     private val desktopFeatureFlags: DesktopFeatureFlags,
 ) : BriarUi, EventListener {
@@ -110,6 +115,9 @@ constructor(
     @OptIn(ExperimentalComposeUiApi::class)
     @Composable
     override fun start(onClose: () -> Unit) {
+        // invalidate whole application window in case the theme or language setting is changed
+        settings.invalidateScreen.react {}
+
         val title = i18n("main.title")
         val platformLocalization = object : PlatformLocalization {
             override val copy = i18n("copy")
@@ -133,7 +141,9 @@ constructor(
             ) {
                 var showAbout by remember { mutableStateOf(false) }
                 val settingsViewModel: SettingsViewModel = viewModel()
-                BriarTheme(isDarkTheme = settingsViewModel.isDarkMode.value) {
+                val isDarkTheme = settings.theme == DARK ||
+                    (settings.theme == AUTO && isSystemInDarkTheme())
+                BriarTheme(isDarkTheme) {
                     Column(Modifier.fillMaxSize()) {
                         ExpirationBanner { screenState = EXPIRED; stop() }
                         when (screenState) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt
index 273dae37e6dd0d12b7f175f322933a26ca47e591..3a1cd50620aed8bc904a0703bbc9315f2061ba25 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/SingleStateEvent.kt
@@ -44,18 +44,32 @@ class SingleStateEvent<T : Any> {
         state.value = value
     }
 
+    fun reactAndReset(block: (T) -> Unit) {
+        val value = state.value
+        if (value != null) {
+            state.value = null
+            block(value)
+        }
+    }
+
+    /**
+     * React to every new value of type [T] emitted through this event,
+     * by directly reading the state in the calling function.
+     * This can be used to invalidate a composable function.
+     * Make sure to not react to the same event on multiple places.
+     */
+    @Composable
+    inline fun react(noinline block: (T) -> Unit) = reactAndReset(block)
+
     /**
-     * React to every new value of type [T] emitted through this event.
+     * React to every new value of type [T] emitted through this event
+     * inside a LaunchedEffect.
      * Make sure to not react to the same event on multiple places.
      */
     @Composable
-    fun react(block: (T) -> Unit) {
+    fun reactInCoroutine(block: (T) -> Unit) {
         LaunchedEffect(state.value) {
-            val value = state.value
-            if (value != null) {
-                block(value)
-                state.value = null
-            }
+            reactAndReset(block)
         }
     }
 }
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
index 99327752b145f42d799871f3a4e7e169ea94d8ed..4ff59a88ccdddddb3dc3ad82cbb905ff9781547f 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
@@ -52,6 +52,8 @@ import org.briarproject.briar.api.test.TestAvatarCreator
 import org.briarproject.briar.attachment.AttachmentModule
 import org.briarproject.briar.desktop.attachment.media.ImageCompressor
 import org.briarproject.briar.desktop.attachment.media.ImageCompressorImpl
+import org.briarproject.briar.desktop.settings.Settings
+import org.briarproject.briar.desktop.settings.SettingsImpl
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreator
 import org.briarproject.briar.desktop.testdata.DeterministicTestDataCreatorImpl
 import org.briarproject.briar.desktop.testdata.TestAvatarCreatorImpl
@@ -105,6 +107,10 @@ internal class DesktopTestModule(
         return DesktopDatabaseConfig(dbDir, keyDir)
     }
 
+    @Provides
+    @Singleton
+    fun provideSettings(settings: SettingsImpl): Settings = settings
+
     @Provides
     @Singleton
     @EventExecutor