diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
index 0d3dfcfd7c1911b6442f13c51e486c688809a561..91bf6153a9ae5dd0c166e3ef2eb1515f5f555409 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.material.IconButton
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
@@ -38,6 +39,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.contact.ContactId
@@ -49,8 +55,11 @@ import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.HorizontalDivider
 import org.briarproject.briar.desktop.ui.MessageCounter
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
+import org.briarproject.briar.desktop.utils.appendCommaSeparated
 import java.time.Instant
 
 @Suppress("HardCodedStringLiteral")
@@ -63,7 +72,7 @@ fun main() = preview(
     "timestamp" to Instant.now().toEpochMilli(),
     "selected" to false,
 ) {
-    Column {
+    Column(Modifier.selectableGroup()) {
         ContactCard(
             ContactItem(
                 idWrapper = RealContactIdWrapper(ContactId(0)),
@@ -103,9 +112,14 @@ fun ContactCard(
         modifier = Modifier
             .fillMaxWidth()
             .defaultMinSize(minHeight = HEADER_SIZE)
-            .selectable(selected, onClick = onSel)
-            .background(bgColor)
-            .padding(vertical = 8.dp),
+            .semantics {
+                contentDescription = if (selected) i18n("access.list.selected.yes")
+                else i18n("access.list.selected.no")
+                // todo: stateDescription apparently not used
+                // stateDescription = if (selected) "selected" else "not selected"
+            }
+            .selectable(selected, onClick = onSel, role = Role.Button)
+            .background(bgColor),
         verticalArrangement = Arrangement.Center
     ) {
         when (contactItem) {
@@ -124,11 +138,33 @@ fun ContactCard(
 private fun RealContactRow(contactItem: ContactItem) {
     Row(
         horizontalArrangement = Arrangement.SpaceBetween,
-        modifier = Modifier.fillMaxWidth(),
-        verticalAlignment = Alignment.CenterVertically
+        verticalAlignment = Alignment.CenterVertically,
+        modifier = Modifier
+            .fillMaxWidth()
+            .semantics {
+                text = buildAnnotatedString {
+                    append(i18nF("access.contact.with_name", contactItem.displayName))
+                    appendCommaSeparated(
+                        if (contactItem.isConnected) i18n("access.contact.connected.yes")
+                        else i18n("access.contact.connected.no")
+                    )
+                    if (contactItem.unread > 0)
+                        appendCommaSeparated(i18nP("access.contact.unread_count", contactItem.unread))
+                    if (contactItem.isEmpty)
+                        appendCommaSeparated(i18n("contacts.card.nothing"))
+                    else
+                        appendCommaSeparated(
+                            i18nF(
+                                "access.contact.last_message_timestamp",
+                                getFormattedTimestamp(contactItem.timestamp)
+                            )
+                        )
+                    append('.')
+                }
+            }
     ) {
         Row(
-            modifier = Modifier.padding(start = 16.dp, end = 8.dp)
+            modifier = Modifier.padding(start = 16.dp, end = 8.dp).padding(vertical = 8.dp)
         ) {
             Box {
                 ProfileCircle(36.dp, contactItem)
@@ -152,11 +188,24 @@ private fun RealContactRow(contactItem: ContactItem) {
 private fun PendingContactRow(contactItem: PendingContactItem, onRemove: () -> Unit) {
     Row(
         horizontalArrangement = Arrangement.SpaceBetween,
-        modifier = Modifier.fillMaxWidth(),
+        modifier = Modifier
+            .fillMaxWidth()
+            .semantics {
+                text = buildAnnotatedString {
+                    append(i18nF("access.contact.pending.with_name", contactItem.displayName))
+                    // todo: include pending status
+                    appendCommaSeparated(
+                        i18nF(
+                            "access.contact.pending.added_timestamp",
+                            getFormattedTimestamp(contactItem.timestamp)
+                        )
+                    )
+                }
+            },
         verticalAlignment = Alignment.CenterVertically
     ) {
         Row(
-            modifier = Modifier.padding(start = 16.dp, end = 8.dp)
+            modifier = Modifier.padding(start = 16.dp, end = 8.dp).padding(vertical = 8.dp)
         ) {
             ProfileCircle(36.dp)
             PendingContactInfo(
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
index 8aff4a2d84bae10b7d1c745cfd90eea7a2b8aa1f..f7f6bff50d06c0190407b3872c65f193bbf2a56d 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -21,7 +21,6 @@ package org.briarproject.briar.desktop.contact
 import androidx.compose.foundation.VerticalScrollbar
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -37,10 +36,13 @@ import androidx.compose.material.Surface
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.theme.surfaceVariant
 import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 
 @Composable
 fun ContactList(
@@ -70,7 +72,14 @@ fun ContactList(
             }
 
             Box(modifier = Modifier.fillMaxSize()) {
-                LazyColumn(state = scrollState, modifier = Modifier.selectableGroup()) {
+                LazyColumn(
+                    state = scrollState,
+                    modifier = Modifier
+                        .semantics {
+                            contentDescription = i18n("access.contact.list")
+                        }
+                        .selectableGroup()
+                ) {
                     items(contactList) { contactItem ->
                         ContactCard(
                             contactItem,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
index 49e33ff1fbac87d5a974408ca156e7c032143f36..de17bd810dce5f79345f5b1ee9c5795eb82fe79b 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
@@ -20,7 +20,6 @@ package org.briarproject.briar.desktop.introduction
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AnnotatedStringUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AnnotatedStringUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e1496c4ee7df07eb646286fa0a0b68b92df9e2e7
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AnnotatedStringUtils.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.utils
+
+import androidx.compose.ui.text.AnnotatedString
+
+fun AnnotatedString.Builder.appendLeading(leading: String, text: String? = null) {
+    append(leading)
+    if (text != null) append(text)
+}
+
+fun AnnotatedString.Builder.appendCommaSeparated(text: String? = null) =
+    appendLeading(", ", text)
diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
index 0586f6f815ec43e8f8be53da492969986a6ae05b..c390a330870c18ad1b59e8a3204a9af1d4e3bca3 100644
--- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties
+++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
@@ -2,6 +2,14 @@
 access.attachment_add=Add attachment
 access.attachment_remove=Remove attachment
 access.contacts.add=Add contact
+access.contact.list=contact list
+access.contact.with_name=Contact "{0}"
+access.contact.connected.yes=connected
+access.contact.connected.no=not connected
+access.contact.unread_count={0, plural, one {one unread message} other {{0} unread messages}}
+access.contact.last_message_timestamp=last message: {0}
+access.contact.pending.with_name=Pending contact "{0}"
+access.contact.pending.added_timestamp=added: {0}
 access.contact.menu=Show contact menu
 access.contacts.dropdown.connections.expand=Expand connection menu
 access.contacts.dropdown.contacts.expand=Expand contact menu
@@ -12,6 +20,8 @@ access.introduction.close=Close introduction screen
 access.message.jump_to_unread=Jump to next unread message
 access.message.send=Send message
 access.message.sent=Message sent
+access.list.selected.yes=currently selected
+access.list.selected.no=currently not selected, click to select
 access.logo=Briar logo
 access.swap=Icon showing errors between two contacts
 access.mode.contacts=Contacts
@@ -34,7 +44,7 @@ contacts.none_selected.title=No contact selected
 contacts.none_selected.hint=Select a contact to start chatting
 contacts.pending_selected.title=Pending contact selected
 contacts.pending_selected.hint=You need to wait until the process of adding each other as contacts has finished before you can start chatting.
-contacts.card.nothing=No messages.
+contacts.card.nothing=No messages
 contacts.dropdown.connections=Connections
 contacts.dropdown.connections.title=Connections
 contacts.dropdown.connections.bluetooth=Connect via Bluetooth