Inclusive Android Apps

Archives
Log in
June 9, 2026

Inclusive Android Apps #7: The Problem of Inacessible Inline Links

Seventh issue covers the accessibility issues of inline links in apps and how to solve them with LinkAnnotation.Url.

I was recently fixing an inline link in an app. Tapping it worked fine, but it was completely unreachable with a keyboard or screen reader.

The Problem of Inline Links

Inline links are links that are within paragraphs of text. They often appear in onboarding or other places, and common cases for inline links are, for example, links to terms and conditions or privacy policy.

They've also often been inaccessible. For example, this code:

val text = buildAnnotatedString {
  append("By continuing, you agree to our ")

  pushStringAnnotation(
    tag = "URL", 
    annotation = "https://example.com/terms"
  )
  withStyle(
    SpanStyle(
      color = Color.Blue, 
      textDecoration = TextDecoration.Underline
    )
  ) {
    append("Terms of Service")
  }
  pop()

  append(" and ")

  pushStringAnnotation(
    tag = "URL", 
    annotation = "https://example.com/privacy"
  )
  withStyle(
    SpanStyle(
      color = Color.Blue, 
      textDecoration = TextDecoration.Underline
    )
  ) {
    append("Privacy Policy")
  }
  pop()
}

ClickableText(
  text = inaccessibleText,
  onClick = { offset ->
    inaccessibleText
      .getStringAnnotations(
        tag = "URL", 
        start = offset, 
        end = offset
      )
      .firstOrNull()
      ?.let { annotation ->
        uriHandler.openUri(annotation.item)
      }
  }
)

Renders the following:

Text: By continuing, you agree to our Terms of Service and Privacy Policy. Terms of Service and Privacy Policy are blue with an underline, and other text is black.

And if you click or tap the links, they actually work. But if you try to navigate to the links with any assistive technology such as keyboard, screen reader, or switch access, nothing happens. Those links are not recognized as links.

On top of the accessibility problems, ClickableText has been deprecated since Compose 1.7, so there are two good reasons to move away from it.

Who This Hurts

  • Keyboard users
  • Switch Access users
  • Screen reader users
  • Anyone needing semantic information about interactive elements

Why Developers Do This

Most likely, the reason is that developers don't test with other input methods than pointer input, and thus, the fact that it's not working for other input methods doesn't come up. ClickableText also sounds like exactly the right solution, as it's literally called "Clickable", so it's easy to reach for without questioning whether it actually works for all users.

The Solution

The solution is to use LinkAnnotation.Url:

val text = buildAnnotatedString {
  append("By continuing, you agree to our ")

  withLink(
    LinkAnnotation.Url(
      url = "https://example.com/terms",
      styles = TextLinkStyles(
        style = SpanStyle(
          color = Color.Blue,
          textDecoration = TextDecoration.Underline
        ),
      )
    ) {
      append("Terms of Service")
    }
  )

  append(" and ")

  withLink(
    LinkAnnotation.Url(
      url = "https://example.com/privacy",
      styles = TextLinkStyles(
        style = SpanStyle(
          color = Color.Blue,
          textDecoration = TextDecoration.Underline
        ),
      )
    ) {
      append("Privacy Policy")
    }
  )
}

Text(
  text = text
)

Unlike ClickableText, LinkAnnotation.Url semantically marks up the link, so keyboard, Switch Access, and screen reader users can all discover and navigate to it.

Semantic markup gets us most of the way there, but there's still one more thing: visible state changes. Keyboard users need to see where the focus is, and without explicit styles for focused, hovered, and pressed states, the link looks the same regardless of its state. Luckily, TextLinkStyles has got us covered: We can also provide the focused, hovered, and pressed styles:

val linkStyles = TextLinkStyles(
  style = SpanStyle(
    color = Color.Blue,
    textDecoration = TextDecoration.Underline
  ),
  focusedStyle = SpanStyle(
    textDecoration = TextDecoration.None,
    fontWeight = FontWeight.Bold
  ),
  hoveredStyle = SpanStyle(
    textDecoration = TextDecoration.None
  ),
  pressedStyle = SpanStyle(
    background = MaterialTheme.colorScheme.secondaryContainer,
    color = MaterialTheme.colorScheme.onSecondaryContainer
  )
)

And then, we would pass these styles to the link:

withLink(
  LinkAnnotation.Url(
    url = "https://example.com/privacy",
    styles = linkStyles
  )
) {
  append("Privacy Policy")
}

For example, the focused state would then look like this:

The same text as in previous image, with Privacy Policy without underline, but it's bold.

Testing

  • Connect an external keyboard and try tabbing to and activating the links
  • Enable Switch Access and try to reach the links
  • Test with TalkBack

Read More

LinkAnnotation API

The official Android documentation for LinkAnnotation, including LinkAnnotation.Url used in this issue.

Understanding SC 2.1.1 Keyboard (Level A)

The WCAG success criterion that underpins the problem in this issue: interactive elements must be operable with a keyboard. While written for web, the principle applies directly to Android.


That's a wrap for Issue #7 of Inclusive Android Apps. Have you come across inaccessible inline links in apps you've worked on? Hit reply and let me know!

Next month: Animations and how to make them more accessible.


Thanks for reading!
-Eevis
eevis.codes

Don't miss what's next. Subscribe to Inclusive Android Apps:
Share this email:
Share via email Share on Mastodon Share on Bluesky
Eevis.codes
Bluesky
Mastodon
Powered by Buttondown, the easiest way to start and grow your newsletter.