Clearable Text Field
A text field with a leading symbol, a clear button that keeps the focus, a tinted border while editing and an optional error message.

- Inputs
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- text field · clear button · input · icon · focus · error · form · search · campo de texto · botón de borrar · entrada · icono · foco · error · formulario · búsqueda
Description
Section titled “Description”A text field with a leading symbol and a button that clears it. The border and the symbol take the tint while it has focus, and both turn red with a message under the field when the caller passes an error.
Clearing keeps the focus, so the keyboard stays up for the next text. With Reduce Motion on, the clear button and the error fade instead of moving.
struct ClearableTextFieldDemo: View { @State private var search = "Coffee near me" @State private var name = "" @State private var email = ""
private var emailError: LocalizedStringResource? { !email.isEmpty && !email.contains("@") ? "Enter a valid email address." : nil }
var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Contact") .font(.largeTitle.bold())
ClearableTextField(title: "Search", text: $search, systemImage: "magnifyingglass") ClearableTextField(title: "Name", text: $name, systemImage: "person") .textContentType(.name) ClearableTextField(title: "Email", text: $email, systemImage: "envelope", errorMessage: emailError) .textContentType(.emailAddress) .keyboardType(.emailAddress) .textInputAutocapitalization(.never) .autocorrectionDisabled() } .padding(20) } .tint(.indigo) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
title | LocalizedStringResource | Required |
text | Binding<String> | Required |
systemImage | String? | nil |
errorMessage | LocalizedStringResource? | nil |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A text field with a leading symbol and a button that clears it. The border/// and the symbol take the tint while it has focus, and both turn red with a/// message under the field when the caller passes an error.////// Clearing keeps the focus, so the keyboard stays up for the next text./// With Reduce Motion on, the clear button and the error fade instead of moving.struct ClearableTextField: View { let title: LocalizedStringResource @Binding var text: String var systemImage: String? var errorMessage: LocalizedStringResource?
@FocusState private var isFocused: Bool @Environment(\.accessibilityReduceMotion) private var reduceMotion
private var accent: AnyShapeStyle { if errorMessage != nil { return AnyShapeStyle(.red) } return isFocused ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary) }
var body: some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 10) { if let systemImage { Image(systemName: systemImage) .foregroundStyle(accent) .accessibilityHidden(true) }
TextField(text: $text) { Text(title) } .focused($isFocused) .accessibilityLabel(Text(title)) .accessibilityHint(errorMessage.map { Text($0) } ?? Text(""))
if !text.isEmpty { Button { text = "" isFocused = true } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.tertiary) .padding(13) .contentShape(.rect) .padding(-13) } .buttonStyle(.plain) .accessibilityLabel("Clear text") .transition(reduceMotion ? .opacity : .scale.combined(with: .opacity)) } } .padding(.horizontal, 14) .padding(.vertical, 13) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 14, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(accent, lineWidth: isFocused || errorMessage != nil ? 1.5 : 0.5) } .contentShape(.rect) .onTapGesture { isFocused = true }
if let errorMessage { Label { Text(errorMessage) } icon: { Image(systemName: "exclamationmark.circle.fill") } .font(.footnote) .foregroundStyle(.red) .transition(reduceMotion ? .opacity : .opacity.combined(with: .move(edge: .top))) .accessibilityHidden(true) } } .animation(reduceMotion ? .easeInOut(duration: 0.15) : .snappy(duration: 0.25), value: text.isEmpty) .animation(reduceMotion ? .easeInOut(duration: 0.15) : .snappy(duration: 0.25), value: errorMessage == nil) .animation(.easeInOut(duration: 0.15), value: isFocused) }}
// MARK: - Demo
struct ClearableTextFieldDemo: View { @State private var search = "Coffee near me" @State private var name = "" @State private var email = ""
private var emailError: LocalizedStringResource? { !email.isEmpty && !email.contains("@") ? "Enter a valid email address." : nil }
var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Contact") .font(.largeTitle.bold())
ClearableTextField(title: "Search", text: $search, systemImage: "magnifyingglass") ClearableTextField(title: "Name", text: $name, systemImage: "person") .textContentType(.name) ClearableTextField(title: "Email", text: $email, systemImage: "envelope", errorMessage: emailError) .textContentType(.emailAddress) .keyboardType(.emailAddress) .textInputAutocapitalization(.never) .autocorrectionDisabled() } .padding(20) } .tint(.indigo) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}
#Preview { ClearableTextFieldDemo()}