What You Will Build

A 6-digit OTP (One-Time Password) input where each digit has its own text field and focus automatically advances to the next field when a digit is entered. When all 6 digits are filled, a verification button becomes enabled. This is the standard pattern for SMS code verification screens.

Why This Pattern Matters

OTP entry is a critical flow in authentication. The auto-advance pattern reduces friction by eliminating manual tab/tap between fields. It teaches you FocusRequester management, keyboard type configuration, and AnimatedVisibility for conditional UI elements.

The OTP Input Screen

@Composable
fun AutoOtpScreen() {
    var otpFields by remember { mutableStateOf(List(6) { "" }) }
    val focusRequesters = remember { List(6) { FocusRequester() } }
    val focusManager = LocalFocusManager.current

    Column(
        modifier = Modifier.fillMaxSize().padding(24.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("Enter OTP Code",
            style = MaterialTheme.typography.headlineSmall,
            fontWeight = FontWeight.Bold)
        Spacer(Modifier.height(8.dp))
        Text("We sent a verification code to your phone",
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSurfaceVariant,
            textAlign = TextAlign.Center)
        Spacer(Modifier.height(32.dp))

        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            otpFields.forEachIndexed { index, value ->
                OutlinedTextField(
                    value = value,
                    onValueChange = { newVal ->
                        if (newVal.length <= 1) {
                            val updated = otpFields.toMutableList()
                            updated[index] = newVal
                            otpFields = updated
                            if (newVal.isNotEmpty() && index < 5) {
                                focusRequesters[index + 1].requestFocus()
                            }
                        }
                    },
                    modifier = Modifier
                        .width(48.dp)
                        .height(56.dp)
                        .focusRequester(focusRequesters[index]),
                    textStyle = LocalTextStyle.current.copy(
                        textAlign = TextAlign.Center,
                        fontWeight = FontWeight.Bold,
                        fontSize = 20.sp
                    ),
                    singleLine = true,
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Number
                    )
                )
            }
        }

        val code = otpFields.joinToString("")
        AnimatedVisibility(visible = code.length == 6) {
            Text("Code: $code",
                color = Color(0xFF4CAF50),
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(top = 16.dp))
        }

        Spacer(Modifier.height(24.dp))
        Button(onClick = {}, enabled = code.length == 6,
            modifier = Modifier.fillMaxWidth()) {
            Text("Verify")
        }
        TextButton(onClick = {}) { Text("Resend Code") }
    }

    LaunchedEffect(Unit) {
        focusRequesters[0].requestFocus()
    }
}

Tips and Pitfalls

  • FocusRequester per field: Create a list of 6 FocusRequesters and attach each to its corresponding OutlinedTextField.
  • Auto-advance logic: On value change, if the new value is not empty and the current index is less than 5, request focus on the next field.
  • LaunchedEffect(Unit) focuses the first field automatically when the screen appears.
  • KeyboardType.Number ensures the numeric keyboard appears, reducing input errors.
  • Handle backspace: For production, detect empty input on a previously filled field and move focus backward.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.