Pattern · Tab Bars

New

Search Tab Bar SwiftUI Block

A floating tab bar with search set apart: tap it and the tabs fold away as it opens into a field over your recent searches.

The tabs sit in one floating capsule with the selected tab on a quiet pill, and a round search button stands beside it, the way iOS 26 apps place search. Tapping search folds the tabs into one round button showing the current tab and opens the button into a field with the keyboard up. The screen above shows recent searches, then your results as the query changes, and tapping the folded tab goes back.

  • SwiftUI
  • iOS 17.0+
  • search-tab-bar
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • Tabs in a floating capsule, the selected one on a sliding pill
  • Search set apart, opening into a field as the tabs fold away
  • Recent searches with Clear, kept in your own store
  • Your results view for the query, in place of the tab
  • Badges, tap again to scroll to top, and tabs that keep their state

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

The tabs float in one capsule at the foot of the screen, each a symbol over its label, with the selected tab on a quiet pill that slides between them. A round search button stands apart beside the capsule, the way iOS 26 apps place search. Tapping it folds the tabs into a single round button showing the current tab and opens the search button into a field with the keyboard up. The screen above shows recent searches with Clear, and as the person types it shows your results for the query in place of the tab. Pressing Search, or picking a recent, adds the query to the front of the recents and calls your closure. Tapping the folded tab clears the field and returns to the tab. Tabs keep their content alive once visited, as TabView does, and tapping the selected tab again calls onReselect.

Notes

  • SearchTabBar(selection:items:query:prompt:recents:onSubmit:onReselect:content:results:) is generic over your own tab type (any Hashable & Sendable value, usually an enum). content builds the screen for a tab and results builds the results for a trimmed, non-empty query.
  • A tab bar is the root of a screen, so this block owns its container rather than being a section: it lays your content out full size and floats the bar over its foot with safeAreaInset(edge: .bottom), so your scroll views end above it. Put a NavigationStack inside each tab's content, not around the bar.
  • SearchTabBarItem(_:title:symbol:badge:) is a tab: your value, its label, an outline SF Symbol (the selected tab shows its fill variant) and an optional count. Over 99 reads "99+"; nil or 0 hides it. Two to four tabs fit the capsule.
  • query and recents are bindings the app owns, so it can filter as the person types and keep recents across launches. Recents hold up to eight, newest first, without repeats. An empty list shows a short prompt instead.
  • onSubmit runs on the main actor with the trimmed query. Filter in results as the query changes, and fetch or log in onSubmit.
  • Haptics: selection when the tab changes.
  • VoiceOver reads each tab with its badge ("Library, 2 new") and the selected trait. The search button is "Search", and the folded tab reads "Back to Library". At accessibility sizes the labels step aside and a long press shows the system's large content viewer, as system tab bars do.
  • Reduce Motion turns the fold and the opening field into short fades, and the press scale off.
  • SearchTabBarExample at the end of SearchTabBar.swift is a listening app's root: Listen Now, Library with two new episodes and Browse, three recent searches, and results that filter a catalog of shows and episodes as you type.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The capsule, the round buttons and the field are color.surface with a separator hairline and the e2 shadow. The selected tab's pill is fill. Tab labels are type.caption at medium weight; symbols and labels are textPrimary when selected and textSecondary otherwise. Badges are textPrimary capsules with surface figures.
  • The field's text is type.body in textPrimary, its symbol textSecondary and its clear button textTertiary. The caret takes the app's tint, the block's one accent.
  • Recent searches sit on a surface card at radius.lg, rows of type.body with textSecondary symbols and inset hairlines, under a type.headline title with Clear in type.subhead. Results and recents sit on bg.
  • The fold and the field open with motion.standard; the clear button fades with motion.snappy.