OTPin Logo

OTPin

Beautifully animated OTP input for Flutter

pub version build license platform


OTPin is a plug-and-play OTP (One-Time Password) input widget for Flutter that ships with multiple, richly animated visual styles. Each style supports five animated states โ€” Empty, Typing, Verifying, Success, and Error โ€” and is fully customizable.

Zero external dependencies. Built entirely with Flutter's built-in animation system.


โœจ Styles

Orbital Style

Digit fields orbit in a circle during verification, converge to a success icon on success, and shake on error.

Orbital Style Demo

Cascade Style

A cascading sinusoidal wave ripples through fields during verification. On success, fields morph into a unified capsule with a checkmark. On error, a staggered domino shake effect plays.

Cascade Style Demo


๐Ÿš€ Installation

Add otpin to your pubspec.yaml:

dependencies:
  otpin: ^0.1.0

Then run:

flutter pub get

๐Ÿ“– Quick Start

import 'package:otpin/otpin.dart';

OTPin(
  length: 4,
  style: OrbitalStyle(),
  onCompleted: (code) async {
    final isValid = await verifyOTP(code);
    return isValid; // true โ†’ success animation, false โ†’ error animation
  },
)

That's it โ€” a fully animated OTP input in 3 lines.


๐ŸŽจ Styles & Usage

Orbital Style

OTPin(
  length: 4,
  style: OrbitalStyle(
    config: OrbitalStyleConfig(
      // โ”€โ”€โ”€ Colors โ”€โ”€โ”€
      focusedBorderColor: Colors.cyan,
      filledBorderColor: Colors.cyanAccent,
      emptyBorderColor: Color(0xFF25334D),
      fieldBackgroundColor: Color(0xFF141C2E),
      cursorColor: Colors.cyanAccent,
      successColor: Color(0xFF00E676),
      errorColor: Color(0xFFFF5252),

      // โ”€โ”€โ”€ Success Display โ”€โ”€โ”€
      successIcon: Icons.check_rounded,
      successIconColor: Colors.white,

      // โ”€โ”€โ”€ Sizing โ”€โ”€โ”€
      fieldSize: 60.0,
      fieldBorderRadius: 16.0,
      fieldSpacing: 12.0,
      orbitRadius: 80.0,
      orbitFieldScale: 0.7,

      // โ”€โ”€โ”€ Durations โ”€โ”€โ”€
      orbitRevolutionDuration: Duration(seconds: 3),
      successConvergeDuration: Duration(milliseconds: 600),
      errorShakeDuration: Duration(milliseconds: 500),
    ),
  ),
  onCompleted: (code) async => await api.verify(code),
  onChanged: (code) => debugPrint('Current: $code'),
)

Orbital Animation States

State Animation
Empty Subtle ambient pulse on all fields
Typing Blinking cursor on focused field, scale-in on digit entry
Verify Fields shrink and orbit in a circle with a guide ring
Success Fields converge to center, revealing a success icon circle
Error Horizontal shake with red flash, then auto-clear

Cascade Style

OTPin(
  length: 4,
  style: CascadeStyle(
    config: CascadeStyleConfig(
      // โ”€โ”€โ”€ Colors โ”€โ”€โ”€
      focusedBorderColor: Color(0xFF3B82F6),
      filledBorderColor: Color(0xFF2563EB),
      emptyBorderColor: Color(0xFF25334D),
      fieldBackgroundColor: Color(0xFF141C2E),
      cursorColor: Color(0xFF60A5FA),
      waveBeamColor: Color(0xFF60A5FA),
      successColor: Color(0xFF00E676),
      errorColor: Color(0xFFFF5252),

      // โ”€โ”€โ”€ Success Display โ”€โ”€โ”€
      successIcon: Icons.check_rounded,
      successText: 'Verified',

      // โ”€โ”€โ”€ Sizing โ”€โ”€โ”€
      fieldSize: 60.0,
      fieldBorderRadius: 16.0,
      fieldSpacing: 12.0,
      waveHeight: 14.0,

      // โ”€โ”€โ”€ Durations โ”€โ”€โ”€
      waveCycleDuration: Duration(milliseconds: 1200),
      morphDuration: Duration(milliseconds: 500),
      errorShakeDuration: Duration(milliseconds: 500),
    ),
  ),
  onCompleted: (code) async => await api.verify(code),
)

Cascade Animation States

State Animation
Empty Subtle ambient pulse on all fields
Typing Blinking cursor on focused field, pop-in on digit entry
Verify Sinusoidal cascading wave with shimmer beam
Success Fields morph into a unified capsule with checkmark + label
Error Staggered domino shake with red flash, then auto-clear

๐Ÿ”ข Configurable Length

Control the number of OTP digit inputs with the length parameter (works with all styles):

// 6-digit OTP
OTPin(
  length: 6,
  style: OrbitalStyle(),
  onCompleted: (code) async => await api.verify(code),
)

// 8-digit OTP with Cascade style
OTPin(
  length: 8,
  style: CascadeStyle(),
  onCompleted: (code) async => await api.verify(code),
)

When using an external controller, make sure the controller length matches:

final controller = OTPinController(length: 6);

OTPin(
  length: 6,
  style: CascadeStyle(),
  controller: controller,
  onCompleted: (code) async => await api.verify(code),
)

๐ŸŽฎ Programmatic Control

Use OTPinController for full programmatic access:

final controller = OTPinController(length: 4);

// Use the controller with the widget.
OTPin(
  length: 4,
  style: OrbitalStyle(),
  controller: controller,
  onCompleted: (code) async => await api.verify(code),
)

// Set digits programmatically (e.g. from clipboard).
controller.setText('1234');

// Clear all digits.
controller.clear();

// Manually trigger states for testing.
controller.triggerSuccess();
controller.triggerError();

๐Ÿ—๏ธ Architecture

OTPin uses the Strategy Pattern to cleanly decouple visual styles from input logic:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  OTPin                  โ”‚  โ† Main widget
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ OTPinControllerโ”‚  โ”‚  OTPinStyle   โ”‚   โ”‚  โ† Input logic + Visual strategy
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ”‚               โ”‚               โ”‚
     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
     โ”‚ OrbitalStyleโ”‚  โ”‚CascadeStyle โ”‚  โ”‚ YourStyle โ”‚
     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Components

Component Responsibility
OTPin Composes controller + style, manages hidden keyboard
OTPinController Digit state, focus index, state machine transitions
OTPinState Enum: empty, typing, verifying, success, error
OTPinStyle Abstract interface โ€” implement to create new styles
OrbitalStyle Orbital orbit/converge animation style
CascadeStyle Cascading wave/morph animation style

๐Ÿงฉ Creating a Custom Style

Implement OTPinStyle to create your own animated OTP input:

import 'package:otpin/otpin.dart';

class MyCustomStyle implements OTPinStyle {
  @override
  Widget build(
    BuildContext context, {
    required OTPinController controller,
    required FocusNode focusNode,
  }) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) {
        // Build your UI based on controller.state,
        // controller.digits, controller.focusedIndex, etc.
        return Row(
          children: List.generate(controller.length, (i) {
            return _buildField(controller, i);
          }),
        );
      },
    );
  }

  Widget _buildField(OTPinController controller, int index) {
    final digit = controller.digits[index];
    final isFocused = controller.focusedIndex == index;
    // ... your custom rendering
  }
}

Then use it:

OTPin(
  length: 6,
  style: MyCustomStyle(),
  onCompleted: (code) async => await api.verify(code),
)

โš™๏ธ Configuration Reference

OrbitalStyleConfig

Property Type Default Description
focusedBorderColor Color Color(0xFF3B82F6) Border color of focused field
filledBorderColor Color Color(0xFF2563EB) Border color of filled field
emptyBorderColor Color Color(0xFF25334D) Border color of empty field
fieldBackgroundColor Color Color(0xFF141C2E) Field background color
successColor Color Color(0xFF00E676) Success state accent color
errorColor Color Color(0xFFFF5252) Error state accent color
successIcon IconData Icons.check_rounded Icon shown on success
successChild Widget? null Custom widget instead of icon
fieldSize double 60.0 Width/height of each field
fieldBorderRadius double 16.0 Corner radius
orbitRadius double 80.0 Radius of orbit circle
orbitRevolutionDuration Duration 2500ms Time for one full orbit

CascadeStyleConfig

Property Type Default Description
focusedBorderColor Color Color(0xFF3B82F6) Border color of focused field
filledBorderColor Color Color(0xFF2563EB) Border color of filled field
emptyBorderColor Color Color(0xFF25334D) Border color of empty field
fieldBackgroundColor Color Color(0xFF141C2E) Field background color
waveBeamColor Color Color(0xFF60A5FA) Shimmer beam color during verify
successColor Color Color(0xFF00E676) Success state accent color
errorColor Color Color(0xFFFF5252) Error state accent color
successIcon IconData Icons.check_rounded Icon shown on success
successText String? 'Verified' Label beside checkmark
fieldSize double 60.0 Width/height of each field
fieldBorderRadius double 16.0 Corner radius
waveHeight double 14.0 Peak wave oscillation height
waveCycleDuration Duration 1200ms Duration of one wave cycle
morphDuration Duration 500ms Capsule morph animation duration

๐Ÿ“‹ API Reference

OTPin Widget

const OTPin({
  int length = 4,             // Number of digits
  required OTPinStyle style,  // Visual style to render
  OTPinController? controller,// Optional external controller
  Future<bool> Function(String)? onCompleted, // Verification callback
  ValueChanged<String>? onChanged,            // Digit change callback
  bool autoFocus = true,      // Auto-focus on mount
})

OTPinController

class OTPinController extends ChangeNotifier {
  OTPinController({required int length});

  List<String> get digits;       // Current digit values
  OTPinState get state;          // Current animation state
  int get focusedIndex;          // Currently focused field
  bool get isFilled;             // Whether all digits are entered
  String get text;               // Current OTP string

  void addDigit(String digit);   // Add a single digit
  void removeLastDigit();        // Remove last digit
  void setText(String value);    // Set all digits at once
  void clear();                  // Clear all digits
  void startVerifying();         // Transition to verifying state
  void triggerSuccess();         // Transition to success state
  void triggerError();           // Transition to error state
}

OTPinState

enum OTPinState {
  empty,      // No digits entered โ€” ambient pulse
  typing,     // User is entering digits
  verifying,  // Code submitted โ€” loading animation
  success,    // Verification passed
  error,      // Verification failed
}

๐Ÿค Contributing

Contributions are welcome! Feel free to:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-style)
  3. Implement your changes
  4. Add tests
  5. Submit a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License โ€” see the LICENSE file for details.


Made with โค๏ธ by Abdelhaliem

Libraries

otpin
OTPin โ€” A customizable OTP input package with multiple visual styles and rich animations.