flutter_smart_otp

pub package License: MIT

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 validator and AutovalidateMode
  • ✅ Obscure text support (PIN-style entry) with a custom obscuring character
  • ✅ Enable / disable state
  • ✅ Read-only mode
  • ✅ Keyboard type customization
  • TextInputFormatter support
  • ✅ 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 / decorationBuilder hooks for total visual control (gradients, shadows, custom shapes)
  • ✅ Box width, height, and spacing customization
  • ✅ Autofocus
  • ✅ Custom FocusNode list support
  • ✅ Custom TextEditingController list 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, and onFocusChanged callbacks
  • ✅ Imperative control (clear, validate, setError) via GlobalKey

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.