
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| Parameter | Type | Default | Purpose |
|---|---|---|---|
| value | String | required | The text currently shown inside the OutlinedTextField |
| onValueChange | (String) -> Unit | required | Runs whenever the typed text changes |
| label | @Composable (() -> Unit)? | null | Floating caption shown above the border on focus |
| placeholder | @Composable (() -> Unit)? | null | Hint shown only when focused and empty |
| leadingIcon / trailingIcon | @Composable (() -> Unit)? | null | Icon placed at the start or end of the input row |
| isError | Boolean | false | Switches the field into red error styling |
| supportingText | @Composable (() -> Unit)? | null | Helper or error text below the border |
| visualTransformation | VisualTransformation | None | Changes how the value renders without changing it |
| keyboardOptions / keyboardActions | KeyboardOptions / KeyboardActions | Default | Configures the keyboard and its action button |
| singleLine / maxLines / minLines | Boolean / Int | false / unlimited / 1 | Controls line wrapping and height |
| shape | Shape | OutlinedTextFieldDefaults.shape | Controls the border's silhouette |
| colors | TextFieldColors | OutlinedTextFieldDefaults.colors() | Sets border, label, and cursor colors per state |
| enabled / readOnly | Boolean | true / false | Controls 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.
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.
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.
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.
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.