Flutter OTP Widget
A highly customizable OTP (One-Time Password) text field widget for Flutter with support for multiple styles, themes, and input types.
Features
β¨ Multiple Field Styles
- Box (default) - Traditional bordered boxes
- Underline - Minimalist underlined fields
- Circle - Circular PIN-style fields
π¨ Flexible Theming
- Light and dark theme presets
- Fully customizable colors and dimensions
- Automatic theme adaptation
β¨οΈ Input Type Control
- Numbers only (default)
- Text only (letters)
- Alphanumeric (mixed)
π Secure Input
- Password/PIN mode with obscured text
- Customizable obscuring character
π― State Management
- Error state styling
- Disabled state
- Auto-focus support
- Focus and change callbacks
β‘ Smart Navigation
- Auto-advance to next field
- Backspace to previous field
- Arrow key navigation
- Automatic completion detection
Screenshots
![]() Box Style |
![]() Underline Style |
![]() Circle Style |
![]() Secure PIN Entry |
![]() Error State |
Installation
Add this to your package's pubspec.yaml file:
dependencies:
flutter_otp_widget: ^1.0.1
Then run:
flutter pub get
Basic Usage
import 'package:flutter_otp_widget/flutter_otp_widget.dart';
class OTPScreen extends StatefulWidget {
@override
_OTPScreenState createState() => _OTPScreenState();
}
class _OTPScreenState extends State<OTPScreen> {
final controllers = List.generate(6, (_) => TextEditingController());
final focusNodes = List.generate(6, (_) => FocusNode());
@override
void dispose() {
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
textStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
onCompleted: (otp) {
print('OTP Entered: $otp');
// Verify OTP
},
onChange: (value) {
print('Current OTP: $value');
},
),
),
);
}
}
Examples
Different Styles
// Box Style (Default)
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
fieldStyle: OTPFieldStyle.box,
textStyle: TextStyle(fontSize: 24),
)
// Underline Style
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
fieldStyle: OTPFieldStyle.underline,
textStyle: TextStyle(fontSize: 24),
)
// Circle Style
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
fieldStyle: OTPFieldStyle.circle,
textStyle: TextStyle(fontSize: 24),
)
Custom Theme
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
theme: OTPTheme.custom(
borderColor: Colors.grey,
focusedBorderColor: Colors.blue,
errorBorderColor: Colors.red,
fillColor: Colors.white,
borderRadius: BorderRadius.circular(12),
),
textStyle: TextStyle(fontSize: 24),
)
Text Input
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
inputType: OTPInputType.text,
textCapitalization: TextCapitalization.characters,
textStyle: TextStyle(fontSize: 24),
)
Secure PIN Entry
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
fieldStyle: OTPFieldStyle.circle,
obscureText: true,
obscuringCharacter: 'β',
textStyle: TextStyle(fontSize: 24),
)
Error State
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
hasError: isWrongOTP,
theme: OTPTheme.light().copyWith(
errorBorderColor: Colors.red,
),
textStyle: TextStyle(fontSize: 24),
)
4-Digit Code
final controllers = List.generate(4, (_) => TextEditingController());
final focusNodes = List.generate(4, (_) => FocusNode());
OTPTextField(
controllers: controllers,
focusNodes: focusNodes,
fieldStyle: OTPFieldStyle.underline,
textStyle: TextStyle(fontSize: 24),
)
With Center Spacing (3-3 Layout)
OTPTextField(
controllers: controllers, // 6 controllers
focusNodes: focusNodes,
centerSpacing: 16, // Extra space in the middle
textStyle: TextStyle(fontSize: 24),
)
// Layout: XXX XXX
Parameters
OTPTextField
| Parameter | Type | Default | Description |
|---|---|---|---|
controllers |
List<TextEditingController> |
required | Text controllers for each field |
focusNodes |
List<FocusNode> |
required | Focus nodes for each field |
textStyle |
TextStyle |
required | Text style for input |
onCompleted |
ValueChanged<String>? |
null | Callback when all fields are filled |
onChange |
ValueChanged<String>? |
null | Callback when any field changes |
fieldWidth |
double |
40 | Width of each field |
fieldHeight |
double |
50 | Height of each field |
fieldSpacing |
double |
8 | Spacing between fields |
centerSpacing |
double? |
null | Extra spacing at the middle |
theme |
OTPTheme? |
null | Theme configuration |
fieldStyle |
OTPFieldStyle |
box | Visual style of fields |
inputType |
OTPInputType |
number | Type of input allowed |
obscureText |
bool |
false | Whether to obscure text |
obscuringCharacter |
String |
'β' | Character for obscuring |
enabled |
bool |
true | Whether fields are enabled |
autoFocus |
bool |
false | Auto-focus first field |
hasError |
bool |
false | Show error state |
textCapitalization |
TextCapitalization |
none | Text capitalization mode |
See the API documentation for complete parameter list.
State Management Integration
The package works seamlessly with any state management solution. Here's an example with Riverpod:
@riverpod
class OtpControllers extends _$OtpControllers {
@override
List<TextEditingController> build(int count) {
final controllers = List.generate(count, (_) => TextEditingController());
ref.onDispose(() {
for (var controller in controllers) {
controller.dispose();
}
});
return controllers;
}
void clear() {
for (var controller in state) {
controller.clear();
}
}
}
// Usage
final controllers = ref.watch(otpControllersProvider(6));
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Issues and Feedback
Please file issues, bugs, or feature requests in our issue tracker.
Example App
For a complete example with all features demonstrated, check out the example directory.




