Primary action

Pattern specification + reference specimen.

Pattern: Primary action (forms)

Universal — every product, every platform. Auth is one consumer; compose sheets, onboarding, and settings forms follow the same rules.

Rule

Primary actions stay visually enabled. Validate on tap. Explain failures inline.

Disable a primary control only when the action is impossible — not when the user simply hasn’t finished typing.

Disable primary?Examples
YesRequest in flight (isLoading), passkey ceremony running, truly no-op (empty list export)
NoEmail empty, password too short, required field missing — tap → inline footnote error

Why (iOS 26+ Liquid Glass)

.glassProminent + .disabled washes out to low-contrast grey-on-grey. It reads as broken UI, not “waiting for input.” That violates Accessibility and Clarity first (principles/FOUNDATION.md).

Apple’s modern form pattern: enabled-looking primary + feedback on submit. Live requirement rows (checkmarks) are fine for multi-field sign-up.

iOS

// ✅ Primary glass — always full strength; validate in action
Button("Continue") { submit() }
    .buttonStyle(.glassProminent)
    .tint(accent)
    .disabled(isLoading)  // in-flight only

private func submit() {
    guard isValid else {
        inlineMessage = "Enter a valid email address."
        return
    }
    inlineMessage = nil
    // …
}
// ✅ Loading — same footprint as idle
Button { submit() } label: {
    GlassProminentButtonLabel(title: "Sign In", isLoading: isLoading)
}
.buttonStyle(.glassProminent)
.tint(accent)
.disabled(isLoading)
// ❌ Do not — pill shrinks to spinner width
if isLoading { ProgressView() } else { Text("Sign In").frame(maxWidth: .infinity) }
// ❌ Do not — illegible on device
.disabled(!isValidEmail)
  • Inline errors: .footnote, .red or semantic error role
  • Loading: keep the full-width glass pill — hide title with opacity(0) and center ProgressView in a ZStack (GlassProminentButtonLabel); never swap label for spinner alone (pill collapses)
  • Placement: directly under the field — Section footer in Form; grouped custom fields use a VStack footnote below the control (never between field block and primary button)
  • VoiceOver: field accessibilityHint + UIAccessibility.post(.announcement) on submit failure
  • Shape: voice/COPY_STANDARDS.md (what happened + what to do)

Web

  • Primary button stays full opacity; disabled only while loading
  • Invalid submit → role="alert" message adjacent to the field or button
  • Do not rely on disabled:opacity-45 as validation UX for primary CTAs

Relationship to other patterns

PatternNotes
EDITOR_SHEET.mdSave/checkmark in toolbar: same rule for body glass primaries; toolbar confirmation may use system disabled when platform provides legible chrome
AUTH_IDENTIFIER.mdAuth specimens; must comply with this pattern
liquid-glass-ui.mdcGlass vocabulary; no bordered fallbacks

Specimens (Tasteful)

  • AuthenticationView — Continue / Sign In
  • DifferentEmailEntrySheet — sheet Continue
  • ForgotPasswordView — Send Reset Link