Jetpack Compose OutlinedTextField

A Jetpack Compose OutlinedTextField is the composable Jetpack reaches for whenever a screen needs a text input that reads clearly against any background - a login field, a search box, or a settings form. Jetpack draws a thin border around a Jetpack Compose OutlinedTextField instead of filling it with a solid color, so the field stays legible without needing a background fill behind it. Jetpack Compose developers pick an outlined text field for forms, dialogs, and cards where a filled background would compete with the surrounding surface. Jetpack ships this OutlinedTextField style ready to use out of the box, and Jetpack keeps it behaving the same way on every platform Jetpack Compose targets. Jetpack maintains the OutlinedTextField composable as part of the same library that ships every other Jetpack Compose component, so nothing extra needs installing before an OutlinedTextField shows up on screen.

This blog walks through every parameter the OutlinedTextField composable in Jetpack Compose exposes - value, onValueChange, label, placeholder, leadingIcon, trailingIcon, isError, supportingText, visualTransformation, keyboardOptions, keyboardActions, singleLine, maxLines, minLines, shape, colors, enabled, readOnly, prefix, suffix, textStyle, and interactionSource. Every code sample below is fully self-contained, so you can paste it into an online Kotlin compiler and watch a Jetpack Compose OutlinedTextField behave exactly as Jetpack describes it.

What Is the Jetpack Compose OutlinedTextField Composable

The OutlinedTextField composable is an editable text input that Jetpack Compose draws with a border tracing its outline instead of a solid background fill. A Jetpack Compose OutlinedTextField is stateless by design, meaning the composable never stores the text a user types on its own - it only reports changes upward through onValueChange and expects the caller to hold the actual value. Jetpack ships the OutlinedTextField composable in the material3 package, so a Jetpack Compose OutlinedTextField automatically inherits your app's Material color scheme the moment Jetpack draws it.

Here is the simplest jetpack compose outlinedtextfield example you will run into, showing Jetpack's OutlinedTextField composable with its two required parameters, value and onValueChange:

kotlin
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun BasicOutlinedTextFieldExample() {
    var text by remember { mutableStateOf("") }
    OutlinedTextField(
        value = text,
        onValueChange = { text = it }
    )
}

Running this Jetpack Compose OutlinedTextField example draws a bordered rectangle a user can tap into and type inside, with the border brightening the moment the Jetpack Compose OutlinedTextField gains focus. The text variable lives outside the OutlinedTextField in a remember block, and value is the String currently shown inside the OutlinedTextField while onValueChange is the lambda Jetpack Compose invokes with the updated String on every keystroke. This is why a Jetpack Compose OutlinedTextField is often called a "controlled" input - the OutlinedTextField composable never owns the text, it only displays whatever value you hand back to the Jetpack Compose OutlinedTextField after each onValueChange call. Jetpack also exposes an overload that takes a TextFieldValue instead of a plain String, additionally tracking cursor position for an OutlinedTextField in Jetpack Compose that needs finer control.

Jetpack designed the OutlinedTextField composable so the border and focus ring animate together whenever a user taps in or out of a Jetpack Compose OutlinedTextField. An OutlinedTextField stays this predictable no matter which screen it appears on, because Jetpack keeps the same OutlinedTextField behavior everywhere a Jetpack Compose OutlinedTextField is used. Jetpack documents the OutlinedTextField composable alongside its other Material input fields, so the mental model Jetpack teaches for one Jetpack Compose OutlinedTextField applies the moment Jetpack draws another.

Adding a Label and Placeholder to the OutlinedTextField

The label parameter accepts an optional composable lambda, and it is the piece that makes a Jetpack Compose OutlinedTextField instantly recognizable next to a plain unlabeled box. Jetpack floats this label above the border once a user starts typing, but before that it sits inside the field as a full-size hint, animating between the two positions automatically. The placeholder parameter takes a composable lambda too, but Jetpack only shows it once the field is both focused and empty, rather than at rest - a placeholder inside a compose outlinedtextfield is meant for a short example of the expected format, not a description of the field itself.

kotlin
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun LabeledOutlinedTextField() {
    var phone by remember { mutableStateOf("") }
    OutlinedTextField(
        value = phone,
        onValueChange = { phone = it },
        label = { Text("Phone number") },
        placeholder = { Text("555-123-4567") }
    )
}

This compose outlinedtextfield label example shows "Phone number" centered in the Jetpack Compose OutlinedTextField until a user taps in, at which point the label shrinks and floats above the border while "555-123-4567" appears grayed out inside the now-empty, focused OutlinedTextField. Jetpack breaks the border itself around the floated label on an OutlinedTextField in Jetpack Compose, leaving a small gap so the outline never crosses through the label text. Because label and placeholder serve different moments, most forms pair them together - label stays visible the whole time as a floated caption on a Jetpack Compose OutlinedTextField, while placeholder only appears during that brief empty-and-focused window before disappearing on the first keystroke. Jetpack expects most Jetpack Compose OutlinedTextField instances to define a label, since a field without one loses the floating caption that makes an outlined text field self-explanatory.

Leading and Trailing Icons

The leadingIcon and trailingIcon parameters each accept an optional composable lambda, letting a Jetpack Compose OutlinedTextField place an icon at the start or end of the input row. Jetpack tints both icons using the same color logic it applies to the border, so a leading or trailing icon automatically matches the field's focused, unfocused, or error color, and Jetpack updates that tint the instant focus changes.

kotlin
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun SearchOutlinedTextField() {
    var query by remember { mutableStateOf("") }
    OutlinedTextField(
        value = query,
        onValueChange = { query = it },
        leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
        trailingIcon = { Icon(Icons.Filled.Mic, contentDescription = "Voice search") }
    )
}

Jetpack shows the search icon fixed at the start of this Jetpack Compose OutlinedTextField and the microphone icon fixed at the end, so both slots stay visible the whole time rather than appearing conditionally. Because leadingIcon and trailingIcon are ordinary composable lambdas, a Jetpack Compose OutlinedTextField can swap in any Icon, and Jetpack redraws that icon in the same tint as the border on every focus change. Jetpack reserves layout space for whichever icons are present on an OutlinedTextField in Jetpack Compose, so the input area shrinks slightly without ever overlapping the typed text. Jetpack lets a Jetpack Compose OutlinedTextField define only leadingIcon, only trailingIcon, both, or neither, so an outlined text field never pays for icon space it does not use.

Showing Errors With isError and supportingText

The isError parameter is a Boolean that switches a Jetpack Compose OutlinedTextField into its error styling, turning the border, label, and any icons red to flag invalid input. Pairing isError with supportingText - another optional composable lambda - lets an outlined text field explain exactly what went wrong directly beneath the border.

kotlin
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun ValidatedOutlinedTextField() {
    var age by remember { mutableStateOf("") }
    val isInvalid = age.isNotEmpty() && age.toIntOrNull() == null

    OutlinedTextField(
        value = age,
        onValueChange = { age = it },
        label = { Text("Age") },
        isError = isInvalid,
        supportingText = {
            if (isInvalid) Text("Age must be a number")
        }
    )
}

Typing a non-numeric value into this Jetpack Compose OutlinedTextField flips isInvalid to true, which turns the border and label red while supportingText prints "Age must be a number" underneath the Jetpack Compose OutlinedTextField. Because supportingText is just another composable slot, a Jetpack Compose OutlinedTextField can also show a helper hint even when isError is false, like a character counter on a compose outlinedtextfield that only turns red once a limit is passed. Jetpack never validates the value itself - an OutlinedTextField in Jetpack Compose only reacts to whatever Boolean logic you compute and pass into isError, which is why isError and supportingText appear on nearly every Jetpack Compose OutlinedTextField used inside a real form.

Hiding Input With visualTransformation

The visualTransformation parameter changes how a Jetpack Compose OutlinedTextField renders its value on screen without touching the underlying value string at all. The most common use is PasswordVisualTransformation, which swaps every visible character for a dot while onValueChange still receives the real typed characters underneath.

kotlin
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.PasswordVisualTransformation

@Composable
fun PasswordOutlinedTextField() {
    var password by remember { mutableStateOf("") }
    OutlinedTextField(
        value = password,
        onValueChange = { password = it },
        label = { Text("Password") },
        visualTransformation = PasswordVisualTransformation()
    )
}

Every character typed into this Jetpack Compose OutlinedTextField still lands in the password variable exactly as typed, but PasswordVisualTransformation replaces each one with a dot before Jetpack draws the Jetpack Compose OutlinedTextField on screen. Because visualTransformation only affects rendering, copying text out of a masked Jetpack Compose OutlinedTextField still copies the real characters, not the dots shown on screen. A Jetpack Compose OutlinedTextField can also accept a custom VisualTransformation for formatting input as you type, like inserting dashes into a typed card number, and Jetpack applies that same transformation to every keystroke a Jetpack Compose OutlinedTextField receives.

Controlling the Keyboard With keyboardOptions and keyboardActions

The keyboardOptions parameter configures which on-screen keyboard Jetpack shows for a Jetpack Compose OutlinedTextField, while keyboardActions defines what happens when a user presses the keyboard's action button. Together these two parameters make an outlined text field feel tailored to the exact kind of data it collects, and Jetpack reads both from the same OutlinedTextField call.

  • keyboardType: chooses the keyboard layout, such as Email, Number, Phone, or Password, so an OutlinedTextField in Jetpack Compose shows the right keys immediately.
  • imeAction: sets the label on the keyboard's action button, like Next, Done, or Search, letting a compose outlinedtextfield guide users toward the next step.
  • capitalization: controls automatic capitalization behavior, useful for name or sentence fields.
  • autoCorrect: toggles the platform's autocorrect suggestions on or off for that field.
  • keyboardActions: supplies a lambda such as onNext or onDone that runs when the matching imeAction button is pressed.
kotlin
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType

@Composable
fun EmailKeyboardOutlinedTextField() {
    var email by remember { mutableStateOf("") }
    var submitted by remember { mutableStateOf(false) }

    OutlinedTextField(
        value = email,
        onValueChange = { email = it },
        label = { Text(if (submitted) "Submitted" else "Email") },
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Email,
            imeAction = ImeAction.Done
        ),
        keyboardActions = KeyboardActions(
            onDone = { submitted = true }
        )
    )
}

Focusing this Jetpack Compose OutlinedTextField brings up an email-optimized keyboard with an At sign readily available, since keyboardType is set to Email on the Jetpack Compose OutlinedTextField. Pressing the keyboard's Done button fires the onDone lambda inside keyboardActions, flipping submitted to true and updating the label text on this Jetpack Compose OutlinedTextField immediately. Jetpack keeps keyboardOptions and keyboardActions separate on purpose on every Jetpack Compose OutlinedTextField - one describes the keyboard's appearance, the other describes what happens when its action button is pressed.

Styling the Jetpack Compose OutlinedTextField With Shape and Colors

The shape parameter controls the silhouette of the border itself, defaulting to OutlinedTextFieldDefaults.shape, a small rounded rectangle. The colors parameter accepts a TextFieldColors object built through OutlinedTextFieldDefaults.colors(), letting a Jetpack Compose OutlinedTextField override the border, label, and cursor colors for focused, unfocused, and error states independently.

kotlin
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun StyledOutlinedTextField() {
    var city by remember { mutableStateOf("") }
    OutlinedTextField(
        value = city,
        onValueChange = { city = it },
        label = { Text("City") },
        shape = RoundedCornerShape(20),
        colors = OutlinedTextFieldDefaults.colors(
            focusedBorderColor = MaterialTheme.colorScheme.tertiary,
            focusedLabelColor = MaterialTheme.colorScheme.tertiary
        )
    )
}

Passing RoundedCornerShape(20) rounds every corner of this Jetpack Compose OutlinedTextField's border into a softer pill-like rectangle instead of the default subtle curve. Setting focusedBorderColor and focusedLabelColor to the theme's tertiary color means the border and floated label on this Jetpack Compose OutlinedTextField both switch to that hue the instant the field gains focus, then fade back once it loses focus. Because shape and colors are independent parameters, a Jetpack Compose OutlinedTextField can adopt a whole new visual identity without touching any other parameter, and Jetpack lets every Jetpack Compose OutlinedTextField on a screen share the same shape and colors object for a consistent look.

Line Limits, Enabled State, and Smaller Parameters

A handful of smaller parameters round out what a Jetpack Compose OutlinedTextField composable can do, each useful in narrower situations that don't need a full section of their own.

  • singleLine: a Boolean that collapses an OutlinedTextField in Jetpack Compose to one non-wrapping line and forces maxLines to 1, defaulting to false.
  • maxLines and minLines: Int values that cap and floor how tall a multi-line outlined text field grows as a user types.
  • enabled: a Boolean that turns focus and editing on or off entirely, and Jetpack grays out the whole field when it is false.
  • readOnly: allows focus and text selection but blocks actual editing, unlike enabled which blocks focus too.
  • prefix and suffix: composable lambdas drawn inline before or after the editable text, useful for a fixed currency symbol or unit like "kg".
  • textStyle: a TextStyle controlling the font size, weight, and color of the typed text, separate from label or placeholder styling.
  • interactionSource: a custom MutableInteractionSource exposing raw focus and press events Jetpack normally hides, useful for reacting before onValueChange fires.
kotlin
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun PriceOutlinedTextField() {
    var price by remember { mutableStateOf("") }
    OutlinedTextField(
        value = price,
        onValueChange = { price = it },
        prefix = { Text("$") },
        suffix = { Text("USD") },
        singleLine = true,
        readOnly = false
    )
}

Typing "25" into this Jetpack Compose OutlinedTextField displays it as "$25 USD" on screen, while the price variable itself still holds only "25", since prefix and suffix never touch the underlying value. Setting singleLine to true keeps this Jetpack Compose OutlinedTextField from wrapping onto a second line no matter how much a user types, and leaving readOnly false keeps normal editing enabled on the field. Jetpack rarely asks a developer to touch every one of these parameters on the same Jetpack Compose OutlinedTextField, but each stays available for the screen that needs that extra bit of control from an outlined text field.

Comparing OutlinedTextField Parameters

ParameterTypeDefaultPurpose
valueStringrequiredThe text currently shown inside the OutlinedTextField
onValueChange(String) -> UnitrequiredRuns whenever the typed text changes
label@Composable (() -> Unit)?nullFloating caption shown above the border on focus
placeholder@Composable (() -> Unit)?nullHint shown only when focused and empty
leadingIcon / trailingIcon@Composable (() -> Unit)?nullIcon placed at the start or end of the input row
isErrorBooleanfalseSwitches the field into red error styling
supportingText@Composable (() -> Unit)?nullHelper or error text below the border
visualTransformationVisualTransformationNoneChanges how the value renders without changing it
keyboardOptions / keyboardActionsKeyboardOptions / KeyboardActionsDefaultConfigures the keyboard and its action button
singleLine / maxLines / minLinesBoolean / Intfalse / unlimited / 1Controls line wrapping and height
shapeShapeOutlinedTextFieldDefaults.shapeControls the border's silhouette
colorsTextFieldColorsOutlinedTextFieldDefaults.colors()Sets border, label, and cursor colors per state
enabled / readOnlyBooleantrue / falseControls whether the field can be focused or edited

Reading across this table shows how a Jetpack Compose OutlinedTextField layers state handling, labeling, validation, and styling on top of one core value and onValueChange pair. Jetpack groups every OutlinedTextField parameter the same way regardless of which ones a particular screen actually sets. Once these pieces are familiar, reaching for a Jetpack Compose OutlinedTextField becomes a quick, repeatable decision for any text input a form needs, and Jetpack expects a developer to reuse this same OutlinedTextField knowledge on the very next screen.

Practical Use Case: Building a Login Form With OutlinedTextField

A common pattern places two Jetpack Compose OutlinedTextField instances stacked together - one for an email and one for a password - combining value, label, isError, and visualTransformation into a single working login form.

kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation

@Composable
fun LoginForm() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    val emailInvalid = email.isNotEmpty() && !email.contains("@")

    Column {
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            isError = emailInvalid,
            supportingText = { if (emailInvalid) Text("Enter a valid email") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
    }
}

Both fields in this Jetpack Compose OutlinedTextField form are built from the same composable, yet they behave completely differently because of the parameters passed into each one. The email field validates its own content and shows supportingText the moment emailInvalid turns true, while the password field stays masked behind PasswordVisualTransformation the entire time a user types. Stacking a Jetpack Compose OutlinedTextField this way - matching label and singleLine to what each field actually collects - is how most Compose login screens keep their inputs both usable and consistent; see the Material3 text field guidelines for more on choosing labels and supporting text. Jetpack scales this outlined text field pattern easily - add a third OutlinedTextField for a confirm-password step and the layout logic stays the same, since Jetpack treats every OutlinedTextField in the Column identically.

Complete Jetpack Compose OutlinedTextField Example

This final example pulls several pieces together - value, label, isError, supportingText, and keyboardOptions - into one runnable screen built entirely around the Jetpack Compose OutlinedTextField composable.

kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface {
                    SignupScreen()
                }
            }
        }
    }
}

@Composable
fun SignupScreen() {
    var username by remember { mutableStateOf("") }
    val usernameInvalid = username.isNotEmpty() && username.length < 4

    Column {
        Text(if (usernameInvalid) "Username too short" else "Choose a username")

        OutlinedTextField(
            value = username,
            onValueChange = { username = it },
            label = { Text("Username") },
            isError = usernameInvalid,
            supportingText = { if (usernameInvalid) Text("Must be at least 4 characters") },
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
    }
}

Launching this Jetpack Compose OutlinedTextField example shows a status line above one outlined text field, where typing fewer than four characters flips usernameInvalid to true and turns the border, label, and supportingText red until the username grows long enough. Every piece covered above - value, label, isError, supportingText, and keyboardOptions - comes together in this one screen, the kind of validated Jetpack Compose OutlinedTextField input a real signup screen is meant to build. Jetpack intends every OutlinedTextField built this way to feel consistent, whether it is one field in a signup screen or a dozen scattered across a longer form, so the same value, label, and validation knowledge carries over the next time an OutlinedTextField in Jetpack Compose shows up in a different part of an app.