One-time code input boxes
Six-box verification code field backed by one hidden text input, with paste, backspace and platform SMS autofill support.
FlutterAuthenticationflutterotpinputautofill
Code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class OtpInput extends StatefulWidget {
const OtpInput({super.key, this.length = 6, required this.onCompleted});
final int length;
final ValueChanged<String> onCompleted;
@override
State<OtpInput> createState() => _OtpInputState();
}
class _OtpInputState extends State<OtpInput> {
final _controller = TextEditingController();
final _focus = FocusNode();
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {});
if (_controller.text.length == widget.length) widget.onCompleted(_controller.text);
});
}
@override
void dispose() {
_controller.dispose();
_focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final text = _controller.text;
return GestureDetector(
onTap: _focus.requestFocus,
child: Stack(
children: [
// The real input is invisible but receives typing, paste and autofill.
Opacity(
opacity: 0,
child: TextField(
controller: _controller,
focusNode: _focus,
autofocus: true,
keyboardType: TextInputType.number,
autofillHints: const [AutofillHints.oneTimeCode],
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(widget.length),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (var i = 0; i < widget.length; i++)
Container(
width: 46,
height: 56,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
width: i == text.length ? 2 : 1,
color: i == text.length ? scheme.primary : scheme.outlineVariant,
),
),
child: Text(i < text.length ? text[i] : '',
style: Theme.of(context).textTheme.headlineSmall),
),
],
),
],
),
);
}
}
How it works
What it solves
Verification codes are typed on phones, usually while the SMS banner is still on screen. Six separate text fields make focus handling, paste and backspace painful and break the system autofill suggestion.
How it works
Only one TextField exists. It is invisible but focused, filtered to digits and limited to the code length, so the keyboard, paste and the oneTimeCode autofill hint all work natively. The visible boxes are plain containers that read characters from the controller text by index, and the box matching the current length gets a thicker primary border to show the cursor position.
When the text reaches the required length, onCompleted fires once, so the parent can verify automatically without a submit button.
Adapting it
Change length for four digit codes and adjust the box width. Add an error flag that colours the borders with scheme.error and clears the controller after a wrong attempt. To support letters, replace the digits filter with an alphanumeric formatter and the keyboard type.
Pitfalls
- Opacity zero still receives taps in Flutter, but never use
Visibility, which removes it from the tree. onCompletedalso fires after you clear and retype; guard it in the parent while a request is running.- On iOS the autofill suggestion appears only when the field is inside an
AutofillGroup.
How to use it
Copy to lib/auth/. No packages.
OtpInput(length: 6, onCompleted: (code) => verify(code));
Wrap the screen in AutofillGroup to enable the iOS suggestion above the keyboard.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.