Kotlin Multiplatform Help

Tab navigation and keyboard focus

In Compose Multiplatform for desktop, you can set up navigation between components with the Tab keyboard shortcut for the next component and Shift+Tab for the previous one.

Default tab navigation

By default, tab navigation allows users to move between focusable components in the order of their appearance. This functionality is enabled by default and doesn't require any additional code.

Focusable components include anything that uses the clickable(), selectable(), toggleable(), or focusable() modifiers in its implementation. For example, text fields, buttons, sliders, navigation items, selection controls with a non-null callback, onClick overloads of Card(), Surface(), and ListItem().

Here is a window where the user can navigate between five text fields using standard shortcuts:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 500.dp)) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { repeat(5) { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) } } } } }
Default tab order

Custom focusable components

To include a component that isn't focusable by default in the tab order, apply the focusable() modifier.

To change the appearance of the component when it receives focus, pass a MutableInteractionSource to the focusable() modifier, read the focus state from it with collectIsFocusedAsState(), and use that state to change the style of the component: a different background, a border, or any other highlight. To make the component react to keyboard presses, handle key events with the onKeyEvent() modifier.

The following example turns a Box() composable into a button-like component. The box is highlighted when focused, and pressing Enter or Space triggers the related action:

import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme 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.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 450.dp)) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( primary = Color(10, 132, 232), secondary = Color(150, 232, 150) ) ) { var clicks by remember { mutableStateOf(0) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(40.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { Text(text = "Clicks: $clicks") repeat(5) { index -> FocusableBox("Button ${index + 1}", onClick = { clicks++ }) } } } } } } @OptIn(ExperimentalComposeUiApi::class) @Composable fun FocusableBox( text: String = "", onClick: () -> Unit = {}, size: DpSize = DpSize(200.dp, 35.dp) ) { var isKeyPressed by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() val backgroundColor = when { isFocused && isKeyPressed -> lerp(MaterialTheme.colorScheme.secondary, Color(64, 64, 64), 0.3f) isFocused -> MaterialTheme.colorScheme.secondary else -> MaterialTheme.colorScheme.primary } Box( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(backgroundColor) .size(size) .onPointerEvent(PointerEventType.Press) { onClick() } .onKeyEvent { if (it.key == Key.Enter || it.key == Key.Spacebar) { when (it.type) { KeyEventType.KeyDown -> isKeyPressed = true KeyEventType.KeyUp -> { isKeyPressed = false onClick() } } } false } .focusable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text(text = text, color = Color.White) } }
A custom focusable component

Custom tab order

To move focus in an order other than the order of appearance, combine two modifiers:

  • focusRequester() attaches a FocusRequester handle to a focusable component. If the component is not focusable by default, apply the focusable() modifier after focusRequester().

  • focusProperties() sets the next and previous elements in the tab order: components with a FocusRequester handle that are focused by pressing Tab or Shift+Tab.

The following example creates a FocusRequester for each of the five text fields and reverses the default tab order:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 500.dp)) ) { val focusRequesters = remember { List(5) { FocusRequester() } } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { focusRequesters.forEachIndexed { index, focusRequester -> TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier .focusRequester(focusRequester) .focusProperties { // Reverses the default order: next = focusRequesters[(index - 1 + focusRequesters.size) % focusRequesters.size] previous = focusRequesters[(index + 1) % focusRequesters.size] } ) } } } } }
Custom tab order

Moving focus from code

To bring a component into focus without user interaction, attach FocusRequester to a focusable component using the focusRequester() modifier and call FocusRequester.requestFocus(). If the component is not focusable by default, the focusable() modifier should be applied after focusRequester().

In the following example, a button moves the focus to a text field and back to itself:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.TextField 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.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 450.dp)) ) { val buttonFocusRequester = remember { FocusRequester() } val textFieldFocusRequester = remember { FocusRequester() } var isTextFieldFocused by remember { mutableStateOf(false) } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { Button( onClick = { isTextFieldFocused = !isTextFieldFocused if (isTextFieldFocused) { textFieldFocusRequester.requestFocus() } else { buttonFocusRequester.requestFocus() } }, modifier = Modifier .fillMaxWidth() .focusRequester(buttonFocusRequester) ) { Text(text = "Focus switcher") } TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier.focusRequester(textFieldFocusRequester) ) } } } }
Moving focus from code

Focusing a component when it appears

Forms and dialogs commonly focus their first input right away, so the user can start typing without reaching for the mouse. In this use case, request focus from a LaunchedEffect(Unit) block, which runs once after the component enters the composition.

In the following example, the first text field is focused as soon as the window opens:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application fun main() = application { Window( onCloseRequest = ::exitApplication, state = WindowState(size = DpSize(350.dp, 300.dp)) ) { val focusRequester = remember { FocusRequester() } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine, modifier = Modifier.focusRequester(focusRequester) ) TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) } } LaunchedEffect(Unit) { focusRequester.requestFocus() } } }
Focus text field on appearance

Moving focus from multiline text fields

In multiline text fields, pressing Tab inserts a tab character instead of moving the focus to the next component:

Column { repeat(5) { TextField( state = rememberTextFieldState("Hello, World!"), // MultiLine is the default value of lineLimits lineLimits = TextFieldLineLimits.MultiLine(), modifier = Modifier.padding(8.dp) ) } }

This is a known issue, CMP-5822. It affects any text field that accepts more than one line, which is the default behavior. As a workaround, intercept the Tab key with the onPreviewKeyEvent modifier and move the focus using the FocusManager from LocalFocusManager:

import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.isShiftPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Multiline text fields") { Column { repeat(5) { TextField( state = rememberTextFieldState("Hello, World!"), lineLimits = TextFieldLineLimits.MultiLine(), modifier = Modifier.padding(8.dp).moveFocusOnTab() ) } } } @Composable fun Modifier.moveFocusOnTab(): Modifier { val focusManager = LocalFocusManager.current return onPreviewKeyEvent { if (it.type == KeyEventType.KeyDown && it.key == Key.Tab) { focusManager.moveFocus( if (it.isShiftPressed) FocusDirection.Previous else FocusDirection.Next ) true } else { false } } }

What's next

31 August 2026