A customizable, animated One-Time Password (OTP) input component for SwiftUI that supports rounded borders, underlines, shake animation (invalid otp), blur text replacement (iOS 17+), and more..
Easily handle 2FA/OTP flows with flexible visual styles and a developer-friendly API.
- Underlined and Rounded Border styles
- Animates on invalid OTP input
- Fully customizable (font, spacing, border, colors)
-
@Bindingsupport for OTP text and state - Focus and typing state handling out of the box
You can use Swift Package Manager:
.package(url: "https://github.com/huyparody/OTPTextField.git", from: "1.0.0")β’ The shake animation and blur text replacement are only available on iOS 17+
β’ The rest of the features are compatible with iOS 15+
β’ Built with SwiftUI
struct ContentView: View {
@State private var text = ""
@State private var typingState: TypingState = .typing
var body: some View {
OTPTextField(
otpMaxDigit: 6,
style: .underlined,
text: $text,
typingState: $typingState,
onComplete: {
print("OTP input complete: \(text)")
}
)
.padding()
}
}style(.underlined)Visually shows a line under each digit box.
style(.roundedBorder)Each digit is inside a rounded rectangle.
Use OTPTextFieldOptions to customize colors, font, spacing, and sizing:
| Property | Type | Default Value | Description |
|---|---|---|---|
borderColor |
Color |
.gray |
The border color in the normal state. |
borderCornerRadius |
CGFloat |
10 |
Corner radius for rounded border style. |
borderWidth |
CGFloat |
1.2 |
Border thickness. |
fillColor |
Color |
.clear |
Background fill color for each OTP field (used with .roundedBorder). |
underlineHeight |
CGFloat |
1 |
Height of the underline (used with .underlined). |
activeBorderColor |
Color |
.blue |
Border color when the field is focused. |
invalidColor |
Color |
.red |
Border color used when the input is invalid. |
textFieldHeight |
CGFloat |
50 |
Height of each OTP field box. |
textFont |
Font |
.system(size: 20, weight: .medium) |
Font used for OTP characters. |
textColor |
Color |
.primary |
Text color for OTP input. |
underlineSpacing |
CGFloat |
16 |
Spacing between underline segments (used with .underlined). |
roundedCornerSpacing |
CGFloat |
6 |
Spacing between boxes (used with .roundedBorder). |
Trigger a visual "shake" or invalid animation when OTP is incorrect:
typingState = .invalidTo reset:
typingState = .typinginvalid.mp4
enum TypingState {
case typing
case invalid
}struct ContentView: View {
@State private var text = ""
@State private var typingState: TypingState = .typing
@State private var selectedStyle: OTPTextFieldStyle = .underlined
@State private var simulateInvalidOTP = false
let options = OTPTextFieldOptions(borderColor: .red,
activeBorderColor: .green,
textFont: .system(size: 24, weight: .bold),
textColor: .gray)
var body: some View {
VStack(spacing: 24) {
Picker("Style", selection: $selectedStyle) {
ForEach(OTPTextFieldStyle.allCases) { style in
Text(style.rawValue.capitalized).tag(style)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
OTPTextField(
otpMaxDigit: 6,
style: selectedStyle,
options: options,
text: $text,
typingState: $typingState,
onComplete: {
print("Trigger validate otp")
}
)
.padding(.horizontal)
Toggle("Simulate OTP invalid", isOn: $simulateInvalidOTP)
.padding()
.onChange(of: simulateInvalidOTP) { isOn in
if isOn {
text = (0..<6).map { _ in "\(Int.random(in: 0...9))" }.joined()
typingState = .invalid
} else {
text = ""
typingState = .typing
}
}
}
.padding(.vertical)
}
}OTPTextField is released under an MIT license.