Pattern · Tab Bars

New

Top Tabs SwiftUI Block

A scrolling strip of top tabs over swipeable pages, its indicator tracking the swipe and the strip keeping the selected tab in view.

A strip of tabs over a pager of your pages. The accent indicator follows the pager continuously, stretching to each tab's width, tapping a tab pages to it, and the strip scrolls to keep the selected tab in the middle. Tabs can show counts, and three or fewer share the width while more scroll.

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

  • An indicator that tracks the swipe and stretches to each tab's width
  • Tap a tab to page to it, the strip scrolling to keep it centred
  • Counts on tabs, in a quiet capsule
  • Few tabs fill the width, many tabs scroll
  • Generic over your own tab type, with lazily built pages

02

Implementation notes

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

A strip of tabs sits over a hairline, and a pager of your pages sits under it. A thin accent indicator under the strip follows the pager as it moves: drag a page halfway and the indicator is halfway to the next tab, stretching or shrinking to its width. Tapping a tab pages straight to it, the indicator gliding along, and the strip scrolls to bring the selected tab to the middle. Tabs can show a count in a small capsule after the title. Three tabs or fewer share the strip's width equally; with more, each tab takes its own width and the strip scrolls. The selected title is textPrimary and the rest textSecondary. Pages are built lazily as they come near.

Notes

  • TopTabs(selection:items:sizing:page:) is generic over your own tab type. page builds the page for a tab; each page may hold its own vertical ScrollView.
  • The block owns its paging container, a horizontal ScrollView with .scrollTargetBehavior(.paging) and .scrollPosition(id:), so give it the height it should fill: under an inline navigation title, or in a sheet. It brings no background or padding.
  • TopTabsItem(_:title:count:) (also TopTabs.Item) is a tab: your value, a one-line title and an optional count. 0 still shows "0"; nil hides the capsule.
  • sizing is .automatic (fill with three tabs or fewer, scroll with more), .fill or .scroll.
  • Set selection from your own code and the pager pages there. A swipe sets selection once the page settles.
  • The indicator's place is worked out from the pager's offset and each tab's measured width, so it tracks the finger at any speed and in either direction.
  • Haptics: selection when the tab changes.
  • VoiceOver reads each tab as a button with its count and the selected trait; only the current page is exposed. Tab titles grow with Dynamic Type and the strip scrolls to fit them.
  • Reduce Motion pages without the slide when a tab is tapped.
  • TopTabsExample at the end of TopTabs.swift is a reading app's feed with nine tabs (Following with 12, Events with 3), each page a card of articles. The file's "Few tabs" preview shows three tabs filling the width.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • Titles are type.subhead at medium weight, in textPrimary when selected and textSecondary otherwise. Counts are type.caption with monospaced digits on a fill capsule.
  • The indicator is the block's one accent: a 2 point accent capsule. An SPHairline in separator runs under the strip.
  • Paging and the strip's scroll use motion.smooth; title colours change with motion.snappy.