
In this article, we’ll explore Material 3 TextField in Jetpack Compose. If you are familiar with XML, it is the EditText.
Prerequisites:
This article talks about Material 3 TextField. If you want the Material 2 version, follow this link.
What is TextField in Jetpack Compose?
It is used to get the data from the user. For example, a signup page that contains a name, phone number, password, etc.
Jetpack Compose provides three TextField APIs:
- TextField
- OutlinedTextField
- BasicTextField
1. TextField:
It is a filled text field. It follows the material design.

2. OutlinedTextField:
It is an outlined text field with a less visual emphasis. It shows a beautiful border around it and follows material design.

3. BasicTextField:
It is a basic composable with no decorations. We have to define everything.

In this article, we’ll play with the TextField API.
First, create an empty Jetpack Compose project and add the following material icon dependency in the app level gradle file.
// replace 1.4.3 with your compose version
implementation "androidx.compose.material:material-icons-extended:1.4.3"
The library size is large. So, if you are using it in production, enable Proguard.
Next, open MainActivity. Create a composable called MyUI() and call it from the onCreate() method. We’ll write our code in it.
// add the following packages
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
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.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourProjectNameTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
MyUI()
}
}
}
}
}
}
@Composable
fun MyUI() {
}
The TextField API looks like this:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors()
)
Don’t get scared of the parameters. Instead of looking at each one, let’s understand them with use cases.
Simple TextField Example:
The value and onValueChange parameters are mandatory for a simple text field.
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
TextField(
value = value,
onValueChange = { newText ->
value = newText
}
)
Text(text = "Input Text: $value")
}
}
Output:

The variable value remembers the text we enter in the TextField. Its data type is String. You can use this to get real-time updates from the TextField.
In the Column layout, we added TextField() and Text() composables. In the TextField(), we are using the two parameters:
value – This is the text to be shown in the field.
onValueChange – It is a lambda that gets called every time the user updates the data in the field. It provides the new (updated) text.
Initially, the value is an empty string. This is why the text field is empty. When the user enters the data, onValueChange is called which contains the new text. We assigned it to our value variable. As a result, the value is updated and shown in the field.
TextField Placeholder and Label:
There are two parameters – label and placeholder.
label – This is an optional text. It is displayed at the top left corner when the text field is in focus and inside the container when the text field is not in focus.
placeholder – An optional text to be displayed when the text field is in focus and the input text is empty.
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") }
)
}
Output:

TextField Icons:
We can add two icons to the text field.
leadingIcon – Placed at the start of the text field container.
trailingIcon – Placed at the end of the text field container.
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
leadingIcon = {
Icon(imageVector = Icons.Default.Email, contentDescription = null)
},
trailingIcon = {
Icon(imageVector = Icons.Default.Person, contentDescription = null)
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") }
)
}
Output:

TextField Keyboard Options and Actions:
There are two parameters for customizing the keyboard – keyboardOptions and keyboardActions.
Keyboard Options:
It is used to change the input type.
class KeyboardOptions constructor(
val capitalization: KeyboardCapitalization = KeyboardCapitalization.None,
val autoCorrect: Boolean = true,
val keyboardType: KeyboardType = KeyboardType.Text,
val imeAction: ImeAction = ImeAction.Default
)
It takes 4 parameters:
1. Capitalization:
We can request the keyboard to capitalize the text. It has multiple options.
KeyboardCapitalization.None – Do not auto-capitalize text.
KeyboardCapitalization.Characters – Capitalize all characters.
KeyboardCapitalization.Words – Capitalize the first character of every word.
KeyboardCapitalization.Sentences – Capitalize the first character of each sentence.
2. Auto Correct:
A boolean value that informs the keyboard whether to enable auto-correct.
3. Keyboard Type:
This is the type of keyboard, for example, text or number pad. Available options:
KeyboardType.Text – Regular keyboard.
KeyboardType.Ascii – Users can enter ASCII characters.
KeyboardType.Number – For digits.
KeyboardType.Phone – For mobile numbers.
KeyboardType.Uri – For URIs.
KeyboardType.Email – For email addresses.
KeyboardType.Password – For passwords.
KeyboardType.NumberPassword – For number passwords.
KeyboardType.Decimal – For decimal numbers. In this case, the keyboard explicitly provides a decimal separator as input, which is not assured by KeyboardType.Number.
4. IME Action:
It signals the keyboard what type of action should be displayed. The Enter key on the keyboard is changed according to the action.
ImeAction.Go – Represents that the user would like to go to the target of the text in the input i.e. visiting a URL.
ImeAction.Search – To execute a search.
ImeAction.Send – To send a text, for example, SMS.
ImeAction.Previous – To go back to the previous text field in a form.
ImeAction.Next – To move to the new line or next text field in a form.
ImeAction.Done – It represents the user done providing the input. For example, the last text field in a form.
ImeAction.None – It represents that no action is expected from the keyboard. The keyboard might choose to show an action which mostly will be a new line.
ImeAction.Default – This is the default. The keyboards will mostly show one of ImeAction.Done or ImeAction.None actions.
Note: It is not guaranteed that the software keyboard will comply with the options provided above. The keyboard may or may not show the requested configuration.
Keyboard Actions:
It is a lambda that gets called when the above imeAction is triggered. Available options:
- onDone
- onGo
- onNext
- onPrevious
- onSearch
- onSend
Example:
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
val contextForToast = LocalContext.current.applicationContext
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
Toast.makeText(
contextForToast,
"OnSearch Click: value = $value",
Toast.LENGTH_SHORT
).show()
}
)
)
}
Output:

In the above output, the keyboard is not closed even after onSearch is triggered. We can fix it in two ways.
The first one is to use SoftwareKeyboardController API. It closes the keyboard without removing the focus from the TextField. We can get its object from LocalSoftwareKeyboardController.
val keyboardController = LocalSoftwareKeyboardController.current
keyboardController?.hide() // call this method to close the keyboard
Example:
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
val contextForToast = LocalContext.current.applicationContext
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
// close the keyboard
keyboardController?.hide()
Toast.makeText(
contextForToast,
"OnSearch Click: value = $value",
Toast.LENGTH_SHORT
).show()
}
)
)
}
Output:

The second method is to use the FocusManager. It closes the keyboard and removes the focus from the TextField.
val focusManager = LocalFocusManager.current
focusManager.clearFocus()
Example:
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
val contextForToast = LocalContext.current.applicationContext
val focusManager = LocalFocusManager.current
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
// close the keyboard and remove the focus from the TextField
focusManager.clearFocus()
Toast.makeText(
contextForToast,
"OnSearch Click: value = $value",
Toast.LENGTH_SHORT
).show()
}
)
)
}
Output:

Password Field:
Let’s create the following password field:

To mask the text, we can use the visualTransformation parameter. Jetpack Compose provides PasswordVisualTransformation out of the box.
class PasswordVisualTransformation(val mask: Char = '\u2022')
Here, mask is the character that replaces the original text.
Here is the complete code:
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
var showPassword by remember {
mutableStateOf(false)
}
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Lock,
contentDescription = "Lock Icon"
)
},
trailingIcon = {
IconButton(onClick = { showPassword = !showPassword }) {
Icon(
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
contentDescription = if (showPassword) "Show Password" else "Hide Password"
)
}
},
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation()
)
}
Output:

TextField Background and Cursor Colors:
TextField() provides colors parameter. We can easily customize the colors by calling the TextFieldDefaults.colors() method. We can change almost any color using it.
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
TextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Green.copy(alpha = 0.2f),
unfocusedContainerColor = Color.Green.copy(alpha = 0.2f),
cursorColor = Color.Green
)
)
}
Output:

OutlinedTextField:
It shows a beautiful border around the container.
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
OutlinedTextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") }
)
}
Output:

It has the same parameters as TextField(). So, you can customize it as we did with TextField(). But there are some exceptions. For example, you have to use OutlinedTextFieldDefaults.colors() to change the colors.
@Composable
fun MyUI() {
var value by remember {
mutableStateOf("")
}
OutlinedTextField(
value = value,
onValueChange = { newText ->
value = newText
},
label = { Text(text = "Name") },
placeholder = { Text(text = "Enter your name") },
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Color.Green.copy(alpha = 0.2f),
unfocusedContainerColor = Color.Green.copy(alpha = 0.2f),
cursorColor = Color.Green
)
)
}
Output:

This is all about the Material 3 TextField in Jetpack Compose. I hope you have learned something new. If you have any doubts, leave a comment below.
Related Articles:
References: