IME Actions and Software Keyboard Handling in Compose

Quick answer: Set an
ImeActionthat matches the field’s intent, useKeyboardActionsonly when you need custom behavior, and keep focus routing separate from submission logic. Treat the keyboard’s requested layout and action icon as hints: IMEs can choose not to honor them. For long forms, make the focused field visible with IME insets rather than hiding content behind the keyboard.
The action button in the bottom corner of the software keyboard can say—or visually represent—Next, Done, Search, Send, or Go. It is a small detail with a large effect: it tells the user what happens after input and can make a multi-field form feel continuous instead of fragmented.
Choose an action that expresses intent
KeyboardOptions describes the input the field expects. Alongside keyboardType, capitalization, and autocorrect, imeAction asks the IME for an appropriate action button.
| Field purpose | Recommended action | Expected outcome |
|---|---|---|
| An intermediate form field | ImeAction.Next | Advance to the next logical field |
| Final single-line form field | ImeAction.Done | Finish the group or submit after validation |
| Search query | ImeAction.Search | Run the search |
| URL or address | ImeAction.Go | Navigate to the entered target |
| Message composer | ImeAction.Send | Send after the app validates the action |
| Multi-line notes | Usually keep the return key | Insert a new line, not an accidental submit |
The AndroidX ImeAction reference defines these as signals to the keyboard. It is not a guarantee that every installed IME will display the requested icon or send exactly that behavior, so the screen must remain usable with tapping, Tab navigation, and hardware keyboards.
OutlinedTextField(
value = state.email,
onValueChange = { onAction(SignUpAction.EmailChanged(it)) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Email") },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Next,
),
)singleLine = true matters for a form field that should advance. Android’s KeyboardOptions reference notes that a multi-line text field typically displays a return key instead of the requested action.
Build a predictable form flow
Use the built-in next action whenever it matches the form order. Add KeyboardActions when an explicit focus destination or final action is required.
@Composable
fun SignUpFields(
state: SignUpUiState,
onAction: (SignUpAction) -> Unit,
) {
val passwordRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = state.email,
onValueChange = { onAction(SignUpAction.EmailChanged(it)) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Email") },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next,
),
keyboardActions = KeyboardActions(
onNext = { passwordRequester.requestFocus() },
),
)
OutlinedTextField(
value = state.password,
onValueChange = { onAction(SignUpAction.PasswordChanged(it)) },
modifier = Modifier
.fillMaxWidth()
.focusRequester(passwordRequester),
label = { Text("Password") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
onAction(SignUpAction.Submit)
focusManager.clearFocus()
keyboardController?.hide()
},
),
)
}
}This is a UI example: in a production screen, SignUpUiState and SignUpAction belong to the ViewModel, and the UI merely forwards input and submit events. See state hoisting in real Compose screens for that split.
The current KeyboardActions documentation says custom actions can replace a default action. That makes them useful for a deliberate focus route or final event, but it also means you should not install one casually. If the standard Next behavior is correct, leave it alone.
Submit safely on Done
ImeAction.Done should follow the same path as tapping the submit button. It must not bypass validation, duplicate an in-flight request, or make a password field silently disappear before the error is visible.
val canSubmit = state.emailError == null &&
state.passwordError == null &&
!state.isSubmitting
KeyboardActions(
onDone = {
if (canSubmit) {
onAction(SignUpAction.Submit)
} else {
onAction(SignUpAction.ValidateAndFocusFirstError)
}
},
)Let the ViewModel validate again after receiving Submit; the UI’s canSubmit value is feedback, not an authority boundary. Keep an invalid field focused and its message visible so the user knows what to fix. The form validation article covers error timing and accessible supporting text.
For a search field, use the same principle: onSearch should invoke the same state event as the visible search button. Avoid launching a new request directly in the field composable, where request state and cancellation become hard to test.
Hide the keyboard as a consequence, not a ritual
LocalSoftwareKeyboardController.current provides a nullable controller for asking the current software keyboard to show or hide. Android’s API reference calls out hiding it after a completed network or custom Done/Search action.
There are two useful patterns:
// A completed action: clear the focused editor and ask the IME to hide.
focusManager.clearFocus()
keyboardController?.hide()
// A field must be reached after a validation result: keep focus so typing can continue.
emailRequester.requestFocus()Do not treat hide() as a success signal. The controller is nullable and keyboard visibility is ultimately controlled by the system and IME. More importantly, hiding it after an unsuccessful submit interrupts correction. Clear focus and hide the keyboard after a real completion, navigation, or an intentional dismissal.
Keep content above the IME
On a long screen, the keyboard can cover the submit button or the field being edited. Handle IME insets at the scrollable container or bottom action area.
Scaffold(
bottomBar = {
Button(
onClick = { onAction(SignUpAction.Submit) },
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.imePadding()
.padding(16.dp),
) {
Text("Create account")
}
},
) { contentPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.imePadding(),
contentPadding = contentPadding,
) {
// Form fields.
}
}For keyboard-aware scrolling, Android’s IME animation guide documents imePadding() and, where it fits the experience, imeNestedScroll(). Compose handles the normal software-keyboard layout and animation work; add a custom animation only when there is a specific visual requirement. Pair this with the system-bar patterns in Scaffold and window insets.
Hardware keys are not IME actions
KeyboardActions responds to the action button from an IME. A physical keyboard sends key events instead. Text fields already support common editing shortcuts, so avoid intercepting them by default.
When a real shortcut is needed, handle it with onKeyEvent or onPreviewKeyEvent, return true only when consumed, and return false for all other keys. Android’s keyboard command guide documents the default editing shortcuts and event propagation.
Focus traversal deserves its own design. A Next action should go to the next meaningful field, and Tab, Shift+Tab, arrows, and D-pad should still make sense when the IME is not visible. Use the patterns in focus management and keyboard navigation instead of attempting to turn every hardware key into an IME event.
Test the behavior, not one keyboard app
Different keyboards honor configuration differently, so test the outcome across enough input modes:
- Verify the requested action is sensible for each single-line field.
- Trigger Next and ensure focus reaches the expected field.
- Trigger Done on valid and invalid states; assert that both use the same validation/submission flow as the button.
- Rotate the device and use a small-height emulator to confirm the IME does not cover the active field or critical action.
- Attach a hardware keyboard and test Tab, Shift+Tab, Enter, and common copy/paste shortcuts.
Compose UI tests can invoke an IME action and assert the resulting state:
composeTestRule
.onNodeWithText("Password")
.performImeAction()
composeTestRule
.onNodeWithText("Creating account…")
.assertExists()Use test tags or stable semantics when a visible label is dynamic. Test that the event occurred, focus moved when intended, and content remains reachable—not the exact icon a specific keyboard renders.
Common mistakes
Using Done on a multi-line message
Users expect Enter to add a line in notes, comments, and chat drafts. Let the field be multi-line and keep a visible send button unless the product has an unambiguous alternate convention.
Hiding the keyboard before showing an error
This makes correction slower. Keep the relevant field active after a failed validation and move focus only when it helps recovery.
Treating imeAction as guaranteed behavior
It is an IME hint. Always retain a tappable path and keyboard-navigation path.
Placing a bottom button behind the keyboard
Use IME and navigation-bar insets, then test on short windows and with large font sizes.
FAQ
Do I need both KeyboardOptions and KeyboardActions?
No. KeyboardOptions describes the requested keyboard and action. Add KeyboardActions only when the default behavior needs a custom response, such as a particular focus target or a controlled submit event.
Should onDone always hide the software keyboard?
No. Hide it after a completed task or intentional navigation. Keep it visible when the user still needs to correct an error.
Why does my requested action not appear?
The field may be multi-line, or the installed IME may choose a different presentation. Verify the actual action flow rather than depending on an icon.
Summary
Good IME handling communicates the next meaningful action, keeps the focused field reachable, and uses the same state events as visible controls. Request Next, Done, Search, Send, or Go based on the task; customize only where needed; and make the form robust when the keyboard, hardware input, or window size differs from your development device.