Skip to content

Password Strength Field

A password field with a strength meter and a checklist of rules that tick off as they are met.

Password Strength Field, playing
Download .swift
Origin
Original work
Found by
password · secure field · strength meter · checklist · validation · sign up · form · contraseña · campo seguro · fortaleza · lista de requisitos · validación · registro · formulario

A password field with a strength meter and a checklist of rules that tick off as they are met.

struct PasswordStrengthFieldDemo: View {
@State private var password = ""
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 22) {
Text("Create your account")
.font(.largeTitle.bold())
PasswordStrengthField(title: "Password", password: $password)
}
.padding(24)
}
.tint(.indigo)
.background(LinearGradient(colors: [.indigo.opacity(0.14), .pink.opacity(0.08)], startPoint: .top, endPoint: .bottom))
}
}
ParameterTypeDefault
titleLocalizedStringResourceRequired
passwordBinding<String>Required
rulesNot writtenPasswordStrengthFieldRule.defaults

Copy this code into your project, or download the file.

PasswordStrengthField.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// One requirement of the password, shown as a line of the checklist.
struct PasswordStrengthFieldRule: Identifiable, Sendable {
let id: String
let title: LocalizedStringResource
let isMet: @Sendable (String) -> Bool
static let defaults: [PasswordStrengthFieldRule] = [
.init(id: "length", title: "At least 8 characters") { $0.count >= 8 },
.init(id: "case", title: "Upper and lower case letters") { $0.contains(where: \.isUppercase) && $0.contains(where: \.isLowercase) },
.init(id: "digit", title: "A number") { $0.contains(where: \.isNumber) },
.init(id: "symbol", title: "A symbol") { $0.contains { $0.isPunctuation || $0.isSymbol } },
]
}
/// Turns the rules into a strength level, from 0 (empty) to `levels`.
enum PasswordStrengthFieldMeter {
static let levels = 4
static func level(for password: String, rules: [PasswordStrengthFieldRule]) -> Int {
guard !password.isEmpty, !rules.isEmpty else { return 0 }
let met = rules.filter { $0.isMet(password) }.count
let scaled = Double(met) / Double(rules.count) * Double(levels)
return max(1, min(levels, Int(scaled.rounded(.up))))
}
}
/// A password field with a strength meter and a checklist of rules that tick off as they are met.
struct PasswordStrengthField: View {
let title: LocalizedStringResource
@Binding var password: String
var rules = PasswordStrengthFieldRule.defaults
@State private var isRevealed = false
@FocusState private var isFocused: Bool
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private let fieldCornerRadius: CGFloat = 14
private var level: Int { PasswordStrengthFieldMeter.level(for: password, rules: rules) }
private var levelName: String {
switch level {
case 0: "Empty"
case 1: "Weak"
case 2: "Fair"
case 3: "Good"
default: "Strong"
}
}
private var levelColor: Color {
switch level {
case 1: .red
case 2: .orange
case 3: .yellow
default: .green
}
}
var body: some View {
VStack(alignment: .leading, spacing: 14) {
field
HStack(spacing: 6) {
ForEach(1...PasswordStrengthFieldMeter.levels, id: \.self) { segment in
Capsule()
.fill(segment <= level ? AnyShapeStyle(levelColor) : AnyShapeStyle(.quaternary))
.frame(height: 6)
}
Text(levelName)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.fixedSize()
.padding(.leading, 6)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Password strength")
.accessibilityValue(levelName)
VStack(alignment: .leading, spacing: 8) {
ForEach(rules) { rule in
checklistRow(rule)
}
}
}
.animation(reduceMotion ? nil : .snappy(duration: 0.25), value: password)
}
private var field: some View {
HStack(spacing: 10) {
Group {
if isRevealed {
TextField(text: $password) { Text(title) }
} else {
SecureField(text: $password) { Text(title) }
}
}
.focused($isFocused)
.textContentType(.newPassword)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.accessibilityLabel(title)
Button {
isRevealed.toggle()
isFocused = true
} label: {
// The icon is too small to hit. The padding widens what answers to a touch,
// and the same padding taken back keeps the field as it was.
Image(systemName: isRevealed ? "eye.slash" : "eye")
.foregroundStyle(.secondary)
.padding(15)
.contentShape(.rect)
.padding(-15)
}
.buttonStyle(.plain)
.accessibilityLabel(isRevealed ? "Hide password" : "Show password")
}
.padding(14)
.background(.background, in: .rect(cornerRadius: fieldCornerRadius, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: fieldCornerRadius, style: .continuous)
.strokeBorder(isFocused ? AnyShapeStyle(.tint) : AnyShapeStyle(.quaternary), lineWidth: 1.5)
}
}
private func checklistRow(_ rule: PasswordStrengthFieldRule) -> some View {
let isMet = rule.isMet(password)
return Label {
Text(rule.title)
} icon: {
Image(systemName: isMet ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isMet ? .green : .secondary)
.contentTransition(reduceMotion ? .identity : .symbolEffect(.replace))
}
.font(.subheadline)
.accessibilityElement(children: .ignore)
.accessibilityLabel(rule.title)
.accessibilityValue(isMet ? "Met" : "Not met")
}
}
// MARK: - Demo
struct PasswordStrengthFieldDemo: View {
@State private var password = ""
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 22) {
Text("Create your account")
.font(.largeTitle.bold())
PasswordStrengthField(title: "Password", password: $password)
}
.padding(24)
}
.tint(.indigo)
.background(LinearGradient(colors: [.indigo.opacity(0.14), .pink.opacity(0.08)], startPoint: .top, endPoint: .bottom))
}
}
#Preview {
PasswordStrengthFieldDemo()
}