Skip to content

Expanding Search Button

A round magnifying glass button that opens into a search field.

Expanding Search Button, playing
Download .swift
Origin
Original work
Found by
search · expanding · search bar · text field · morph · magnifying glass · button · buscar · búsqueda · barra de búsqueda · campo de texto · expandir · lupa · botón

A round magnifying glass button that opens into a search field. Closing it clears the text and dismisses the keyboard.

With Reduce Motion on, the field appears without animating.

struct ExpandingSearchButtonDemo: View {
private struct Row: Identifiable {
let id: String
let systemImage: String
}
@State private var text = ""
private let rows = [
Row(id: "Albums", systemImage: "square.stack.fill"),
Row(id: "Artists", systemImage: "music.mic"),
Row(id: "Playlists", systemImage: "music.note.list"),
Row(id: "Podcasts", systemImage: "antenna.radiowaves.left.and.right"),
Row(id: "Radio", systemImage: "dot.radiowaves.left.and.right"),
]
private var visibleRows: [Row] {
text.isEmpty ? rows : rows.filter { $0.id.localizedStandardContains(text) }
}
var body: some View {
VStack(alignment: .leading, spacing: 28) {
HStack(spacing: 16) {
Text("Library")
.font(.largeTitle.bold())
.fixedSize()
ExpandingSearchButton(text: $text)
.frame(maxWidth: .infinity, alignment: .trailing)
}
VStack(alignment: .leading, spacing: 22) {
ForEach(visibleRows) { row in
Label(row.id, systemImage: row.systemImage)
.font(.title3)
}
}
if visibleRows.isEmpty {
ContentUnavailableView.search(text: text)
}
Spacer(minLength: 0)
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
ParameterTypeDefault
textBinding<String>Required
promptLocalizedStringResource"Search"
onSubmit() -> Void{}

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

ExpandingSearchButton.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A round magnifying glass button that opens into a search field.
/// Closing it clears the text and dismisses the keyboard.
///
/// With Reduce Motion on, the field appears without animating.
struct ExpandingSearchButton: View {
@Binding var text: String
var prompt: LocalizedStringResource = "Search"
var onSubmit: () -> Void = {}
@State private var isExpanded = false
@FocusState private var isFocused: Bool
@ScaledMetric(relativeTo: .body) private var height = 54.0
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var animation: Animation? {
reduceMotion ? nil : .spring(duration: 0.45, bounce: 0.25)
}
var body: some View {
HStack(spacing: 0) {
Button {
open()
} label: {
Image(systemName: "magnifyingglass")
.font(.headline)
.frame(width: height, height: height)
.contentShape(.circle)
}
.buttonStyle(.plain)
.accessibilityLabel("Search")
if isExpanded {
TextField(text: $text) { Text(prompt) }
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($isFocused)
.submitLabel(.search)
.onSubmit(onSubmit)
.onAppear { isFocused = true }
.transition(.opacity)
Button {
close()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.symbolRenderingMode(.hierarchical)
.frame(width: height, height: height)
.contentShape(.circle)
}
.buttonStyle(.plain)
.accessibilityLabel("Close")
.transition(.opacity)
}
}
.frame(maxWidth: isExpanded ? .infinity : height)
.background(.regularMaterial, in: .capsule)
.overlay(Capsule().strokeBorder(.quaternary))
}
private func open() {
if isExpanded {
isFocused = true
} else {
withAnimation(animation) { isExpanded = true }
}
}
private func close() {
text = ""
isFocused = false
withAnimation(animation) { isExpanded = false }
}
}
// MARK: - Demo
struct ExpandingSearchButtonDemo: View {
private struct Row: Identifiable {
let id: String
let systemImage: String
}
@State private var text = ""
private let rows = [
Row(id: "Albums", systemImage: "square.stack.fill"),
Row(id: "Artists", systemImage: "music.mic"),
Row(id: "Playlists", systemImage: "music.note.list"),
Row(id: "Podcasts", systemImage: "antenna.radiowaves.left.and.right"),
Row(id: "Radio", systemImage: "dot.radiowaves.left.and.right"),
]
private var visibleRows: [Row] {
text.isEmpty ? rows : rows.filter { $0.id.localizedStandardContains(text) }
}
var body: some View {
VStack(alignment: .leading, spacing: 28) {
HStack(spacing: 16) {
Text("Library")
.font(.largeTitle.bold())
.fixedSize()
ExpandingSearchButton(text: $text)
.frame(maxWidth: .infinity, alignment: .trailing)
}
VStack(alignment: .leading, spacing: 22) {
ForEach(visibleRows) { row in
Label(row.id, systemImage: row.systemImage)
.font(.title3)
}
}
if visibleRows.isEmpty {
ContentUnavailableView.search(text: text)
}
Spacer(minLength: 0)
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
#Preview {
ExpandingSearchButtonDemo()
}