Join the Newsletter

Subscribe to get the latest content by email.

    I won't send you spam. Unsubscribe at any time.

    Complete guide to Rich Text in Jetpack Compose

    Jetpack Compose series:

    This article teaches how to customize text content in Jetpack Compose. It covers basics first and then goes deeper into advanced ways of customizing text appearance.

    Users don’t read anymore. They scan the content for anything that seems to be what they need.

    If you are not familiar with what rich text is then see the comparison to plain text below:

    plain text and rich text

    From the picture we can see that rich text:

    1. Displays content using multiple different styles;
    2. Emphasizes some segments to stress the importance of some particular text.

    It’s important to say that text styling is about UI and UX. Just like huge text in a book tells you it’s a header for a new chapter, this blue text makes you think it’s a link (it’s not).
    So rich text is not only about formatting and styling, but also about the delivery of the content and its meaning.

    Now how to apply styles to Text?

    We’re going to cover 3 ways how one can tweak text appearance. Each following approach is more advanced and flexible:

    1. Filling Text composable properties
    2. Using (and reusing) TextStyle
    3. With AnnotatedString

    Style Text composable with its properties

    In this section we will go through Text parameters. We will learn what these parameters do, how we can specify them and then see what content looks like when we apply them.

    Let’s start with a basic text field in Jetpack Compose:

        Text(
    text = "Hello world"
    )

    Here’s our text!

    hello world image

    Now let’s have some fun and style it.

    Text size

    A Text composable actually supports 2 units for size:

    We will simply set an sp value to fontSize to make it slightly bigger.

    ⏭️ tldr: fontSize = 24.sp

    hello world image with font size

    Text color

    Jetpack Compose has its own Color class which is:

    1. not compatible with Android View Color
      However, you can simply turn Compose Color into View Color using the toArgb() function.
    2. created in a different way:
      When setting a custom hex color we specify it as Int. Be aware of its transparency as we have to set it explicitly.

    Let’s make the text purple. The value would be 0xFF81009E where

    • 81009E is the hex of the color (#81009E)
    • 0xFF is the alpha that adjusts transparency (hex transparency table)
      Now 0xFF81009E is an Int value.

    ⏭️ tldr:color = Color(0xFF81009E)

    hello world image color

    Font family

    Using fontFamily we can select a custom typeface or choose one of the font families that come by default:

    To apply a platform default font use FontFamily.Default

    Others go by:

    • FontFamily.Monospace
    • FontFamily.SansSerif
    • FontFamily.Cursive
    • FontFamily.Serif

    Here’s how different these font styles are visually:

    fonts difference

    To use a custom font put it into the Font class like this: FontFamily(Font(R.font.open_sans))

    ⏭️ tldr:fontFamily = FontFamily.Monospace

    hello world image font family

    Text alignment

    The TextAlign value is used to align text content.

    There are 6 types of alignment:

    • TextAlign.Center - centers text content(based on Text width)
    • TextAlign.Justify - fills the width of the Text composable by stretching its content all over the available space.
    • TextAlign.Start and TextAlign.Left - the default. Align the text on the leading/left edge.
    • TextAlign.End and TextAlign.Right - Align the text on the leading/right edge.

    The difference between start and left, end and right is not big, but never mix them.

    image of start/left and right/end

    ❗ Keep in mind that start ≠ left and end ≠ right when you have to support RTL languages (such as Arabic).
    end is the same as right if the direction is left-to-right and left if the direction is right-to-left.
    Align to right and the content is aligned to the right edge of the composable.

    ⏭️ tldr:textAlign = TextAlign.Center

    Set max lines limit

    The maxLines property sets how many lines your text can be on.
    Now what happens if maxLines equals 1, but the text requires 2 lines? It’s called text overflow and there are a few strategies on how to handle it.

    ⏭️ tldr:maxLines = 1

    Text overflow

    There are 3 ways how you can handle text overflow by default:

    • TextOverflow.Clip - clips the overflowing text to the bounds of the composable;
    • TextOverflow.Ellipsis - replace overflowing text with an ellipsis (…);
    • TextOverflow.Visible - shows all text even if there is not enough space (will render even outside of the composable)

    ⏭️ tldr:overflow = TextOverflow.Ellipsis - end with 3 dots if too many characters

    Make text clickable

    • To Make the entire text clickable use modifier:
      modifier = Modifier.clickable { }
    • To make some parts of text clickable:
      Use ClickableText and AnnotatedString(more details below)

    ⏭️ tldr: modifier = Modifier.clickable { /* execution */ }

    Text all caps

    Just make the input text uppercase:
    text = "Hello world".uppercase()

    Add underline

    TextDecoration draws a line for your text.

    • TextDecoration.Udnerline - draws it right below;
    • TextDecoration.LineThrough - draws a line right through the content;
    • TextDecoration.None - as the name suggests - does nothing.

    ⏭️ tldr: textDecoration = TextDecoration.Udnerline

    hello world image text decoration

    Make text bold

    Text now has weight! It gives a whole range of values from Thin & ExtraLight to Bold & ExtraBold!
    You can set how bold the text is in glyphs(“unit” for font weight) or by simply using aliases like:

    • FontWeight.Thin
    • FontWeight.ExtraLight
    • FontWeight.Light
    • FontWeight.Normal
    • FontWeight.Medium
    • FontWeight.SemiBold
    • FontWeight.Bold
    • FontWeight.ExtraBold
    • FontWeight.Black

    ⏭️ tldr: fontWeight = FontWeight.ExtraBold

    hello world image font weight

    Don’t be confused with the fontStyle parameter though!

    Make text italic

    Now fontStyle is very simple. It has only 2 options:

    • FontStyle.Italic - applies italic to the text.
    • FontStyle.Normal - keeps the original text.

    ⏭️ tldr: fontStyle = FontStyle.Italic

    hello world image font style

    Now briefly about some other properties:

    letterSpacing - specifies the amount of space between each character.
    lineHeight - specifies line height for the text paragraph.
    softWrap - specifies if the text breaks at soft line breaks.
    onTextLayout - a callback to trigger when text layout is calculated (more details).

    Recap

    • Text composable has common basic properties for styling
    • Use fontWeight to make text Bold and fontStyle to make it Italic

    Update text appearance with TextStyle

    Text properties provide common styling attributes. However, this approach has 2 major drawbacks:

    1. It’s tedious to fill properties manually every time.
    2. It has a limited number of attributes and is missing other properties like text background color or shadows.
    3. Not very convenient to reuse.

    Applying 1 style to many texts in a single line

    đź’ˇ TextStyle is a styling configuration. It can be reused easily and its properties are all attributes that a text can have by default.

    When creating a TextStyle you can either fill out its properties directly or initialize it with SpanStyle and ParagraphStyle whose parameters TextStyle takes anyways.

    • SpanStyle parameters affect text itself.
    • ParagraphStyle adjusts the paragraph or “the lines” text is on.

    Just like we could use styles from Android resource for our Text Views we now can reuse one TextStyle in many composables. So this XML style resource:

        <style name="TextAppearance.MyApp.Medium" parent="TextAppearance.AppCompat.Medium">
    <item name="android:textSize">32sp</item>
    <item name="android:color">@color/black</item>
    <item name="background">@color/gray</item>
    </style>

    Becomes this:

        val medium = SpanStyle(
    fontSize = 32.sp,
    color = Color.Black,
    background = Color.Gray
    )

    If we go further, we can organize it in a way that we can reuse it later as a part of our app theme! So in this case we could collect other text styles under TextAppearance object:

    object TextAppearance {

    val Medium = SpanStyle(
    fontSize = 32.sp,
    color = Color.Black,
    background = Color.Gray
    )
    }

    Check out my article on how to create custom themes in Jetpack Compose to see more on theming!

    Recap

    • TextStyle extends the number of properties you can style your text by;
    • It is possible to reuse the same TextStyle for multiple Text composable;
    • You can generate TextStyle with different combinations of SpanStyle and ParagraphStyle. The first tweaks text content appearance, the second changes paragraph settings.

    Full customization with AnnotatedString

    What makes AnnotatedString different from TextStyle and Text properties is that their parameters affect the entire text. AnnotatedString allows to tweak the apperance of its certain parts and can even add metadata to it.

    Let’s say we want to make a hyperlink and open a website when we click on it. Theoretically, we could set the blue color to text and add Modifier.clickable to open a static link in a browser.

    But.

    Our entire content is plain text. It’s just a few links here and there that should be highlighted and clickable.
    AnnotatedString is the perfect tool to make a hyperlink in Jetpack compose in this case! Let’s implement this use case to see it.

    First, let’s create some constants and add a ClickableText composable. It requires us to specify the text and onClick parameters by default.

    private const val TAG_URL = "ANNOTATION_TAG_URL"

    @Composable
    fun MainText() {
    val uriHandler = UriHandlerAmbient.current // utility to open links in a browser
    val text = "Here is my website." // "my website" contains a link

    val link = "https://dmytroshuba.com/"
    val annotatedText = attachLink(
    source = text,
    segment = "my website",
    link = link
    )
    ClickableText(
    text = annotatedText,
    onClick = { /* implementation */}
    )
    }

    onClick when triggered provides the index of the character that a user clicked on. It’s important as that’s the way we can verify that there’s a link.

    Not let’s have a closer look at the attachLink function:

    fun attachLink(
    source: String,
    segment: String,
    link: String
    ): AnnotatedString {
    val builder = AnnotatedString.Builder() // builder to attach metadata(link)
    builder.append(source) // load current text into the builder

    val start = source.indexOf(segment) // get the start of the span "my website"
    val end = start + segment.length // get the end of the span
    val hyperlinkStyle = SpanStyle(
    color = Color.Blue,
    textDecoration = TextDecoration.Underline
    ) // create a hyperlink text style

    builder.addStyle(hyperlinkStyle, start, end) // style "my website" to make it look like a link
    builder.addStringAnnotation(TAG_URL, link, start, end) // attach the link to the span. We can then access it via the TAG_URL

    return builder.toAnnotatedString()
    }

    AnnotatedString contains the text. We styled “my website” substring in it and added a url “behind its characters”.
    Now let’s get back to the ClikableText.

        ClickableText(
    text = annotatedString,
    onClick = {
    annotatedString
    .getStringAnnotations(TAG_URL, it, it)
    .firstOrNull()
    ?.let { url -> uriHandler.openUri(url.item) }
    }

    When clicking on the text we call onClick callback and get the index of the character that was clicked.

    Then what we do is we try to extract the url with TAG_URL from the character behind the index. If the character is the one that is behind the span with “my website” text then we receive the link and open it in a browser using uriHandler.

    my website text picture

    Extra: dynamic parsing example

    Using AnnotatedString we can pretty much automate the styling and annotation of our content.
    One of my recent releases is DailyTags library. It can parse markdown, some HTML and even custom markups! Using AnnotatedString it could both style individual segments of text and add metadata (such as url) on the fly as it splits the entire content into small chunks called nodes.
    In fact, it worked pretty much the same way as in the hyperlink example.

    Recap

    • Using AnnotatedString we can style individual parts of text independently.
    • AnnotatedString allows us to add metadata to text. It can be a url, an ID, etc.
    • AnnotatedString is a great

    Q&A

    SpannableString vs AnnotatedString

    SpannableString is used for decorating text in Android View while AnnotatedString is a native way to do so in Jetpack Compose.
    In a nutshell, they’re pretty much the same, but made for different UI systems.

    The entire text row is clickable. How do I make only text Clickable?

    The problem is that your Text composable takes more width/height than it needs for its content.
    Wrap it into a Box and center the content inside it. Now Box should cover extra space while Text boundaries are right next to its content.

    How to make 3 dots in the end of a string if it’s too long in Jetpack Compose?

    Add ellipsis overflow mode in the text properties:

    Text(
    text = "...",
    overflow = TextOverflow.Ellipsis
    )

    How to parse HTML text in Jetpack Compose?

    HTML is a markup and the real question is what’s the tool to parse it. One way to do so is with AndroidView.
    Another way to do so is using DailyTags library as it supports HTML text tags.

    Resources

    Jetpack Compose Text
    How to create a custom theme
    Text Style documentation
    Jetpack Compose styling text by Alex Zhukovich