Skip to content

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.

Clearable Text Field, playing
Download .swift
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

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())
}
}
ParameterTypeDefault
titleLocalizedStringResourceRequired
textBinding<String>Required
systemImageString?nil
errorMessageLocalizedStringResource?nil

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

ClearableTextField.swift
// 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()
}