Text Field
A single-line SwiftUI text field whose visible label is exposed to VoiceOver through the field title, an accessibility label, or LabeledContent, with keyboardType and textContentType set so the correct keyboard and AutoFill appear.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when the user enters a single line of free-form text such as a name, email address, or search query (e.g., "Email", "Search"). Uses a native SwiftUI
TextFieldso the field exposes editable-text semantics, its label, and its typed value to VoiceOver. - Use when the entered value maps to a system AutoFill category such as credentials or contact details, so the keyboard can offer to fill it (e.g., "Password", "One-Time Code", "Street Address").
- Use when the input has a natural on-screen keyboard tied to its data type, such as an email address or a numeric postal code.
Try a different component when
- Do not use when the user enters more than one line of text, such as a message body or review (use
text-editor.basic). - Do not use when the value is chosen from a fixed set of options rather than typed (use
select.menu). - Do not use when the value is a date or time selected from a calendar or wheel (use
date-picker.basic).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- The field has an accessible name matching its visible label, supplied by the
TextFieldtitle, an.accessibilityLabelon a titleless field, or aLabeledContentwrapper.- When a
LabeledContentsupplies the name, do not also add an.accessibilityLabel; the wrapper already provides the name and a second one overrides it.
- When a
- Set
.keyboardTypeto match the expected input (e.g.,.emailAddress,.numberPad) so the correct software keyboard appears. - Set
.textContentTypeto the matching semantic type (e.g.,.emailAddress,.password,.oneTimeCode,.postalCode) so AutoFill and QuickType offer the right value. - Give the field a visible boundary that meets 3:1 non-text contrast against its background, e.g.,
.textFieldStyle(.roundedBorder)or.border(.secondary); the default borderlessTextFieldboundary can fall below 3:1. - When the field can show a validation error, append the error text to the field's accessibility label dynamically (e.g., "Email, Enter a valid email address.") and signal it with more than color, so VoiceOver announces the error with the field name while the typed text remains the field's value, and the state is not conveyed by color alone.
- Move VoiceOver focus to the errored field with
@AccessibilityFocusStateand.accessibilityFocusedwhen validation fails on submit.
- Move VoiceOver focus to the errored field with
- Support Dynamic Type: use a built-in text style so the field's label and text scale, and allow the label to wrap rather than truncate at accessibility sizes.
- Meets the touch target size baseline in
global_rules.md(global.touch-target-size). - Meets the system focus indicator baseline in
global_rules.md(global.focus-visible).
Donts
Avoid these accessibility and UX barriers.
- Do not rely on a placeholder as the only label; the placeholder disappears once the user types, can fall below text contrast, and leaves the field with no accessible name for VoiceOver.
- Do not wrap the field in a
LabeledContentstyled to stack its label vertically above the field (a custom verticalLabeledContentStyle); VoiceOver then cannot double-tap to activate the field and Voice Control cannot target it by name, due to a known Apple bug. - Do not add an
.accessibilityLabelon top of aLabeledContent-supplied name; the extra label overrides the paired one and can desync from the visible text. - Do not include the control type in the label (e.g.,
.accessibilityLabel("Email text field")); VoiceOver already announces "Text Field", so this doubles it. - Do not leave
.keyboardTypeand.textContentTypeunset on a field that maps to a known type; it suppresses the right keyboard and blocks AutoFill. - Do not signal a validation error with color alone.
- Do not add error text to the
.accessibilityValue; it replaces the typed text, so VoiceOver stops speaking the field's actual value. - Do not expose error text only through an
.accessibilityHint; users can turn hints off in VoiceOver settings and would never hear the error.
Customizable
Alternatives and options that give the AI agent some room to move.
- The accessible name may come from the
TextFieldtitle, an.accessibilityLabelon a titleless field, or aLabeledContentwrapper. These are equivalent as long as exactly one name results. - For masked secret entry,
SecureFieldmay replaceTextField; it carries the same labeling and content-type requirements and additionally hides the typed value (pair it with.textContentType(.password),.newPassword, or.oneTimeCode). - The visual field style (
.textFieldStyle(.roundedBorder), a custom.border, or a bespoke background) is at the engineer's discretion as long as the boundary keeps 3:1 non-text contrast. - Editing-behavior modifiers such as
.textInputAutocapitalizationand.autocorrectionDisabled()may be tuned to the field's data (e.g., disabling both on an email field) without affecting the accessible name.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
import SwiftUI
struct TextFieldDemo: View {
@State private var email = ""
@State private var password = ""
@State private var fullName = ""
@State private var zip = ""
@State private var emailError: String?
@AccessibilityFocusState private var emailFocused: Bool
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Name from the field's own title; email keyboard + AutoFill via content type
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder) // visible boundary, 3:1 non-text contrast
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
// Error appended to the label dynamically; the typed text stays the value
.accessibilityLabel(emailError.map { "Email, \($0)" } ?? "Email")
.accessibilityFocused($emailFocused)
// Masked secret entry keeps the same labeling contract
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
.textContentType(.password)
// Name supplied by LabeledContent: do NOT also add .accessibilityLabel
LabeledContent("Full name") {
TextField("", text: $fullName)
.textFieldStyle(.roundedBorder)
.textContentType(.name)
}
// Titleless field: the name comes from .accessibilityLabel
TextField("", text: $zip)
.textFieldStyle(.roundedBorder)
.keyboardType(.numberPad)
.textContentType(.postalCode)
.accessibilityLabel("ZIP code")
if let emailError {
Text(emailError)
.foregroundStyle(.red) // color PLUS text, never color alone
.font(.footnote)
}
Button("Sign In") {
if email.contains("@") {
emailError = nil
} else {
emailError = "Enter a valid email address."
emailFocused = true // move VoiceOver to the errored field
}
}
}
.padding()
}
}
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Traits & semantics
- Each field is a single accessible element exposing the text-field trait, its accessible name, and its current value; a
SecureFieldmasks its value and surfaces separately as a secure text field. - The accessible name does not repeat the control type (no "text field" in the name).
VoiceOver
- VoiceOver speaks each field's name from its title,
.accessibilityLabel, orLabeledContentwrapper; the titleless ZIP field speaks "ZIP code", never silence or a blank name. - Double-tapping a field begins editing, including the
LabeledContent-named field; this is the exact activation a verticalLabeledContentStylebreaks. - On submitting an invalid email, VoiceOver announces the error text as part of the field's label, the typed text is still spoken as the field's value, and focus moves to the errored field.
- The secure field does not speak its typed characters.
Switch Control & Full Keyboard Access
- Each field is reachable and focusable via Switch Control and a hardware keyboard, and editing can begin from either.
Dynamic Type
- Field labels and entered text scale with Dynamic Type and stay fully visible (no clipping or truncation) at accessibility text sizes.
Visual
- Each field has a visible boundary meeting 3:1 non-text contrast against the background, including the borderless default case.
- The validation error is conveyed by more than color (error text plus color, not color alone).