Pantrix UI

Filled Auto-resize Text Field

A text field whose label is inside its box and whose text gets smaller to stay in view: an amount, a card's number, a short note, anything that is read whole, in the filled box of PantrixFilledTextField.

import com.pantrixui.components.inputs.filledautoresizetextfield.PantrixFilledAutoResizeTextField

Examples

At its plainest

The least there is to write: a TextFieldState and a label. The field was given no width: the box is at its style's least width and does not grow wider with its text. The account name is too long for that box, so it is drawn smaller; and since the field holds a text, the label is on its own line above the text.

Delete the text: what is left grows back. Once the field is empty and loses the focus, the label comes down to the text's place.

PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("Yılmaz Mimarlık ve Mühendislik Limited Şirketi"),
    label = "Account name",
    tagKey = "account_name"
)

The text style and the box's height

textStyle is the style of the text at its largest; without it the text is in the body size. A large style makes the box taller only by what its label and one line of that style need: compare the first two fields.

The third field is empty and in the display style. Whatever the text's style, the label rests in the text's place in the body size. Press the field: the label moves up, and the placeholder appears in the text's style.

PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("1250"),
    modifier = Modifier.fillMaxWidth(),
    label = "No style given",
    tagKey = "style_body"
)
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("1250"),
    modifier = Modifier.fillMaxWidth(),
    label = "In the title style",
    textStyle = PantrixTheme.typography.title,
    tagKey = "style_title"
)
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "In the display style, empty",
    placeholder = "0",
    textStyle = PantrixTheme.typography.display,
    tagKey = "style_display"
)

The smallest size

Both fields hold the same name in the title style, and the name fits neither box. The first was given no minFontSize: the text shrinks as far as the theme's footnote size. In the second it is never set smaller than 16 sp. minFontSize can also be given in em: it is then a share of the style's own size.

A text that still does not fit at its smallest size stops shrinking and scrolls, as the text of any field does.

val company = "Yılmaz Mimarlık, Mühendislik ve Danışmanlık Limited Şirketi"
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState(company),
    label = "Not given: footnote size",
    textStyle = PantrixTheme.typography.title,
    tagKey = "smallest_default"
)
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState(company),
    label = "Smallest 16 sp",
    textStyle = PantrixTheme.typography.title,
    minFontSize = 16.sp,
    tagKey = "smallest_sp"
)

An amount that carries its text to a ViewModel

A payment amount, in the headline style. The field's state is created with rememberTextFieldState, from the value the ViewModel holds at that moment; the text in the state is followed with snapshotFlow, and every change is reported to the ViewModel. This is how the text is taken from the field's state.

The first value in the ViewModel is read once. From then on it goes one way: the text flows from the field to the ViewModel. When the screen is composed anew the ViewModel is still there, and the field starts with its value.

The line below shows what the ViewModel holds, and the button reads from it too: it cannot be pressed until an amount has arrived.

val viewModel = viewModel(key = "payment") { PaymentViewModel() }
val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val initialUiState = remember(viewModel) { viewModel.uiState.value }
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

val amount = rememberTextFieldState(initialUiState.amount)
LaunchedEffect(amount) {
    snapshotFlow { amount.text.toString() }.collectLatest { viewModel.updateAmount(it) }
}
PantrixFilledAutoResizeTextField(
    state = amount,
    modifier = Modifier.fillMaxWidth(),
    label = "Amount to pay",
    placeholder = "0",
    supportingText = "Shrinks as you type, grows back as you delete",
    trailingIcon = { PantrixText(text = "TL") },
    showClearButton = true,
    textStyle = PantrixTheme.typography.headline,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
    inputTransformation = DigitsOnly,
    tagKey = "payment_amount"
)

PantrixText(
    text = "ViewModel: amount \"${uiState.amount}\", digits: ${uiState.amount.length}",
    style = PantrixTheme.typography.footnote,
    color = PantrixTheme.colors.textSecondary,
    tagKey = "payment_state"
)
PantrixButton(
    text = "Pay",
    onClick = {
        scope.launch { snackbars?.show(message = "${uiState.amount} TL paid", tagKey = "payment_made") }
    },
    enabled = uiState.isComplete,
    tagKey = "pay"
)

private class PaymentViewModel : ViewModel() {
    private val state = MutableStateFlow(PaymentUiState())
    val uiState: StateFlow<PaymentUiState> = state.asStateFlow()

    fun updateAmount(amount: String) {
        state.update { it.copy(amount = amount) }
    }
}

private data class PaymentUiState(val amount: String = "") {
    val isComplete: Boolean get() = amount.isNotEmpty() && amount.any { it != '0' }
}

private val DigitsOnly = InputTransformation {
    if (!asCharSequence().all(Char::isDigit)) revertAllChanges()
}

A field whose text is held as a String

A customer number of twelve digits. The caller holds the number as a String: the field shows it and asks for every change, and since a change that is not all digits is not taken, it is not shown either. The supporting text is made from that String.

The text that is asked for has to come back as value by the next frame, or the field shows the old text and the caret jumps: keep the text in state that the callback writes at once, and choose the overload that takes a state when the text lives in a flow.

var number by rememberSaveable { mutableStateOf("") }
val missing = 12 - number.length
PantrixFilledAutoResizeTextField(
    value = number,
    onValueChange = { if (it.all(Char::isDigit)) number = it },
    modifier = Modifier.fillMaxWidth(),
    label = "Customer number",
    supportingText = if (missing == 0) "Number complete" else "Digits to go: $missing",
    maxLength = 12,
    showCharacterCount = false,
    textStyle = PantrixTheme.typography.title,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
    tagKey = "customer_number"
)

A reference code bound to a form

The overload that takes a field gets its value, its error and its name from its field in the form: leave the code empty and move on, and the rule's message takes the place of the supporting text. In a field that is given form, the keyboard's action key submits the form through the "Save reference" button.

The text of a field bound to a form is the value of the form's field: it does not have to be taken separately.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val form = rememberFormState()
PantrixFilledAutoResizeTextField(
    field = form.field("reference", initial = "", Rules.notBlank("Enter a reference code")),
    modifier = Modifier.fillMaxWidth(),
    form = form,
    label = "Reference code",
    supportingText = "The code on the receipt",
    showClearButton = true,
    maxLength = 40,
    keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Characters)
)
PantrixButton(
    text = "Save reference",
    onClick = {
        scope.launch {
            form.submit { snackbars?.show(message = "Reference saved", tagKey = "reference_saved") }
        }
    },
    form = form,
    tagKey = "save_reference"
)

An address of several lines

A delivery address of several lines. The caller holds the address as a String; in this overload the field takes its lines as singleLine, minLines and maxLines. The box is two lines tall and grows to three; once the address passes three lines the box stays as it is, and the text is set as small as it takes to fit, its line spacing with it.

The supporting text is made from the String the caller holds. Its limit is 240 characters; its count is hidden with showCharacterCount = false.

var address by rememberSaveable { mutableStateOf("") }
PantrixFilledAutoResizeTextField(
    value = address,
    onValueChange = { address = it },
    modifier = Modifier.fillMaxWidth(),
    label = "Delivery address",
    supportingText = if (address.isBlank()) "Neighbourhood, street, house number" else "Characters: ${address.length}",
    showClearButton = true,
    maxLength = 240,
    showCharacterCount = false,
    singleLine = false,
    minLines = 2,
    maxLines = 3,
    keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
    tagKey = "delivery_address"
)

Supporting text and error

supportingText stands under the box. error = true outlines the box in the danger colour; the supporting text and the caret turn to that colour too, and a screen reader announces the message as the error. The label keeps its own colour.

The field starts in error: the plate has lowercase letters. Type them again in capitals: the error goes. It is the caller, not the field, that works out the error; here by reading plate.text.

val plate = rememberTextFieldState("34 abc 1903")
val lowercase = plate.text.any(Char::isLowerCase)
PantrixFilledAutoResizeTextField(
    state = plate,
    modifier = Modifier.fillMaxWidth(),
    label = "Licence plate",
    supportingText = if (lowercase) "Type the plate in capitals" else "As written on the registration document",
    error = lowercase,
    showClearButton = true,
    maxLength = 12,
    showCharacterCount = false,
    textStyle = PantrixTheme.typography.title,
    keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Characters),
    tagKey = "plate"
)

Disabled and read-only

enabled = false takes the field's fill away: the field is drawn in the theme's disabled text colour and takes no focus. Its text is still fitted to its box: the IBAN is in the title style, and is set smaller in a box it does not fit.

With readOnly = true the field keeps its fill: it can be focused, and its text selected and copied; it just cannot be changed.

PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("TR330006100519786457841326"),
    modifier = Modifier.fillMaxWidth(),
    enabled = false,
    label = "Saved IBAN",
    supportingText = "Disabled: in the title style, shrunk to fit",
    textStyle = PantrixTheme.typography.title,
    tagKey = "saved_iban"
)
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("PX-2026-0914-TR-000187"),
    modifier = Modifier.fillMaxWidth(),
    readOnly = true,
    label = "Contract number",
    supportingText = "Read-only: it can be selected and copied",
    textStyle = PantrixTheme.typography.title,
    tagKey = "contract_no"
)

Its own colours and style

This field takes the filled text field's FilledTextFieldColors and FilledTextFieldStyle: the filled fields are restyled together.

The first field's fill, label, text, outline and caret are from the app's own purple; the fill with the focus was not given, and is the theme's surface colour. The second's box is fully round: FilledTextFieldStyle(shape = ShapeRole.Full). textStyle is the style of the text, style that of the box.

val own = ownColors()
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("Summer 2026 Collection"),
    modifier = Modifier.fillMaxWidth(),
    label = "List name",
    textStyle = PantrixTheme.typography.title,
    colors = FilledTextFieldColors(
        container = own.container,
        focusedOutline = own.color,
        text = own.onContainer,
        label = own.onContainer,
        cursor = own.color
    ),
    tagKey = "own_colors"
)
PantrixFilledAutoResizeTextField(
    state = rememberTextFieldState("KP-48213"),
    modifier = Modifier.fillMaxWidth(),
    label = "Coupon",
    textStyle = PantrixTheme.typography.title,
    style = FilledTextFieldStyle(shape = ShapeRole.Full),
    tagKey = "pill"
)

API

state

A text field whose label is inside its box and whose text gets smaller to stay in view: an amount, a card's number, a short note, anything that is read whole, in the filled box of PantrixFilledTextField.

ParameterTypeDefaultWhat it is
stateTextFieldStatethe text and the selection. Create it with rememberTextFieldState(), and read what was typed from it: state.text where the text is used, or snapshotFlow { state.text.toString() } to follow it as it changes.
modifierModifierModifierthe modifier of the whole field: its box and what is under it. fillMaxWidth() makes the box as wide as its parent, and a height is one the text is fitted to as it is to the width.
enabledBooleantruewhether the field can be typed into. One that cannot takes no focus, is drawn without its fill in the disabled colours, and says so.
readOnlyBooleanfalsewhether the text can only be read: it can still be focused, selected and copied.
labelString?nullwhat the field is for, written inside the box: the field's name for a screen reader. Without one the text stands in the middle of the box, and the placeholder shows whenever the field is empty.
placeholderString?nullshown where the text goes while the field is empty and its label is out of the way, which is while it has the focus. It is set in the text's style, without the weight a filled field adds to what is typed.
supportingTextString?nullhelp under the box, in a smaller type. With error it is the message that says what to put right, which a screen reader announces as the error.
errorBooleanfalsewhether the text is wrong: the box is outlined in the danger role's colour, the supporting text and the caret turn to it, and a screen reader is told.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the label and the text, in the secondary text colour. A slot does not size what is put in it: PantrixIcon(size = PantrixTheme.style(FilledTextFieldStyle).iconSize) is the platform's size.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text, and after the clear button: a currency's sign after an amount.
showClearButtonBooleanfalseshows a button that empties the field, while the field can be typed into and is not empty. It leaves the caret in the field, and is no stop for Tab.
maxLengthInt?nullthe most characters the user can enter, counted as String.length counts them. An edit that would pass the limit is turned down whole. Text set in code is not cut, and the user can still make it shorter. It has to be positive.
showCharacterCountBooleantrueshows count/maxLength under the box while maxLength is set.
textStyleTextStyle?nullthe style of the text at its largest: a role of the theme's typography, such as PantrixTheme.typography.title for an amount. What it leaves unset is the body's a weight heavier, which is what a filled field's text is set in without it. A box too short for the style grows to hold its label and a line of it.
minFontSizeTextUnitPantrixTheme.typography.footnote.fontSizethe smallest the text gets: a size in sp, or one in em, which is a share of the style's own. It is the theme's footnote size unless told. A text that does not fit at this size scrolls, and at the style's own size or above it the text never shrinks.
lineLimitsTextFieldLineLimitsTextFieldLineLimits.SingleLineone line, or TextFieldLineLimits.MultiLine, in which the text wraps and Enter starts a new line. The box is as tall as its lines are at the text's largest size: it grows from minHeightInLines to maxHeightInLines, and past the most of them the text is set smaller. With no most lines the box grows with its text for as long as it may, and the text is set smaller only where the field is given a height. Tab moves the focus on from either.
keyboardOptionsKeyboardOptionsKeyboardOptions.Defaultthe keyboard's type, its capitals, its corrections and its action key.
onKeyboardActionKeyboardActionHandler?nullcalled for the keyboard's action key, for the action keyboardOptions names. Null keeps what the platform does.
inputTransformationInputTransformation?nullwhat is done to the text as the user types it. With maxLength, it comes first and the limit applies to what it lets through.
outputTransformationOutputTransformation?nullhow the text is shown, without changing it: an amount's separators. The text is fitted as it is shown.
contentTypeContentType?nullwhat the field holds, for the platform to fill it in: ContentType.CreditCardNumber and the like.
styleFilledTextFieldStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsFilledTextFieldColors?nullthis call's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. A disabled field has no fill and is drawn in the theme's disabled text colour whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field: its name where it has no label. A placeholder that says the same is then left to the eye.
tagKeyString?nullthe field's automation key: the field is FilledAutoResizeTextField_<tagKey>, and its clear button FilledAutoResizeTextFieldClear_<tagKey>. The field reports that it took the focus and that it was cleared, and never its text.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted: its focus, its hover and its presses. It keeps a source of its own when this is null.

value

A filled text field whose text shrinks to fit, and whose text is a String its caller holds: value is what it shows, and onValueChange is asked for every change the user makes.

ParameterTypeDefaultWhat it is
valueStringthe text the field shows.
onValueChange(String) -> Unitasked for each change the user makes, with the whole new text, and by the clear button with an empty one.
modifierModifierModifierthe modifier of the whole field: its box and what is under it.
enabledBooleantruewhether the field can be typed into.
readOnlyBooleanfalsewhether the text can only be read: it can still be focused, selected and copied.
labelString?nullwhat the field is for, written inside the box: the field's name for a screen reader.
placeholderString?nullshown where the text goes while the field is empty and its label is out of the way, in the text's size.
supportingTextString?nullhelp under the box. With error it is the message that says what to put right.
errorBooleanfalsewhether the text is wrong: the box is outlined in the danger role's colour, and a screen reader is told.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the label and the text. A slot does not size what is put in it.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text, and after the clear button.
showClearButtonBooleanfalseshows a button that asks for an empty text, while the field can be typed into and is not empty.
maxLengthInt?nullthe most characters the user can enter. An edit that would pass the limit is turned down whole, before onValueChange is asked. A value over the limit is shown as it is, and the user can still make it shorter. It has to be positive.
showCharacterCountBooleantrueshows count/maxLength under the box while maxLength is set.
textStyleTextStyle?nullthe style of the text at its largest. What it leaves unset is the body's a weight heavier.
minFontSizeTextUnitPantrixTheme.typography.footnote.fontSizethe smallest the text gets, in sp or in em: the theme's footnote size unless told. A text that does not fit at this size scrolls.
singleLineBooleantruewhether the field holds one line, which is fitted to the box's width. It does unless told. A line break in value is then shown as a space, and value keeps it.
minLinesInt1the least lines the box is tall, in a field of several, counted at the text's largest size.
maxLinesIntif (singleLine) 1 else Int.MAX_VALUEthe most lines the box grows to, in a field of several, before its text is set smaller.
keyboardOptionsKeyboardOptionsKeyboardOptions.Defaultthe keyboard's type, its capitals, its corrections and its action key.
keyboardActionsKeyboardActionsKeyboardActions.Defaultwhat the keyboard's action key does, for the action keyboardOptions names.
visualTransformationVisualTransformationVisualTransformation.Nonehow the text is shown, without changing it: an amount's separators. The text is fitted as it is shown.
contentTypeContentType?nullwhat the field holds, for the platform to fill it in.
styleFilledTextFieldStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsFilledTextFieldColors?nullthis call's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. A disabled field has no fill and is drawn in the theme's disabled text colour whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field: its name where it has no label.
tagKeyString?nullthe field's automation key: the field is FilledAutoResizeTextField_<tagKey>, and its clear button FilledAutoResizeTextFieldClear_<tagKey>.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted. It keeps a source of its own when this is null.

field

A filled text field whose text shrinks to fit, bound to a form's field: its text, its error and its name come from field.

ParameterTypeDefaultWhat it is
fieldFormField<String>the field the text field shows and changes.
modifierModifierModifierthe modifier of the whole field.
formFormState?nullgiven, the keyboard's action key submits the form through the button registered with it. An action of Next or Previous moves the focus as it always does. A field of one line that names no action gets ImeAction.Done, since the default action never reaches a handler. A field of several lines is left alone: there Enter starts a new line. An onKeyboardAction of the caller's own takes the place of the submit.
enabledBooleantruewhether the field can be typed into.
readOnlyBooleanfalsewhether the text can only be read.
labelString?nullwhat the field is for, written inside the box: the field's name for a screen reader.
placeholderString?nullshown where the text goes while the field is empty and its label is out of the way, in the text's size.
supportingTextString?nullhelp under the box, while the field has no error to show there.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the label and the text.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text, and after the clear button.
showClearButtonBooleanfalseshows a button that empties the field, while it can be typed into and is not empty.
maxLengthInt?nullthe most characters the user can enter. It has to be positive.
showCharacterCountBooleantrueshows count/maxLength under the box while maxLength is set.
textStyleTextStyle?nullthe style of the text at its largest. What it leaves unset is the body's a weight heavier.
minFontSizeTextUnitPantrixTheme.typography.footnote.fontSizethe smallest the text gets, in sp or in em: the theme's footnote size unless told.
lineLimitsTextFieldLineLimitsTextFieldLineLimits.SingleLineone line, or TextFieldLineLimits.MultiLine, whose text is set smaller past the most of its lines. Tab moves the focus on from either.
keyboardOptionsKeyboardOptionsKeyboardOptions.Defaultthe keyboard's type, its capitals, its corrections and its action key.
onKeyboardActionKeyboardActionHandler?nullcalled for the keyboard's action key, in place of the form's submit.
inputTransformationInputTransformation?nullwhat is done to the text as the user types it.
outputTransformationOutputTransformation?nullhow the text is shown, without changing it. The text is fitted as it is shown.
contentTypeContentType?nullwhat the field holds, for the platform to fill it in.
styleFilledTextFieldStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsFilledTextFieldColors?nullthis call's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. A disabled field has no fill and is drawn in the theme's disabled text colour whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field where it has no label.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted. It keeps a source of its own when this is null.

FilledTextFieldStyle

PantrixFilledTextField's box: its shape, how tall and how wide it is at the least, the room around what it holds, its icons and its outline.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe box's shape. A fully round one, ShapeRole.Full, is a pill only while the field holds one line: a field of several lines takes ShapeRole.Large instead, since a capsule's curve runs into the first line of its text.
heightDpDp.UnspecifiedThe least the box is tall.
minWidthDpDp.UnspecifiedThe least the box is wide, which keeps an empty field from growing as the user types into it.
horizontalPaddingDpDp.UnspecifiedThe room between the box's edge and its label and text, where no icon is.
verticalPaddingDpDp.UnspecifiedThe room above the label's line and below the last line of the text.
iconPaddingDpDp.UnspecifiedThe room around an icon in its slot.
iconSizeDpDp.UnspecifiedThe size an icon in a slot should take. A slot does not size what is put in it.
borderWidthDpDp.UnspecifiedHow thick the outline is that the box takes with the focus, in error and under a pointer. At rest it has none.
textRoleTypeRole?nullThe role of the type scale the text is typed in, the label rests in and the placeholder is set in: the body's, 16sp, on Android and iOS, as Material 3 types a field, and the label's, 14sp, on the desktop and the web, as a page under a pointer does.

FilledTextFieldColors

What PantrixFilledTextField is painted with, for a field that does not take the theme's: a colour that is set is used, and one that is left out stays the theme's role. A product gives the colours it changes and no others:

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe box's fill at rest. The theme's neutral container.
focusedContainerColorColor.UnspecifiedThe box's fill under an outline: with the focus, and in error. The theme's surface.
hoveredOutlineColorColor.UnspecifiedThe outline under a pointer. The theme's borderHover.
focusedOutlineColorColor.UnspecifiedThe outline while the field has the focus. The theme's primary.
errorColorColor.UnspecifiedWhat says the text is wrong: the outline, the message under the box, the caret, and the count once it is past its limit. The theme's danger.
textColorColor.UnspecifiedThe typed text. The theme's textPrimary.
labelColorColor.UnspecifiedThe label, at rest in the text's place and on its own line above the text. The theme's textSecondary.
placeholderColorColor.UnspecifiedThe placeholder. The theme's textSecondary.
supportingTextColorColor.UnspecifiedThe help under the box, and the count. The theme's textSecondary.
iconColorColor.UnspecifiedWhat an icon in a slot is drawn in unless it sets a tint of its own, and the clear button's cross. The theme's textSecondary.
cursorColorColor.UnspecifiedThe caret, and with it the handles and the highlight of a selection. The theme's primary.
scrollBarColorColor.UnspecifiedThe bar that shows how far the text of a box of several lines is scrolled. The theme's borderStrong: it is a mark on the box's fill, and a box's soft edge would not be seen there.