Skip to content
Compose · Article

Creating Self-Adjusting TextFields with Jetpack Compose

Using BoxWithConstraints and ParagraphIntrinsics to shrink a TextField's font so long input still fits on one line.

Ali Rezaiyan2 min read

I needed a text field that stays readable no matter how much the user types: instead of clipping the text or scrolling, the font should get a bit smaller until it fits. Here's how I did it in Jetpack Compose.

If you're also working on Compose animations, the official codelab is a good place to start.

Short version

  • Compose makes it easy to react to the space a layout gets.
  • ParagraphIntrinsics tells you how wide a piece of text will be in a given style.
  • A shrinking text field works well for chat input, forms, and search bars.

The problem

Long input in chat bubbles and forms tends to get cut off or pushed behind a scroll bar. I wanted a text field that shrinks the font just enough to fit the width it has.

What I used

  • BoxWithConstraints, to know how wide the parent is.
  • ParagraphIntrinsics, to measure how wide the text wants to be.
  • LocalDensity, to convert between dp and px.

The code

@Composable
fun ResizableInputField(
    text: String,
    onValueChange: (String) -> Unit,
) {
    BoxWithConstraints {
        val shrunkFontSize = calculateShrunkFontSize(text)
        BasicTextField(
            value = text,
            onValueChange = onValueChange,
            textStyle = TextStyle(fontSize = shrunkFontSize),
        )
    }
}

@Composable
private fun BoxWithConstraintsScope.calculateShrunkFontSize(text: String): TextUnit {
    var shrunkFontSize = Theme.typography.displayMedium.fontSize
    val calculateIntrinsics: @Composable () -> ParagraphIntrinsics = {
        ParagraphIntrinsics(
            text = text,
            style = Theme.typography.displayMedium.copy(fontSize = shrunkFontSize),
            density = LocalDensity.current,
            fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
        )
    }

    var intrinsics = calculateIntrinsics()
    with(LocalDensity.current) {
        while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
            shrunkFontSize *= 0.9f
            intrinsics = calculateIntrinsics()
        }
    }

    return shrunkFontSize
}

It starts at the normal font size, measures the text, and keeps shrinking it by 10% until it fits the available width. You can use a regular TextField instead of BasicTextField and it works the same way.

Why bother

On small screens this keeps long input readable, without a scroll bar or text that gets cut off.

Wrapping up

This only needed a few Compose building blocks and no extra libraries. It's a small detail, but it makes the input feel a lot more finished.

I'm on GitHub (@rezaiyan) and Twitter (@arezaiyan) if you want to see more Compose experiments.

Enjoyed this?

There's more where that came from.

All posts