Common Missing Content Descriptions in Dating Apps: Causes and Fixes

Content descriptions fail for three structural reasons in dating apps, and none of them are "we forgot."

April 21, 2026 · 4 min read · Common Issues

What Causes Missing Content Descriptions in Dating Apps

Content descriptions fail for three structural reasons in dating apps, and none of them are "we forgot."

Dynamic content rendered without semantic context. Match cards, chat bubbles, and profile carousels are often built as custom View composites — ConstraintLayout nests containing ImageView, TextView, and clickable icons — but the parent container lacks android:contentDescription or accessibilityDelegate. TalkBack reads "ImageView, ImageView, TextView" instead of "Sarah, 28, 3 miles away, loves hiking."

Image loading libraries strip metadata. Glide, Coil, and Picasso load profile photos into ImageView targets. The contentDescription set in XML gets overwritten when the bitmap loads unless you explicitly preserve it. Most teams don't.

Cross-platform parity gaps. React Native's accessibilityLabel on works for static assets but fails on network images without explicit accessibilityProps. Flutter's Semantics widget requires manual wrapping. Dating apps shipping iOS/Android/Web from shared codebases consistently miss one platform.

State-dependent UI without state announcements. "Online now" badges, typing indicators, read receipts, and verification checkmarks change visibility based on WebSocket events. The content description stays static while the visual state flips. Screen reader users hear "verified badge" on an unverified profile because the description never updated.

Real-World Impact

App Store reviews tell the story. Search "TalkBack" or "VoiceOver" in Google Play reviews for Tinder, Bumble, Hinge. You'll find 1-star reviews like: "Can't read profiles, just hears 'button, button, image'" and "Matches disappear because I can't find the chat button." These aren't edge cases — they're blockers.

Revenue correlation. A 2023 analysis of 12 dating apps showed a 0.3-star rating increase correlated with 12% higher Day 7 retention for accessibility-compliant builds. Blind and low-vision users *pay for premium subscriptions* at comparable rates — when they can use the app. One major app lost an estimated $2.3M ARR from accessibility churn before fixing content descriptions.

Legal exposure. NFB v. Target established that inaccessible apps violate ADA. Dating apps face additional scrutiny: they're places of public accommodation *and* handle sensitive personal data. Settlement costs average $250K-$500K plus mandated remediation timelines.

7 Dating-App-Specific Manifestations

#ComponentWhat Screen Reader HearsWhat User Sees
1Profile card in discovery stack"ImageView, button, button"Photo, name/age/location, like/pass buttons
2Verification badge"ImageView" (static)Blue checkmark appears/disappears after video selfie verification
3Chat list item"TextView, TextView"Contact name, last message preview, timestamp, unread count badge
4Photo carousel in profile detail"ImageView" (repeated 6x)Swipeable gallery with caption overlay per photo
5Super Like / Boost button"Button"Animated star/lightning icon with pulse effect
6Distance filter pill"Button, Button""5 mi", "10 mi", "25 mi" chips with active state
7Report/Block menu item"MenuItem""Report Sarah", "Block Sarah", "Unmatch" in bottom sheet

Critical nuance: Example 2 (verification badge) is a *security* issue. Blind users cannot distinguish verified from unverified profiles — a documented vector for romance scams targeting visually impaired users.

Detection: Tools and Techniques

Automated (CI gate):


# .github/workflows/a11y.yml
- name: Run Accessibility Scanner
  run: |
    ./gradlew assembleDebug
    java -jar accessibility-scanner.jar \
      --app app/build/outputs/apk/debug/app-debug.apk \
      --test-type content-description \
      --fail-on-violation

SUSATest autonomous exploration catches this differently: our accessibility persona navigates every screen, triggers every state (matched, chatting, blocked, reported), and logs missing contentDescription per element with screenshot + view hierarchy. It finds the *dynamic* gaps static analyzers miss — like the verification badge that only appears after WebSocket confirmation.

Manual testing checklist:

  1. Enable TalkBack → swipe through discovery stack → verify each card reads: "Name, Age, Distance, [Bio snippet], Like button, Pass button"
  2. Open profile detail → swipe photo carousel → verify each photo reads caption: "Photo 3 of 6: At Machu Picchu"
  3. Trigger verification flow → confirm badge announces "Verified profile" *after* completion
  4. Open chat list → verify unread count announces: "Sarah, 3 unread messages, last: 'Hey! 2m ago'"
  5. Long-press match → verify report menu reads: "Report Sarah for spam, Block Sarah, Unmatch"

What to look for in logs:

Fixes: Code-Level Guidance

1. Profile Card (Discovery Stack)


// ProfileCardView.kt
class ProfileCardView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    private val profilePhoto: ImageView = findViewById(R.id.profile_photo)
    private val nameAgeLocation: TextView = findViewById(R.id.name_age_location)
    private val bioSnippet: TextView = findViewById(R.id.bio_snippet)
    private val likeBtn: MaterialButton = findViewById(R.id.like_btn)
    private val passBtn: MaterialButton = findViewById(R.id.pass_btn)

    fun bind(profile: Profile, onLike: () -> Unit, onPass: () -> Unit) {
        // Photo: use alt text from profile or fallback
        profilePhoto.contentDescription = profile.photos.firstOrNull()?.altText
            ?: "${profile.name}'s profile photo"

        // Composite description for the whole card
        val cardDesc = StringBuilder()
            .append("${profile.name}, ${profile.age}")
            .append(", ${profile.distanceKm} kilometers away")
        profile.bio?.let { cardDesc.append(", ${it.take(100)}") }
        contentDescription = cardDesc.toString()

        // Buttons need explicit labels — "Like button" not "Button"
        likeBtn.contentDescription = "Like ${profile.name}"
        passBtn.contentDescription = "Pass on ${profile.name}"

        // Ensure focus order: photo → name → bio → like → pass
        ViewCompat.setAccessibilityTraversalBefore(likeBtn, R.id.bio_snippet)
        ViewCompat.setAccessibilityTraversalBefore(passBtn, R.id.like_btn)
    }
}

2. Verification Badge (State-Dependent)


// VerificationBadge.kt
class VerificationBadge @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {

    private var isVerified = false

    fun setVerified(verified: Boolean) {
        isVerified = verified
        setImageResource(if (verified) R.drawable.ic_verified else 0)
        visibility = if (verified) VISIBLE else GONE
        
        // CRITICAL: Announce state change
        contentDescription = if (verified) "Verified profile" else ""
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
    }

    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        info.isCheckable = true
        info.isChecked = isVerified
        info.className = "android.widget.CheckBox" // Semantic role
    }
}

3. Chat List Item (Unread Count)


<!-- item_chat.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/chat_container"
    android:focusable="true"
    android:clickable="true"
    android:contentDescription="@string/chat_item_desc">

    <TextView android:id="@+id/contact_name" ... />
    <TextView android:id="@+id/last_message" ... />
    <TextView android:id="@+id/timestamp" ... />
    <TextView 
        android:id="@+id/unread_badge"
        android:visibility="gone"
        android:contentDescription="@string/unread_count" />
</androidx.constraintlayout.widget.ConstraintLayout>

// ChatAdapter.kt
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
    val chat = chats[position]
    val container = holder.itemView.findViewById<View>(R.id.chat_container)
    
    val desc = StringBuilder()
        .append("${chat.contactName}")
    if (chat.unreadCount > 0) {
        desc.append(", ${chat.unreadCount} unread messages")
    }
    chat.lastMessage?.let { desc.append(", last message: $it") }
    chat.timestamp?.let { desc.append(", ${formatTime(it)}") }
    
    container.contentDescription = desc.toString()
    
    // Update badge separately for screen readers
    val badge = holder.itemView.findViewById<TextView>(R.id.unread_badge)
    badge.apply {
        text = chat.unreadCount.toString()
        visibility = if (chat.unreadCount > 0) VISIBLE else GONE
        contentDescription = if (chat.unreadCount > 0) 
            "${chat.unreadCount} unread messages" else ""
    }
}

4. Photo Carousel (ViewPager2)


// ProfilePhotoAdapter.kt
class ProfilePhoto

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free