flutter_smart_otp
A highly customizable, production-ready OTP / PIN input field for Flutter.
SmartOtpField renders a row of individual input boxes that behave like a
single logical text field — with auto-focus, backspace navigation, full paste
support, validation, and rich styling — while remaining fully accessible and
leak-free.
Auto-advance • Auto-back on backspace • Paste anywhere • Obscure text • Validation • RTL/LTR • Fully custom styling
Features
- ✅ Custom OTP length (any number of boxes)
- ✅ Auto-focus to the next field as the user types
- ✅ Auto-focus to the previous field on backspace
- ✅ Paste a complete code into any box — it is distributed across all boxes
- ✅ Built-in validation with
validatorandAutovalidateMode - ✅ Obscure text support (PIN-style entry) with a custom obscuring character
- ✅ Enable / disable state
- ✅ Read-only mode
- ✅ Keyboard type customization
- ✅
TextInputFormattersupport - ✅ Full cursor customization (color, width, height, radius, visibility)
- ✅ Custom text style
- ✅ Custom border, border color, focused border color, error border color, disabled border color, border width, and border radius
- ✅ Advanced
borderBuilder/decorationBuilderhooks for total visual control (gradients, shadows, custom shapes) - ✅ Box width, height, and spacing customization
- ✅ Autofocus
- ✅ Custom
FocusNodelist support - ✅ Custom
TextEditingControllerlist support - ✅ Right-to-left (RTL) and left-to-right (LTR) layout support
- ✅ Responsive layout
- ✅ Leak-free state management with correct disposal of internally-created resources only
- ✅ Accessibility support via
Semantics - ✅
onChanged,onCompleted, andonFocusChangedcallbacks - ✅ Imperative control (
clear,validate,setError) viaGlobalKey
Installation
Add the package to your pubspec.yaml:
dependencies:
flutter_smart_otp: ^1.0.0
Then run:
flutter pub get
Basic usage
import 'package:flutter/material.dart';
import 'package:flutter_smart_otp/flutter_smart_otp.dart';
class VerificationScreen extends StatelessWidget {
const VerificationScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SmartOtpField(
length: 6,
onChanged: (value) => debugPrint('Value: $value'),
onCompleted: (value) => debugPrint('Completed: $value'),
),
),
);
}
}
Styling
SmartOtpField(
length: 5,
boxWidth: 52,
boxHeight: 60,
spacing: 10,
borderRadius: 16,
borderWidth: 2,
borderColor: const Color(0xFFE0E0E0),
focusedBorderColor: const Color(0xFF7C4DFF),
errorBorderColor: const Color(0xFFFF5252),
fillColor: const Color(0xFFF3F1FF),
textStyle: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
cursorColor: const Color(0xFF7C4DFF),
)
Obscured PIN entry
SmartOtpField(
length: 6,
obscureText: true,
obscuringCharacter: '●',
)
Validation
final fieldKey = GlobalKey<SmartOtpFieldState>();
SmartOtpField(
key: fieldKey,
length: 4,
validator: (value) =>
(value != null && value.length == 4) ? null : 'Enter all 4 digits',
autovalidateMode: AutovalidateMode.onUserInteraction,
);
// Trigger validation manually, e.g. from a "Verify" button:
final isValid = fieldKey.currentState!.validate();
// Or surface a server-side error after an async check:
fieldKey.currentState!.setError('Incorrect code, please try again.');
Using your own controllers and focus nodes
final controllers =
List.generate(4, (_) => TextEditingController());
final focusNodes = List.generate(4, (_) => FocusNode());
SmartOtpField(
length: 4,
controllers: controllers,
focusNodes: focusNodes,
);
// You remain responsible for disposing controllers/focusNodes you created.
Right-to-left layout
SmartOtpField(
length: 4,
textDirection: TextDirection.rtl,
)
Imperative API
Attach a GlobalKey<SmartOtpFieldState> to control the field from outside:
| Method | Description |
|---|---|
clear() |
Clears every box and refocuses the first box. |
validate() |
Runs validator against the current value, returns bool. |
setError(String?) |
Manually sets or clears the displayed error message. |
value |
Getter returning the current combined OTP string. |
Callbacks
| Callback | Signature | When it fires |
|---|---|---|
onChanged |
void Function(String value) |
On every keystroke, paste, or deletion. |
onCompleted |
void Function(String value) |
Whenever all boxes become filled. |
onFocusChanged |
void Function(int index, bool hasFocus) |
Whenever an individual box gains/loses focus. |
Example app
See the example/ directory for a complete demo covering:
basic verification, custom styling, obscured PIN entry, validation with
simulated server errors, a resend-code timer using external controllers,
enabled/disabled/read-only toggling, and RTL layout.
Additional information
Contributions, bug reports, and feature requests are welcome via the issue
tracker. This package has no third-party runtime dependencies beyond the
Flutter SDK and characters (already a transitive Flutter dependency).
Licensed under the MIT License.
Libraries
- flutter_smart_otp
- A highly customizable, production-ready OTP / PIN input field for Flutter.