Text Field
Single-line text entry whose accessible name has to come from the field's own label slot rather than a Text beside it. Supporting text and error messages are separate nodes the component does not attach, so both have to be associated deliberately.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when the user types a single line of free text, such as a name, an email address, or a search term.
- Use when the value maps to a system autofill category, so the keyboard can offer to fill it (e.g., a password, a one-time code, a postal code).
- Use when the input has a natural software keyboard tied to its type, such as an email address or a phone number.
Try a different component when
- Do not use when the user types more than one line, such as a message body or a review. The same component takes
singleLine = false, and the multi-line case has its own labeling and scrolling requirements. - Do not use when the value comes from a fixed set rather than being typed (use
select.basic). - Do not use when the value is a date or a time chosen from a picker (use
date-picker.basic). - Do not use when the field is one cell of a segmented code entry (use
pin-input.basic).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- The field reports editable-text semantics and takes its accessible name from its visible label. Material's
TextFieldandOutlinedTextFieldare the reference implementations, supplying that through thelabelslot along with the focus and error states;BasicTextFieldsupplies the editing and none of the rest (global.native-first). - Supply the visible label through the
labelslot. It is what gives the field its accessible name, and it stays on screen as the floating label once the user types. - Set your own
Modifier.semantics { error("...") }with the real message when the field is invalid. Material'sisErrorapplieserror()with a generic default string and does not read the supporting text, soisErroralone announces that something is wrong and never what. - Keep
isError = truealongside it, because that is what drives the visual error treatment and the error color. - Repeat the error message as visible text, through the
supportingTextslot, so it is not available only to TalkBack (global.use-of-color). - Treat
supportingTextas a separate node, not as part of the field. The component lays it out below the field without attaching it, so TalkBack reaches it as its own element after the field rather than as a description of it. Where the hint must be heard with the field, fold it into the label or into acontentDescriptionon the field instead. - Set
keyboardOptionswith theKeyboardTypethat matches the input, so the right software keyboard appears (e.g.,KeyboardType.Email,KeyboardType.Number). - Set
imeActionto match the field's place in the form, and handle it inkeyboardActions, so the keyboard's action key advances or submits instead of inserting a newline. - Set
Modifier.semantics { contentType = ContentType.EmailAddress }or the matching type on any field that maps to an autofill category, so autofill offers the right value. - Announce a validation error rather than moving focus to the field when the error appears after submit. Moving focus loses the user's place in a form (
global.announcements). - Label a trailing icon button, such as a password reveal or a clear-text control, and give it a
stateDescriptionwhen it toggles. It is a real control inside the field and needs its own name. - If the field is unavailable, pass
enabled = falserather than removing the field or making it non-interactive, so it stays in the accessibility tree and reports that it is disabled. - Use
readOnly = truerather thanenabled = falsefor a value the user may read and copy but not change, such as an order number. A disabled field is skipped by some navigation and cannot be selected; a read-only one stays focusable and its text stays reachable. - Meets the touch target baseline in
global_rules.md(global.touch-target-size). - Meets the focus states baseline in
global_rules.md(global.focus-states).
Donts
Avoid these accessibility and UX barriers.
- Do not rely on
isErrorto announce the error message. It announces a generic string; the message the user needs is the one you pass toerror(). - Do not use
placeholderas the field's only label. It is replaced by the typed text and leaves the field unnamed once the user starts typing. - Do not put the label in a
Textabove the field instead of in thelabelslot. It renders identically and leaves the field with no name. - Do not include the control type in the label, such as "Email text field". The component already reports that it is an editable field.
- Do not build the field from
BasicTextFieldwithout adding the label, error, and container semantics yourself. It is the primitive Material decorates, not a finished field. - Do not set
contentDescriptionon a field that already has alabel. It replaces the name rather than adding to it, and the two drift apart. - Do not convey the error state with the red outline alone (
global.use-of-color).
Customizable
Alternatives and options that give the AI agent some room to move.
TextFieldorOutlinedTextFieldis a visual choice. Both carry the same semantics and the same obligations.- The supporting text may carry a hint, a character count, an error message, or nothing.
- A field the user cannot edit may be
readOnlyor disabled. The choice is a semantic one, not a visual one: read-only keeps the value reachable, disabled removes it from the interaction flow. prefix,suffix,leadingIcon, andtrailingIconare available. A decorative leading icon takescontentDescription = null; an interactive trailing one needs a name.- For masked entry,
visualTransformation = PasswordVisualTransformation()with the password content type. The labeling requirements are unchanged, and the reveal control is a labeled toggle.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
@Composable
fun TextFieldExamples() {
var email by remember { mutableStateOf("") }
var submitted by remember { mutableStateOf(false) }
val invalid = submitted && !email.contains("@")
val errorMessage = "Enter an email address that includes an @."
Column {
OutlinedTextField(
value = email,
onValueChange = { email = it },
// The label slot is the accessible name. A Text above the field
// would look the same and leave the field unnamed.
label = { Text("Email") },
singleLine = true,
isError = invalid,
supportingText = {
// Visible copy. The component does not attach this to the
// field, so it is read as its own element after it.
Text(if (invalid) errorMessage else "We use this to send receipts.")
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
modifier = Modifier
.fillMaxWidth()
.semantics {
contentType = ContentType.EmailAddress
// isError on its own announces a generic default string.
// This is what makes TalkBack say what is actually wrong.
if (invalid) error(errorMessage)
}
)
// Password, with a labeled reveal control. The toggle is a real button
// inside the field and carries its own name and state.
var password by remember { mutableStateOf("") }
var revealed by remember { mutableStateOf(false) }
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
singleLine = true,
visualTransformation =
if (revealed) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
trailingIcon = {
IconButton(
onClick = { revealed = !revealed },
modifier = Modifier.semantics {
stateDescription = if (revealed) "Shown" else "Hidden"
}
) {
Icon(
imageVector = if (revealed) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
contentDescription = "Show password"
)
}
},
modifier = Modifier
.fillMaxWidth()
.semantics { contentType = ContentType.Password }
)
}
}