Pantrix UI

Filled Text Field

A text field whose label is inside its box: the label rests where the text will be, and moves up to its own line above the text when the field takes the focus or holds a text.

import com.pantrixui.components.inputs.filledtextfield.PantrixFilledTextField

Examples

At its plainest

The least it takes: a TextFieldState and a label. The label is inside the box: it rests in the text's place while the field is empty and without the focus. Press the field: the label moves up to its own line above the text and makes room for the text. If the focus leaves the field while it is empty, the label goes back to its place.

The typed text is read from the state: the line below shows city.text as it is typed.

val city = rememberTextFieldState()
PantrixFilledTextField(
    state = city,
    modifier = Modifier.fillMaxWidth(),
    label = "City",
    tagKey = "city"
)
PantrixText(
    text = if (city.text.isEmpty()) "No city typed yet" else "Typed: ${city.text}",
    color = PantrixTheme.colors.textSecondary
)

Label and placeholder

The first field is empty: the label rests in the text's place and the placeholder is not shown. Press the field: the label moves up, and the placeholder appears where the label was. A placeholder is not a label; it gives an example of what to type.

The second field holds a text, so its label is always up. The third has no label: the text stands in the middle of the box, the placeholder shows whenever the field is empty, and contentDescription tells a screen reader the field's name.

PantrixFilledTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Website",
    placeholder = "https://example.com",
    tagKey = "website"
)
PantrixFilledTextField(
    state = rememberTextFieldState("Kadıköy"),
    modifier = Modifier.fillMaxWidth(),
    label = "District",
    tagKey = "district"
)
PantrixFilledTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    placeholder = "Search neighbourhoods",
    contentDescription = "Search neighbourhoods",
    tagKey = "neighbourhood"
)

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 outline and the message are what show the error. The fill turns to the surface's colour, as it does with the focus.

The field starts in error. Delete the space: the outline loses the error's colour and the supporting text takes the place of the error message.

val username = rememberTextFieldState("ayşe yılmaz")
val hasSpace = username.text.contains(' ')
PantrixFilledTextField(
    state = username,
    modifier = Modifier.fillMaxWidth(),
    label = "Username",
    supportingText = if (hasSpace) "A username cannot contain spaces" else "Appears in your profile's address",
    error = hasSpace,
    showClearButton = true,
    tagKey = "username"
)

Icon, clear button and action key

leadingIcon stands before the label and the text. A slot does not size what is put in it; the icon takes the size in the filled field's style: PantrixTheme.style(FilledTextFieldStyle).iconSize. While the field is not empty, showClearButton shows the button that empties it: the button leaves the caret in the field, and is no stop for Tab.

keyboardOptions makes the keyboard's action key "Search"; onKeyboardAction is called when that key is pressed. Type something and press the keyboard's "Search" key or Enter: the app's snackbar says what was searched for.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val query = rememberTextFieldState()
PantrixFilledTextField(
    state = query,
    modifier = Modifier.fillMaxWidth(),
    label = "Search branches",
    leadingIcon = {
        PantrixIcon(
            imageVector = PantrixIcons.Search,
            contentDescription = null,
            size = PantrixTheme.style(FilledTextFieldStyle).iconSize
        )
    },
    showClearButton = true,
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
    onKeyboardAction = { scope.launch { snackbars?.show(message = "Branch searched for: ${query.text}") } },
    tagKey = "branch"
)

Input and output transformations

inputTransformation turns an edit down whole if it holds anything that is not a digit; maxLength = 4 applies to what it lets through. outputTransformation draws a slash between the month and the year: the slash is not part of the text.

Type "1228": the box shows "12/28", while expiry.text, which the line below reads, is four digits.

val expiry = rememberTextFieldState()
PantrixFilledTextField(
    state = expiry,
    modifier = Modifier.fillMaxWidth(),
    label = "Expiry date",
    placeholder = "MM/YY",
    maxLength = 4,
    showCharacterCount = false,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
    inputTransformation = DigitsOnly,
    outputTransformation = MonthSlashYear,
    contentType = ContentType.CreditCardExpirationDate,
    tagKey = "expiry"
)
PantrixText(text = "Text held: \"${expiry.text}\"", color = PantrixTheme.colors.textSecondary)

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

private val MonthSlashYear = OutputTransformation {
    if (length > 2) insert(2, "/")
}

A money transfer form

A money transfer form, with the three overloads together. The first two fields are bound to the form: each takes its value, its error and its name from its field in the form; if the focus leaves one while it is empty, the rule's message shows under the box and the box is outlined in the error colour. The amount field holds its text as a String and turns down whatever is not a digit in onValueChange: an edit that is turned down is not shown. Its limit is seven digits; its count is hidden with showCharacterCount = false.

The note is three lines tall: a text that does not fit scrolls inside the box, and the bar at its end shows how far. Its text is in the state created with rememberTextFieldState, and is read from there when the form is sent: the notification reads it with note.text.

In the first three fields the keyboard's "Next" key moves the focus to the next field. Leave the fields empty and press "Send": the form is not sent, and the first field in error takes the focus.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val form = rememberFormState()
var amount by rememberSaveable { mutableStateOf("") }
val note = rememberTextFieldState()
PantrixFilledTextField(
    field = form.field("recipient", initial = "", Rules.notBlank("Enter the recipient's name")),
    modifier = Modifier.fillMaxWidth(),
    form = form,
    label = "Recipient's full name",
    keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Next)
)
PantrixFilledTextField(
    field = form.field("iban", initial = "", Rules.notBlank("Enter the IBAN")),
    modifier = Modifier.fillMaxWidth(),
    form = form,
    label = "IBAN",
    placeholder = "TR00 0000 0000 0000 0000 0000 00",
    showClearButton = true,
    keyboardOptions = KeyboardOptions(
        capitalization = KeyboardCapitalization.Characters,
        imeAction = ImeAction.Next
    )
)
PantrixFilledTextField(
    value = amount,
    onValueChange = { if (it.all(Char::isDigit)) amount = it },
    modifier = Modifier.fillMaxWidth(),
    label = "Amount",
    placeholder = "0",
    supportingText = "Digits only",
    trailingIcon = { PantrixText(text = "TL") },
    maxLength = 7,
    showCharacterCount = false,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
    tagKey = "amount"
)
PantrixFilledTextField(
    state = note,
    modifier = Modifier.fillMaxWidth(),
    label = "Note",
    placeholder = "Rent, dues, debt…",
    maxLength = 140,
    lineLimits = TextFieldLineLimits.MultiLine(minHeightInLines = 3, maxHeightInLines = 3),
    tagKey = "transfer_note"
)
PantrixButton(
    text = "Send",
    onClick = {
        scope.launch {
            form.submit {
                // The note is read from the state at the moment it is used.
                val sent = if (note.text.isBlank()) "Money sent" else "Money sent: ${note.text}"
                snackbars?.show(message = sent, tagKey = "transfer_sent")
            }
        }
    },
    form = form,
    tagKey = "send_transfer"
)

A field given a height

A height given to the field is the whole field's: Modifier.height(140.dp). The box takes what the supporting text under it leaves of that height; the label and the text stay inside the box, a text that does not fit scrolls there, and the bar at its end shows how far.

PantrixFilledTextField(
    state = rememberTextFieldState(COMPLAINT),
    modifier = Modifier.fillMaxWidth().height(140.dp),
    label = "Your complaint",
    supportingText = "The field's height is fixed: the text scrolls inside the box",
    lineLimits = TextFieldLineLimits.MultiLine(),
    tagKey = "complaint"
)

private const val COMPLAINT = "The same payment was taken from my card twice.\n" +
    "The first was on the third of the month, the second on the fourth.\n" +
    "Both are for the same amount and from the same merchant.\n" +
    "I have both receipts.\n" +
    "I would like one of them refunded."

Disabled and read-only

enabled = false takes the field's fill away: a disabled field is drawn in the theme's disabled text colour on the ground it stands on, takes no focus, and a screen reader is told that it is disabled. This is so whatever colors gives: a disabled field looks the same everywhere in an app.

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

PantrixFilledTextField(
    state = rememberTextFieldState("TL current account - 1042"),
    modifier = Modifier.fillMaxWidth(),
    enabled = false,
    label = "Sending account",
    supportingText = "Cannot be changed in this transaction",
    tagKey = "account"
)
PantrixFilledTextField(
    state = rememberTextFieldState("TR33 0006 1005 1978 6457 8413 26"),
    modifier = Modifier.fillMaxWidth(),
    readOnly = true,
    label = "Your IBAN",
    supportingText = "Read-only: it can be selected and copied",
    tagKey = "own_iban"
)

Its own colours

The colours given with colors are used, and the ones left out stay the theme's roles. The first field takes its colours from the theme's info role. At rest the fill is the role's soft tone; the label, the text and the icon are in the colour that reads on that tone. With the focus and under a pointer the outline is the role's own colour. The fill with the focus, the placeholder, the supporting text and the error colour are not given: they stay as the theme has them.

The second field does the same with the app's own purple: a colour that is in none of the theme's roles. Press the field: the outline and the caret are purple, and with the focus the fill turns to the theme's surface colour.

val info = PantrixTheme.colors.info
PantrixFilledTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Referral code",
    placeholder = "PX-0000",
    supportingText = "The code of the person who invited you, if any",
    showClearButton = true,
    colors = FilledTextFieldColors(
        container = info.container,
        hoveredOutline = info.color,
        focusedOutline = info.color,
        text = info.onContainer,
        label = info.onContainer,
        icon = info.onContainer,
        cursor = info.color
    ),
    tagKey = "reference"
)
val own = ownColors()
PantrixFilledTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Promo code",
    colors = FilledTextFieldColors(
        container = own.container,
        hoveredOutline = own.color,
        focusedOutline = own.color,
        text = own.onContainer,
        label = own.onContainer,
        cursor = own.color
    ),
    tagKey = "own_colors"
)

Its own style

style is this call's own style: only the fields it sets change, and the rest comes from the theme's and the platform's style.

The first field's box is fully round: FilledTextFieldStyle(shape = ShapeRole.Full). The second has small corners and, with borderWidth = 2.dp, a thick outline. The outline shows with the focus, in error and under a pointer; at rest the box has none.

PantrixFilledTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Fully round box",
    style = FilledTextFieldStyle(shape = ShapeRole.Full),
    tagKey = "pill"
)
PantrixFilledTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Small corners, thick outline",
    style = FilledTextFieldStyle(shape = ShapeRole.Small, borderWidth = 2.dp),
    tagKey = "small_corners"
)

API

state

A text field whose label is inside its box: the label rests where the text will be, and moves up to its own line above the text when the field takes the focus or holds a text.

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. A height is the whole field's as well: the box fills what the supporting text leaves of it, with one line in its middle and several from its top, and a text too long for the box scrolls in it.
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.
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.
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.
lineLimitsTextFieldLineLimitsTextFieldLineLimits.SingleLineone line, or TextFieldLineLimits.MultiLine: the box is as tall as its least lines, grows to its most, and scrolls from there. 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.
contentTypeContentType?nullwhat the field holds, for the platform to fill it in: ContentType.EmailAddress 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 FilledTextField_<tagKey>, and its clear button FilledTextFieldClear_<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 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.
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.
singleLineBooleantruewhether the field holds one line, which scrolls sideways. It does unless told otherwise. 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.
maxLinesIntif (singleLine) 1 else Int.MAX_VALUEthe most lines the box grows to before its text scrolls, in a field of several.
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: PasswordVisualTransformation() draws a dot for each character.
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 FilledTextField_<tagKey>, and its clear button FilledTextFieldClear_<tagKey>.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted. It keeps a source of its own when this is null.

field

A filled text field 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.
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.
lineLimitsTextFieldLineLimitsTextFieldLineLimits.SingleLineone line, or TextFieldLineLimits.MultiLine. 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.
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.

Accessibility