Pocket Studio Academy
HomePart 33.7

Text and typography

Full course9 min read·3 questions

Every parameter of the Text composable that matters, why text sizes use sp and never dp, how the Material 3 type scale keeps a whole app consistent, and how to style one word inside a sentence.

The composable you will write most often

Text is the most-used composable in Android, by a distance. You have already used it a dozen times in the plainest possible way:

kotlin
Text(text = "DICE DUEL")

That gives you 16sp of near-black in the phone's default face. Perfectly readable, and completely flat — a screen made only of that looks like a spreadsheet, because nothing tells the eye where to start.

This lesson is about the parameters that fix it.

Think of it like this

Look at a newspaper front page from across a room, too far away to read a word of it.

You still know exactly what is going on. There is one huge headline, a smaller line under it, a grey block of body text, and a tiny italic caption under the photograph. You have not read a single letter, yet you already know the order to read them in.

That is typography doing its job: size, weight and colour tell you what matters before you read anything. A screen with one text style makes every reader do that sorting work themselves, every time.

The parameters worth knowing

kotlin
1Text(
2    text = "DICE DUEL",
3    color = Color(0xFF5B3FD6),
4    fontSize = 40.sp,
5    fontWeight = FontWeight.Black,
6    letterSpacing = 6.sp
7)
ParameterWhat it does
textThe characters. A String — numbers must be converted
colorThe colour of the letters themselves
fontSizeSize, in sp
fontWeightNormal, Medium, SemiBold, Bold, Black
fontStyleFontStyle.Italic
letterSpacingExtra gap between characters, in sp
lineHeightDistance between lines of a wrapped paragraph
textAlignTextAlign.Center, .Start, .End, .Justify
maxLinesStop after this many lines
overflowWhat to do with text that does not fit
textDecorationTextDecoration.Underline, .LineThrough
styleA whole bundle of the above, reused (see below)

Two of those have traps in them, so they get their own sections.

sp, never dp, for text

Sizes on screen use . Text uses , and the difference is not cosmetic.

sp means scale-independent pixel. It starts out identical to dp, but it also multiplies by whatever text size the user chose in their phone's accessibility settings. Someone who has set their font to Large gets larger text in your app, automatically, without you writing anything.

Set your text in dp and you have quietly overruled that person's eyesight. It is one of the easiest failures to commit and one of the easiest to avoid: if it is text, it is sp.

kotlin
1fontSize = 15.sp   // right
2fontSize = 15.dp   // wrong, and will not compile

letterSpacing and lineHeight take sp too, for the same reason — they have to grow with the letters or the line falls apart.

overflow: what happens when it does not fit

By default, long text wraps onto as many lines as it needs, and if there is no room it is simply clipped mid-letter. That looks broken. The standard fix is two parameters together:

kotlin
1Text(
2    text = note.title,
3    maxLines = 1,
4    overflow = TextOverflow.Ellipsis
5)

maxLines = 1 stops it wrapping; TextOverflow.Ellipsis puts a at the cut. Neither works properly without the other — overflow has nothing to do unless something is limiting the lines. You will use this pair constantly in Pocket Notes, where note titles are whatever length the user felt like.

The type scale: stop inventing numbers

Setting fontSize by hand on every Text works, and it does not scale. Twenty screens later your headings are 40, 38, 40 and 42sp, and nobody knows which is right.

ships a type scale: fifteen named, pre-designed text styles, sitting in your . You pick a role, not a number:

kotlin
1Text(
2    text = "DICE DUEL",
3    style = MaterialTheme.typography.displaySmall
4)
5Text(
6    text = "First to 30 wins the duel",
7    style = MaterialTheme.typography.bodyMedium
8)

The five families, biggest to smallest, each in Large, Medium and Small:

FamilyUse it for
display*One enormous number or word. A timer, a score
headline*The title of a screen
title*The title of a section or a card
body*Paragraphs and normal reading text
label*Buttons, chips, small captions in CAPS

Text with no style uses bodyLarge, which is why plain text comes out at 16sp.

To bend one of them without abandoning the scale, use .copy():

kotlin
1Text(
2    text = "PLAYER 1",
3    style = MaterialTheme.typography.labelLarge.copy(
4        letterSpacing = 2.sp
5    )
6)

You get the scale's size, weight and line height, with one thing changed. Lesson 3.11 shows how to replace the whole scale with your own fonts.

Tip

Any parameter you pass directly to Text wins over the same setting inside style. So style = ...titleLarge, color = Color.Red is legal and does what you expect. Direct parameters are for one-offs; style is for the pattern you use everywhere.

A real title block

This is Dice Duel's, with the theme colours written out as literals for now — Lesson 3.11 replaces them with proper roles.

TitleBlock.ktkotlin
1@Composable
2fun TitleBlock() {
3    Column {
4        Text(
5            text = "DICE DUEL",
6            color = Color(0xFF5B3FD6),
7            fontSize = 40.sp,
8            fontWeight = FontWeight.Black,
9            letterSpacing = 6.sp
10        )
11        Spacer(Modifier.height(2.dp))
12        Text(
13            text = "First to 30 wins the duel",
14            color = Color(0xFF4A4463),
15            fontSize = 15.sp,
16            fontWeight = FontWeight.Medium,
17            letterSpacing = 1.sp
18        )
19    }
20}
9:41▲ ▮
DICE DUEL
First to 30 wins the duel

TitleBlock() — one composable, two Texts, a clear hierarchy.

Styling one word inside a sentence

Sometimes you need part of a line to look different — a bold number in a sentence, a coloured name. You cannot do it with two Text composables, because they would not wrap together as one paragraph.

The answer is an annotated string:

kotlin
1val line = buildAnnotatedString {
2    append("First to ")
3    withStyle(
4        SpanStyle(fontWeight = FontWeight.Black)
5    ) {
6        append("30")
7    }
8    append(" wins")
9}
10
11Text(text = line)

buildAnnotatedString gives you a little builder: append adds plain text, and withStyle applies a SpanStyle to everything appended inside it. The result is one piece of text that wraps, aligns and truncates as a single unit.

You will use this properly in Pocket Notes to highlight the matched part of a search result.

Try it in Pocket Studio
  1. Open ComposeLab and replace the contents of setContent with a call to TitleBlock().
  2. Type the TitleBlock composable from this lesson. Accept the imports for androidx.compose.ui.unit.sp, androidx.compose.ui.text.font.FontWeight and androidx.compose.ui.graphics.Color.
  3. Tap Run. You should see a violet logo and a grey subtitle.
  4. Change letterSpacing = 6.sp to 0.sp and Run. Same words, much cheaper-looking.
  5. Change fontSize = 40.sp to 40.dp. Try to build — the compiler stops you. This is the one mistake Compose will not let you make.
  6. Now delete both Text parameter lists down to just text = ..., and add style = MaterialTheme.typography.displaySmall to the first and style = MaterialTheme.typography.bodyMedium to the second. Run. Different look, zero magic numbers.
  7. Add a third Text with a very long sentence, maxLines = 1 and overflow = TextOverflow.Ellipsis. Run, and watch it end in a neat .
Error Doctor5 common errors
e: Unresolved reference: sp
MeansThe sp unit needs its own import, and it is in a different package from the Text composable itself.
FixAdd import androidx.compose.ui.unit.sp. It sits next to dp, in androidx.compose.ui.unit.
e: Type mismatch: inferred type is Int but TextUnit was expected
MeansYou wrote fontSize = 40 instead of fontSize = 40.sp. Compose will not guess a unit for you.
FixAdd .sp. The same applies to letterSpacing and lineHeight. If you accidentally wrote .dp you will get a similar mismatch — for text it is always sp.
e: Unresolved reference: FontWeight
MeansFont weights live in the text package, not with the layout or unit imports.
FixAdd import androidx.compose.ui.text.font.FontWeight, then use FontWeight.Bold, FontWeight.Black and so on. For italics you also need androidx.compose.ui.text.font.FontStyle.
My text is invisible, but the layout still leaves a gap for it
MeansThe colour has no alpha channel. Color(0x5B3FD6) is read as 0x005B3FD6 — fully transparent — because the first two digits are the opacity and you only supplied six digits in total.
FixAlways write colours as eight hex digits starting with the alpha: Color(0xFF5B3FD6). FF means fully opaque.
A long title runs off the edge and is chopped mid-letter
MeansText with no maxLines will wrap, and if the container will not grow, the extra is clipped with no indication that anything is missing.
FixSet both: maxLines = 1 and overflow = TextOverflow.Ellipsis. Import androidx.compose.ui.text.style.TextOverflow. Setting overflow on its own does nothing, because nothing is limiting the lines.
Recap
  • Text takes color, fontSize, fontWeight, letterSpacing, lineHeight, textAlign, maxLines and overflow — and a style that bundles them.
  • Text sizes are always , never , so the user's chosen font size is respected.
  • maxLines plus overflow = TextOverflow.Ellipsis is the standard way to handle text that is too long. Neither works alone.
  • Prefer MaterialTheme.typography.* roles over hand-picked numbers, and .copy() when you need to bend one.
  • Colours need eight hex digits: Color(0xFF5B3FD6). Six digits gives you invisible text.
  • buildAnnotatedString styles part of a single piece of text.
  • Next: buttons — the trailing-lambda pattern that lets you put anything inside one, and where a tap should change state.