A simple and customizable OTP (One-Time Password) input component for Android built with Jetpack Compose. Features include focus states, error handling with shake animation, auto-complete callback, and full customization support.
📱 Try the Demo App on Play Store or check out the screenshots below!
- ✅ 6-Digit OTP Input - Clean and intuitive input interface
- ✅ Visual Feedback - Focus states and error states with shake animation
- ✅ Auto-Complete Callback - Automatic trigger when all digits are entered
- ✅ Error Handling - Built-in error message display with shake animation
- ✅ Focus Management - Programmatic focus control with
FocusRequester - ✅ Customizable - Custom spacing, colors, and styling
- ✅ i18n Support - Support for string resources and internationalization
- ✅ Material Design 3 - Follows Material Design guidelines
Add the dependency to your build.gradle.kts (or build.gradle):
dependencies {
implementation("com.github.erikgunawan:otpplus:1.0.0")
}Or if using Groovy:
dependencies {
implementation 'com.github.erikgunawan:otpplus:1.0.0'
}Add JitPack to your project's settings.gradle.kts (or settings.gradle):
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") } // Add this
}
}Or if using Groovy:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' } // Add this
}
}- Min SDK: 24 (Android 7.0)
- Compile SDK: 36
- Kotlin: 2.0.21+
- Jetpack Compose: 1.5.0+
import id.faazlab.otpplus.OTPPlus
import id.faazlab.otpplus.UiText
@Composable
fun OTPInputScreen() {
var otpValue by remember { mutableStateOf("") }
OTPPlus(
value = otpValue,
onValueChanged = { newValue ->
otpValue = newValue
}
)
}@Composable
fun OTPInputWithError() {
var otpValue by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<UiText?>(null) }
OTPPlus(
value = otpValue,
errorMessage = errorMessage,
onValueChanged = { newValue ->
otpValue = newValue
errorMessage = null // Clear error when user types
},
onComplete = {
// Validate OTP when 6 digits are entered
if (otpValue != "123456") {
errorMessage = UiText.DynamicString("Invalid OTP")
}
}
)
}@Composable
fun OTPInputWithAutoComplete() {
var otpValue by remember { mutableStateOf("") }
OTPPlus(
value = otpValue,
onValueChanged = { newValue ->
otpValue = newValue
},
onComplete = {
// This callback is automatically called when 6 digits are entered
// You can use this for auto-submit or immediate validation
validateOTP(otpValue)
}
)
}Use FocusRequester for programmatic focus control:
@Composable
fun OTPInputWithFocus() {
var otpValue by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Column {
OTPPlus(
value = otpValue,
focusRequester = focusRequester,
onValueChanged = { newValue ->
otpValue = newValue
}
)
Button(onClick = { focusRequester.requestFocus() }) {
Text("Focus OTP Input")
}
}
}Customize the spacing between OTP boxes:
@Composable
fun OTPInputWithCustomSpacing() {
var otpValue by remember { mutableStateOf("") }
OTPPlus(
value = otpValue,
innerSpace = 12.dp, // Default is 8.dp
onValueChanged = { newValue ->
otpValue = newValue
}
)
}Use UiText.StringResource for internationalization:
@Composable
fun OTPInputWithStringResource() {
var otpValue by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<UiText?>(null) }
OTPPlus(
value = otpValue,
errorMessage = errorMessage,
onValueChanged = { newValue ->
otpValue = newValue
errorMessage = null
}
)
// Set error using string resource
Button(onClick = {
errorMessage = UiText.StringResource(R.string.otp_error_invalid)
}) {
Text("Validate")
}
}@Composable
fun CompleteOTPExample() {
var otpValue by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<UiText?>(null) }
var isSuccess by remember { mutableStateOf(false) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OTPPlus(
value = otpValue,
errorMessage = errorMessage,
onValueChanged = { newValue ->
otpValue = newValue
errorMessage = null
isSuccess = false
},
onComplete = {
// Auto-validate when 6 digits are entered
validateOTP(otpValue)
}
)
if (isSuccess) {
Text(
text = "✓ OTP successfully validated!",
color = MaterialTheme.colorScheme.primary
)
}
Button(
onClick = {
when {
otpValue.isEmpty() -> {
errorMessage = UiText.DynamicString("Please enter OTP")
}
otpValue.length < 6 -> {
errorMessage = UiText.DynamicString("OTP must be 6 complete digits")
}
else -> {
validateOTP(otpValue)
}
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Submit OTP")
}
}
}
fun validateOTP(otp: String) {
// Your validation logic here
if (otp == "123456") {
isSuccess = true
errorMessage = null
} else {
isSuccess = false
errorMessage = UiText.DynamicString("Invalid OTP")
}
}@Composable
fun OTPPlus(
modifier: Modifier = Modifier,
value: String = "",
errorMessage: UiText? = null,
innerSpace: Dp = 8.dp,
focusRequester: FocusRequester? = null,
onValueChanged: (String) -> Unit = {},
onComplete: (() -> Unit)? = null
)| Parameter | Type | Default | Description |
|---|---|---|---|
modifier |
Modifier |
Modifier |
Modifier for customization of the OTP container |
value |
String |
"" |
Current OTP value (should be 0-6 digits) |
errorMessage |
UiText? |
null |
Error message to display below the OTP fields (optional) |
innerSpace |
Dp |
8.dp |
Spacing between individual OTP digit boxes |
focusRequester |
FocusRequester? |
null |
FocusRequester for programmatic focus control (optional) |
onValueChanged |
(String) -> Unit |
{} |
Callback when the OTP value changes, providing the new value |
onComplete |
(() -> Unit)? |
null |
Callback when all 6 digits are entered (optional) |
UiText is a sealed class for handling different types of UI text with i18n support:
sealed class UiText {
data class DynamicString(val value: String) : UiText()
data class Combined(val parts: List<UiText>) : UiText()
class StringResource(
@StringRes val resId: Int,
vararg val args: Any
) : UiText()
@Composable
fun asString(): String
fun asString(context: Context): String
}// Dynamic string
val error1 = UiText.DynamicString("Invalid OTP")
// String resource
val error2 = UiText.StringResource(R.string.otp_error)
// Combined
val error3 = UiText.Combined(
listOf(
UiText.StringResource(R.string.error_prefix),
UiText.DynamicString("Invalid OTP")
)
)Try the OTP+ demo app directly on your device! The demo showcases various usage scenarios:
- Positive Scenario - Successful OTP validation
- Error Scenario - Error handling with shake animation
- Empty Validation - Validation for empty or incomplete OTP
- Auto-Complete Callback - Automatic callback when 6 digits are entered
- Focus Management - Programmatic focus control
- Custom Spacing - Customizable spacing between boxes
The demo app showcases all 6 usage scenarios in a clean, card-based interface.
Successfully validated OTP with visual feedback and success message.
Error handling with shake animation and error message display.
Validation for empty or incomplete OTP input.
Automatic callback trigger when 6 digits are entered.
Note: A demo video is also available showing the component in action. The video demonstrates all scenarios including animations and interactions.
You can also build and run the demo app locally:
./gradlew :otpplus-demo:installDebugOr install directly:
./gradlew :otpplus-demo:assembleDebug
adb install otpplus-demo/build/outputs/apk/debug/otpplus-demo-debug.apkThe library uses predefined colors in OTPPlusColor (internal implementation):
Error- Error state colorFocused- Focused box border colorBoxDefault- Default box backgroundBoxFocused- Focused box backgroundBorderDefault- Default border colorTextDefault- Text color
To customize colors, you can create your own color scheme or modify the library source.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Copyright 2025 Erik Gunawan
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Erik Gunawan
- GitHub: @erikgunawan
- Repository: otp-plus
- Built with Jetpack Compose
- Follows Material Design 3 guidelines
⭐ If you find this library useful, please consider giving it a star!




