A polished segmented OTP / PIN input for Compose Multiplatform — N boxes, auto-advancing focus, configurable styling, and the feature most OTP components miss: clipboard paste that works on every platform.
Most OTP components are a row of single-character text fields glued together with focus hacks —
and pasting a code from your SMS or password manager either does nothing or only fills the first
box. OtpField is built on a single underlying text field, so one paste fills every box on
Android, iOS, Desktop and Web — and auto-advance, backspace and hardware keyboards all just work.
| Platform | Supported | Tested |
|---|---|---|
| Android | ✅ | ✅ (unit + UI) |
| iOS | ✅ | ✅ (UI, Skiko) |
| Desktop | ✅ | ✅ (unit + UI) |
| Web | ✅ | ✅ (compile + logic) |
gradle/libs.versions.toml:
[libraries]
otp-field = { module = "io.github.nadeemiqbal:otp-field", version = "0.1.0" }commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.otp.field)
}
}
}OtpField(
length = 6,
onComplete = { otp -> viewModel.verify(otp) },
)onComplete fires exactly once, with the full code, the moment the last box is filled — whether
the user typed it or pasted it.
State-hoisted — read, reset or pre-fill the value yourself:
var otp by remember { mutableStateOf("") }
OtpField(
value = otp,
onValueChange = { otp = it },
length = 6,
onComplete = { viewModel.verify(it) },
)Pick a style
OtpField(
length = 4,
onComplete = { ... },
style = OtpFieldStyle.Boxed, // Boxed | Underlined | Rounded
)PIN / obscure mode — mask digits, with a brief reveal of the last one typed:
OtpField(
length = 4,
onComplete = { ... },
obscureChar = '•',
obscureCharShownDelay = 500.milliseconds,
)Error state with shake
OtpField(
length = 6,
onComplete = { ... },
isError = errorState,
errorMessage = "Invalid code",
)Sizing & keyboard
OtpField(
length = 6,
onComplete = { ... },
boxSize = DpSize(48.dp, 56.dp),
boxSpacing = 8.dp,
keyboardType = KeyboardType.NumberPassword, // Number | NumberPassword | Ascii
)style—Boxed,UnderlinedorRounded.shape— overrides the box shape (ignored byUnderlined).boxSize/boxSpacing— size of each box and the gap between them.colors— start fromOtpFieldDefaults.colors()and.copy(...)what you need (border, focused border, error border, cursor, text, disabled and error-text colors).keyboardType— numeric types (Number,NumberPassword,Decimal,Phone) restrict input to digits; other types accept any non-whitespace character. This same filter is what rejects invalid pastes.obscureChar/obscureCharShownDelay— PIN masking and how long the last digit stays visible before it's hidden.isError/errorMessage— error styling, shake animation and an optional message.enabled—falsemakes the field inert and muted.
OtpField is backed by one hidden text field rather than N separate ones, so the platform's
own paste action delivers the whole string in a single edit:
- Exactly
lengthcharacters → fills every box and firesonComplete. - Longer than
length→ truncated tolength. - Contains an invalid character (e.g. letters with
KeyboardType.Number) → the whole paste is rejected; the field is never left half-filled.
- iOS — the system keyboard stays attached to the single underlying field, so it doesn't flicker as focus "moves" between boxes the way an N-field implementation would.
- Android — works across IMEs; auto-advance is driven by value length, not per-field focus hops, which avoids IME-specific focus quirks.
- Desktop — full hardware-keyboard support: type to fill, Backspace to go back, Cmd/Ctrl+V to paste.
- Web (wasmJs) — a single focusable element means tab-focus and browser paste behave predictably; no focus-stealing between sub-fields.
| OtpField | N separate TextFields |
A single plain TextField |
|
|---|---|---|---|
| Segmented box visuals | ✅ 3 styles | ❌ | |
| One paste fills all boxes | ✅ every platform | ❌ usually only box 1 | ✅ |
| Auto-advance / auto-back | ✅ | n/a | |
| Obscure / PIN mode | ✅ with brief reveal | ||
| Error shake animation | ✅ | ❌ DIY | ❌ DIY |
| Hardware keyboard / backspace | ✅ | ✅ | |
| Multiplatform | ✅ Android/iOS/Desktop/Web | ✅ | ✅ |
If you just need a code field and don't care about the segmented look, a plain TextField is
genuinely fine. The moment you want the boxed OTP look and reliable paste, hand-rolling N
fields is where people get stuck — that's the gap this library fills.
- Optional auto-submit delay after the last digit
- Per-box enter/exit animations
- Optional haptic feedback on completion (Android/iOS)
See CONTRIBUTING.md. Bug reports and feature requests are welcome via GitHub Issues.
Copyright 2026 Nadeem Iqbal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
See LICENSE for the full text.