Skip to content
OTP Field preview
Rendered from the SwiftUI source on this page.

OTP Field

From syxUI — written for both platforms, not translated between them.

Interactive

A verification-code field where visible boxes are drawn over one real text field.

Inputs · SwiftUI · Flutter · iOS 17 · Flutter 3.27

  • input
  • otp
  • code
  • verification
  • auth
  • 2fa
  • form

The actual source

OTPField.swift
// OTP Field · syxUI · https://syxui.dev/components/otp-field
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// A one-time-code field: visible boxes over one hidden text field.
///
/// A single `TextField` owns the text, so paste, autofill from Messages, and
/// the delete key all behave. The boxes are decoration drawn from that string.
struct OTPField: View {
    var length: Int = 6
    var code: String = ""
    var onComplete: (String) -> Void = { _ in }

    @State private var text: String = ""
    @FocusState private var focused: Bool

    private var digits: [String] {
        let characters = Array(text)
        return (0..<length).map { index in
            index < characters.count ? String(characters[index]) : ""
        }
    }

    var body: some View {
        ZStack {
            TextField("", text: $text)
                .textContentType(.oneTimeCode)
                .focused($focused)
                .opacity(0.001)
                .onChange(of: text) { _, newValue in
                    // Digits only, never longer than the box count.
                    let filtered = String(newValue.filter(\.isNumber).prefix(length))
                    if filtered != newValue { text = filtered }
                    if filtered.count == length { onComplete(filtered) }
                }
            #if os(iOS)
                .keyboardType(.numberPad)
            #endif

            HStack(spacing: 10) {
                ForEach(0..<length, id: \.self) { index in
                    box(at: index)
                }
            }
            .allowsHitTesting(false)
        }
        .contentShape(Rectangle())
        .onTapGesture { focused = true }
        .onAppear { if text.isEmpty { text = code } }
        .accessibilityElement()
        .accessibilityLabel("Verification code")
        .accessibilityValue(text.isEmpty ? "Empty" : text.map(String.init).joined(separator: " "))
    }

    private func box(at index: Int) -> some View {
        let digit = digits[index]
        let isActive = focused && index == min(text.count, length - 1)

        return Text(digit)
            .font(.system(size: 24, weight: .semibold, design: .rounded))
            .monospacedDigit()
            .frame(width: 46, height: 56)
            .background(
                Color(white: digit.isEmpty ? 0.965 : 1),
                in: RoundedRectangle(cornerRadius: 14, style: .continuous)
            )
            .overlay {
                RoundedRectangle(cornerRadius: 14, style: .continuous)
                    .strokeBorder(
                        isActive ? Color.black : Color(white: 0.89),
                        lineWidth: isActive ? 2 : 1
                    )
            }
            .animation(.snappy(duration: 0.18), value: digit)
    }
}

#Preview {
    OTPField(code: "4218")
        .padding(30)
}
iOS 17 · No dependencies

SwiftUI note. One hidden TextField backs every box, which is what makes SMS autofill and paste work. Six separate fields break both.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27