Pattern · Tab Bars

New

Accessory Tab Bar SwiftUI Block

A tab bar with a live accessory above it for what is playing or running, folding inline into the bar as the content scrolls.

A compact accessory above the tab bar shows what is live: artwork, title and subtitle, a progress line and play and pause or one control of your own, opening its full view when tapped. Scrolling down folds it inline into the bar beside the selected tab, and scrolling up brings it back. On iOS 26 it is the system bar's tabViewBottomAccessory, with the block's own bar before.

  • SwiftUI
  • iOS 17.0+
  • accessory-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.

  • A live accessory row with artwork, progress and one control
  • Folds inline into the bar on scroll down and returns on scroll up
  • The system tabViewBottomAccessory on iOS 26, a matching custom bar on iOS 17
  • Play and pause, or your own control, with working and failed states
  • Tap the accessory to open its full view through your closure

02

Implementation notes

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

A compact accessory sits just above the tab bar for whatever is live: the episode playing, the workout running, the order on its way. It shows an artwork square, a title and subtitle, a thin progress line and one control, play and pause by default; tap the row to open its full view through your closure. The control runs your closure and shows a spinner while it does, and a thrown error replaces the subtitle for a few seconds. As the content scrolls down the bar steps aside: the tab capsule shrinks to a round button showing the selected tab, and the accessory folds down beside it, with its title and control. Scrolling back up, or tapping that symbol, brings the tabs and the full accessory back. On iOS 26 the block is the system TabView with tabViewBottomAccessory and tabBarMinimizeBehavior(.onScrollDown), in Liquid Glass; before iOS 26 it draws its own bar in the same shape: the tabs in a floating capsule, the selected one on a quiet pill, with the accessory in a capsule above.

Notes

  • AccessoryTabBar(selection:items:accessory:style:onControl:onExpand:content:) is generic over your own tab type. content builds each tab's screen; accessory is nil when nothing is live, and the accessory goes.
  • 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, on a soft fade of the background. Put a NavigationStack inside each tab.
  • style: .automatic uses the system bar and accessory on iOS 26 and the block's own before; .custom draws the block's own on every version, so the look is the same everywhere.
  • The block's own bar learns about scrolling from content you mark with .accessoryTabBarScrollTracking(), inside each tab's ScrollView. It folds after 40 points of scrolling down and returns after 40 points up, and always near the top. The system bar on iOS 26 watches scrolling itself, so the modifier does nothing there.
  • AccessoryTabBarItem(_:title:symbol:badge:) (also AccessoryTabBar.Item) is a tab.
  • AccessoryTabBarAccessory (also AccessoryTabBar.Accessory) has an id, title, subtitle, an SF Symbol for the artwork, progress from 0 to 1 (nil hides the line), isActive and a Control: .playPause (Pause while active, Play otherwise), .action(symbol:label:) for one action of your own, such as ending a workout, or .none. Pass a new value as it changes. .sample is an episode playing, .workout a run with End workout, .order a delivery with no control.
  • onControl is async throws: ask your player to pause or play, then update isActive. A thrown error's errorDescription (or "That didn't work. Try again.") shows in the subtitle in the danger colour for four seconds. Nil hides the control. onExpand opens your full view: a player sheet, the workout or the order.
  • VoiceOver reads the accessory as one element: its title, subtitle, percentage and Playing or Paused, with a hint that it opens the full view; the control is labelled Pause, Play or your label. Tabs read their badges and the selected trait. The bar keeps its size up to xLarge text; at accessibility sizes the labels step aside and a long press shows the large content viewer.
  • Reduce Motion turns the fold and the accessory's rise into fades, and the waveform symbol holds still.
  • AccessoryTabBarExample at the end of AccessoryTabBar.swift is a listening app: Home, Library, Search and Downloads, an episode playing whose line creeps on, a play control that takes a moment, and a Now Playing sheet. Pass custom: true to see the block's own bar on iOS 26.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The custom bar's capsule is color.surface with a separator hairline and the e2 shadow, and the selected tab's pill is fill. Labels are in type.caption at medium weight, textPrimary when selected and textSecondary otherwise. Badges are textPrimary capsules with surface figures.
  • The accessory is a surface capsule with a separator hairline and the e2 elevation, since it floats over scrolling content. The artwork square is fill at radius.sm with its symbol in textSecondary. The title is type.subhead at medium weight, the subtitle type.footnote in textSecondary, the control textPrimary, and the progress line textPrimary on a fill track.
  • Folded, the tab button is a surface circle and the accessory keeps its capsule, its artwork smaller at radius.xs. Behind the bar the background fades in from bg at 0 to 90%.
  • On iOS 26 the system draws the bar and the accessory's glass; the tab tint is textPrimary.
  • The fold moves with motion.standard; controls cross-fade with motion.snappy.

Requirements

  • iOS 26 for the system bar and accessory. Earlier versions use the block's own.