Model or dataset
adar2378/pin_code_fields avatar
adar2378/pin_code_fields

pin_code_fields: a headless PIN and OTP input engine for Flutter

A flutter package which will help you to generate pin code fields with beautiful design and animations. Can be useful for OTP or pin code inputs 🤓🤓

794 stars384 forksDartLicense varies

At a glance

What is it?
pin_code_fields is a Flutter package for PIN and OTP entry that splits into a headless input engine and a ready-made Material Design implementation. It suits teams building a verification screen who want animation, obscuring, autofill and paste support without hand-rolling a cell widget, but the real API documentation lives in the package README rather than the repository root.
Who is it for?
Adopt pin_code_fields when your Flutter app needs a verification screen with animation, obscuring and SMS autofill and you want the layout left in your own hands, using the headless PinInput with a builder.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly Dart, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A headless engine with a Material wrapper on top of it

pin_code_fields solves a small, recognisable problem: getting digits into a fixed number of boxes on a phone. The package description calls it a highly customizable Flutter package for PIN code and OTP input fields, and the current major version is built on what the documentation calls a new headless architecture.

That architecture gives you two ways in. One is a ready-made widget, `MaterialPinField`, which arrives with its own layout and animation and needs only a length and a callback. The other is `PinInput`, the headless engine, which hands you the cells and lets you render them yourself. The split exists because the two halves have different audiences. A team shipping a verification screen on a deadline takes the Material widget. A team with a design system takes the engine and paints its own boxes.

The quick start in the repository root README shows both shapes side by side, with the Material one taking a length and an `onCompleted` callback and the headless one taking a length and a `builder` that receives the context and the cells:

dart
import 'package:pin_code_fields/pin_code_fields.dart';

// Material Design (ready to use)
MaterialPinField(
  length: 6,
  onCompleted: (pin) => print('PIN: $pin'),
)

// Custom UI (headless)
PinInput(
  length: 4,
  builder: (context, cells) => YourCustomUI(cells),
)

The repository layout supports the same reading. Under `packages/pin_code_fields/lib/src/` there are two sibling directories, `core/` described as the headless input engine and a second one holding the Material Design implementation, so the engine does not depend on the Material layer and the Material layer is a consumer of it.

The project is published to pub.dev as `pin_code_fields`, and the package's own README inside the repository is where the full documentation lives.

Adding the dependency to a Flutter project

The install path is the standard pub.dev one, since the package is published rather than consumed from a git reference. In your `pubspec.yaml` under dependencies:

yaml
dependencies:
  pin_code_fields: ^9.0.0

The version constraint shown in the README matches the headless architecture release. The repository root also carries its own `pubspec.yaml` and `melos.yaml`, but those describe the monorepo that hosts the package, not the dependency you add to an app.

For a first real use, the shortest path is a `MaterialPinField` with a length matching your OTP and a callback that receives the completed value. The example in the README prints the PIN when the field is filled, and in a real app that callback is where you would hand the string to your verification request and show any error that comes back.

Two things are worth deciding before you build the screen around it. First, whether the input is numeric only, since the package obscures characters and deals in PIN semantics rather than free text. Second, how the error state is presented, because the feature list names a shake animation for errors with programmatic control, and the controller is described as the single place that holds text, focus and errors. If your design has a different error treatment, the headless route is the one that gives you room to implement it.

The package topics include android, ios, otp, pin, shapes and animation, which matches the stated scope: it is a mobile-first widget for short numeric entry.

One controller holding text, focus and errors

The feature list names a unified controller as one of the nine headline items, described as a single controller for text, focus, and errors. This is the piece that decides how the widget behaves when something goes wrong, and it is where a PIN field differs from an ordinary text field.

In a plain `TextField`, a validation error is a separate overlay or a separate error text. Here the error is part of the same object that owns the typed characters, which is what makes the shake animation possible: the widget can react to a change in the error state without the surrounding screen having to coordinate a second widget. The listed animations include scale, fade and slide entry animations as well as the shake, so a completed or reset field has some visual vocabulary of its own.

The mention of programmatic control over the error animation is the detail to plan around. If a verification request fails and your server returns an error, you need to trigger that state from outside the widget, and that is exactly what a programmatic control is for. The top level README does not spell out the API surface, so the shape of that control has to be read in the package README before you design the retry flow around it.

Another line in the feature list is worth noting for form-heavy screens: the package is documented as working with Flutter's `Form` widget. That means a PIN field can participate in the same validation and submission machinery as the rest of a form, rather than needing its own submission path.

Obscuring, long-press paste and SMS autofill on the same widget

Three of the listed features address the practical friction of a PIN entry field, and they are the reason a package like this exists rather than a loop over `TextField`s.

Obscure support covers both a character-level mask and a custom widget, so a field can show dots or show something else entirely. If your product needs a different visual, the custom widget option removes the need to fork the package.

Paste support is described as long-press to paste from clipboard. That matters because the most common user failure with a PIN box is not typing wrong but failing to receive the SMS and then copying the code from another app. A six digit box that ignores the clipboard forces the user into a manual, error prone transfer.

Autofill support covers SMS OTP autofill for iOS and Android, so the platform can offer the code directly above the keyboard.

None of these three features is described in terms of how the underlying platform APIs are called, so treat them as documented capabilities rather than as a description of the implementation. What the documentation does establish is that the same widget handles all three, which means a headless build that swaps in your own rendering still inherits the input behaviour unless you bypass it.

The `length` parameter is the one thing every example sets, and it is the parameter that decides how many cells exist. A four digit PIN and a six digit OTP code are different widgets in this package, not the same widget with validation on top.

The repository root README is a map, the package README is the manual

If you are evaluating this project, the first thing to know is that the repository root README points you elsewhere for the details. It names the publishable package location as `packages/pin_code_fields/` and links to a separate package README for full documentation, with a `docs/FEATURE_COMPARISON.md` file and a `CLAUDE.md` also present at the root.

That split is normal for a melos monorepo, and the structure diagram in the README makes it explicit: a `packages/` tree holding the main package with `lib/`, `example/`, `test/`, its own `README.md`, `CHANGELOG.md` and `LICENSE`, plus `docs/`, `melos.yaml` and `CLAUDE.md` beside it. There is a `docs/FEATURE_COMPARISON.md` which suggests the headless engine and the Material implementation are being compared somewhere, but the root README does not summarise what that comparison contains.

Two consequences follow. If you want to know which of the two APIs to use for a given screen, the answer lives in that comparison document and in the package README, not in the root. And if you are checking whether the package on pub.dev matches what you read in the repository, you have to look at the package README rather than assuming the root reflects the published version.

The version story deserves a note. The README describes Version 9.0 as the release that introduced the headless architecture, and the dependency snippet pins `^9.0.0`. The listed tagged releases for the repository, however, are V7.3.0 from 2021-07-28, V7.2.0 from 2021-05-26 and v7.0.0 from 2021-03-11. The repository root therefore points at a major version well ahead of the tags visible there, and the tags are a good guide to how little release visibility this project has offered recently.

Working on the package with melos from the repository

The repository is a melos workspace, which the README states is used for monorepo management, and the package lives in one of its members. To work on the code rather than consume it, the documented commands are:

bash
# Install melos
dart pub global activate melos

# Bootstrap workspace
melos bootstrap

# Run tests
melos test

# Run analyzer
melos analyze

Activating melos globally puts the `melos` executable on your path, `melos bootstrap` resolves dependencies across the workspace members, and the two verification commands run the test suite and the static analyzer. A `.fvmrc` file at the root suggests the Flutter SDK version is pinned for the workspace, and `analysis_options.yaml` holds the lint configuration that `melos analyze` applies.

This matters mostly to contributors and to teams who intend to vendor or extend the widget. For consumers, the pub.dev package is the whole story and none of this setup applies. The `test/` directory inside the package member is where the widget behaviour would be covered, though the root README does not describe what the tests assert.

There is a `CHANGELOG.md` at both the root and inside the package directory. The root one is the one to check if you are tracking changes across the workspace, and the package one is the release history for the published version. The documentation does not describe a rollback procedure for a bad release, so there is no stated mechanism for pinning back to a specific earlier version beyond the usual pub.dev version constraint syntax.

Against a hand-rolled TextField loop and a full auth screen kit

The comparison worth making is not with another pin code package but with the two alternatives most teams actually have: writing the cell widget themselves, or adopting a broader authentication screen kit.

Writing it by hand means a `Row` of `TextField`s with focus management, a controller, key handling for paste and backspace, and separate obscuring. That is a day of work that most teams would rather not spend on a screen that appears once in the product. pin_code_fields removes exactly that work, and its published version numbers show a package that has been through several major revisions of its API, with the current one reorganising around the headless split. What you give up is control of the internals: there is no documented way to change the key input behaviour, and the repository offers no custom key event handling in its feature list.

A full authentication screen kit goes the other way. Such a kit bundles the PIN field with identity provider flows, session handling, error mapping and a design system, so it is a larger commitment than one input widget. pin_code_fields stays inside the input boundary. It has no network layer, no OTP request logic and no session concept, which means you keep control of the verification request and the error contract, and you also have to write them.

The headless architecture is what makes it competitive in between. Taking `PinInput` and supplying your own builder gives the visual freedom of a custom design while keeping the paste, autofill and error handling that most hand-rolled implementations miss.

Editorial conclusion

Adopt pin_code_fields when your Flutter app needs a verification screen with animation, obscuring and SMS autofill and you want the layout left in your own hands, using the headless PinInput with a builder. Do not adopt it if you need a plain unstyled text field, since the package's value is in the cell and animation layer rather than in basic character entry, and check first that the version you pull from pub.dev matches the headless API described in the repository root README before wiring the builder callback.

Frequently asked questions

How do I add pin_code_fields to a Flutter project?

Add it under dependencies in your pubspec.yaml as `pin_code_fields: ^9.0.0`, which is the constraint given in the README for the headless architecture release. The package is published on pub.dev, so no git dependency is needed.

What is the difference between MaterialPinField and PinInput?

MaterialPinField is the ready-made Material Design widget that takes a length and an `onCompleted` callback. PinInput is the headless engine, taking a length and a `builder` that receives the context and the cells so you can render your own UI.

Does pin_code_fields support pasting an OTP code and SMS autofill?

Both are listed features. Paste is described as long-press to paste from clipboard, and autofill covers SMS OTP autofill for iOS and Android.

How do I show an error when the PIN is wrong?

The unified controller holds text, focus and errors, and the listed error animation is a shake with programmatic control. The top level README does not spell out that API, so the package README inside `packages/pin_code_fields/` is the place to check the exact call.

Is pin_code_fields still being updated?

The repository is not archived and the last push was on 2026-09-30. The listed tagged releases are older, with V7.3.0 published on 2021-07-28 and the current documented version being 9.0.

Can I use pin_code_fields inside a Flutter Form?

Yes, form integration is one of the listed features, so the field can be validated and submitted with Flutter's Form widget rather than needing its own submission path.

Official sources

  1. adar2378/pin_code_fields on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/adar2378-pin-code-fields.svg)](https://hysenlabs.com/projects/adar2378-pin-code-fields)

Was this useful?