uikit_tab_bar 0.1.0
uikit_tab_bar: ^0.1.0 copied to clipboard
Native iOS 26+ tab bar (UITabBarController, Liquid Glass) for Flutter with minimize on scroll, accessory and search tab; CupertinoTabBar fallback elsewhere.
uikit_tab_bar #
The original iOS tab bar for Flutter: a real UITabBarController with
Liquid Glass on iOS 26+, made controllable from Dart. It does not imitate
the bar. Everywhere else (iOS < 26, Android, web, desktop) the same
configuration renders a standard CupertinoTabBar.
| Glass bar with accessory | Minimized while scrolling | Native search field | Dark |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Features #
| Feature | Native bar | Fallback |
|---|---|---|
Tabs with SF Symbol, Flutter IconData or any ImageProvider icon, selected icon |
✓ (selected icon: iOS 26.1+) | ✓ (fallbackIcon for SF Symbols) |
| Badges, disabled tabs, hidden tabs | ✓ | ✓ |
Controlled selection, onReselect (scroll to top) |
✓ | ✓ |
Minimize on scroll (onScrollDown / onScrollUp) |
✓ | – |
| Bottom accessory with icon, title, subtitle, action buttons | ✓ (regular and inline) | – |
| System search tab with native search field, text to Dart | ✓ | regular tab (bring your own field) |
| Prominent tab | ✓ iOS 27+ | – |
| Hide with the system animation | ✓ | ✓ (without animation) |
| Tint, selected color, title font from app assets, title offset, badge colors, offset and font | ✓ | tint, badge colors, offset and font |
| Light/dark following the app theme (not only the system) | ✓ | ✓ |
| Reported geometry (bottom inset, frames, minimized state) | ✓ | ✓ |
Usage #
UIKitTabScaffold(
tabBar: UIKitTabBar(
tabs: const [
UIKitTab(
id: 'home',
label: 'Home',
icon: UIKitTabIcon.symbol('house'),
selectedIcon: UIKitTabIcon.symbol('house.fill'),
fallbackIcon: Icon(CupertinoIcons.house),
),
UIKitTab(id: 'events', label: 'Events', icon: UIKitTabIcon.icon(Icons.event), badge: '3'),
],
selectedId: selected,
onSelected: (id) => setState(() => selected = id),
onReselect: (id) => scrollToTop(id),
minimizeBehavior: UIKitTabBarMinimizeBehavior.onScrollDown,
searchTab: UIKitSearchTab(onChanged: (text) => setState(() => query = text)),
accessory: UIKitTabAccessory(title: 'Now playing', onTap: openPlayer),
style: UIKitTabBarStyle(tintColor: Theme.of(context).colorScheme.primary),
),
body: IndexedStack(index: indexOf(selected), children: pages),
)
- The native bar floats over the content.
UIKitTabScaffoldlets the body extend behind it and setsMediaQuery.padding.bottomof the body to the bar's reported height (83 pt on an iPhone Air, 139 pt with an accessory), soSafeAreaand list paddings keep content clear of the bar. - Selection is controlled. A tap calls
onSelected. If you don't changeselectedId, the bar goes back, which is how you veto a tap. A tap on the selected tab callsonReselectinstead. - For minimizing, the scaffold forwards scroll notifications of the body's
primary vertical scrollable (
depth == 0). Tabs kept alive offstage, for example in anIndexedStack, are ignored. If you place the bar yourself, pass aUIKitTabBarControllerand wrap the content inUIKitTabBarScrollListener. - Read
UIKitTabBarController.geometry(oronGeometryChanged) for the bar's height, frames and minimized state. - Put
UIKitTabScaffoldoutside any widget that resizes for the keyboard (or setresizeToAvoidBottomInset: falsethere), so the bar stays at the bottom edge like a native one. - At most 5 tabs including the search tab. More would open UIKit's "More" screen inside the bar.
example/ shows every option, switchable at runtime.
What works under Liquid Glass #
Measured on iOS 27 (iPhone Air simulator). Only what takes effect is
offered in UIKitTabBarStyle.
| UIKit property | Under Liquid Glass | In the API |
|---|---|---|
UITabBar.tintColor |
works (selected tab) | tintColor |
Item appearance, selected: iconColor, title color |
works | selectedColor |
| Item appearance: title font (size, weight, custom font) | works | titleFontSize, titleFontWeight, titleFontAsset |
Item appearance: titlePositionAdjustment |
works | titleOffset |
Item appearance: badgeBackgroundColor, badgeTextAttributes |
works | badgeColor, badgeTextColor |
Item appearance: badgeTextAttributes font (size, weight) |
works; the badge resizes with the text | badgeFontSize, badgeFontWeight |
Item appearance: badgePositionAdjustment |
works for the whole bar; horizontal points toward the leading edge (the API flips it so dx points toward the end) |
badgeOffset |
The same per tab (UITabBarItem.standardAppearance) |
no effect | – |
Item appearance: badgeTitlePositionAdjustment |
works, but moves the text out of the badge | – |
overrideUserInterfaceStyle |
works | brightness (default: app theme) |
UITab.isEnabled |
works (dimmed) | UIKitTab.enabled |
unselectedItemTintColor, item appearance normal iconColor / title color |
no effect (the system picks legible colors) | – |
UITabBarAppearance.backgroundColor, barTintColor, shadow |
no effect (glass) | – |
selectionIndicatorTintColor |
no visible effect | – |
UITab.subtitle |
sidebar only, not shown in the bar | UIKitTab.subtitle (passed through) |
UITab.isHidden |
sidebar customization only | UIKitTab.hidden is implemented by removing the tab |
How it works #
- A
UITabBarControlleris a child view controller inside a platform view that covers the bottom 200 pt of the screen. Its tab content controllers are transparent placeholders; the content is Flutter, behind them. - UIKit reports the frames of the glass pills, accessory and search field after every layout change and during animations. A render object lets only touches inside those frames reach the platform view. Everything else goes to Flutter.
- UIKit only minimizes for real pans of the content scroll view. An
invisible proxy
UIScrollViewstands in for the Flutter scrollable: its pan recognizer is moved onto the FlutterView, so the user's finger drives it while Flutter scrolls from the same touches. Outside a drag it mirrors Flutter's position. Behavior was verified against a pure UIKit reference app with the same gestures. - Dart sends the complete state on every change; Swift diffs it and applies
the changes in one batch (
performBatchUpdateson iOS 27+). Hot reload just works. - Non-symbol icons are rendered in Dart at device scale and sent once as template images. Fonts from the app's assets are registered natively.
Limits #
- Minimize gating. While minimizing is on and the selected tab is
scrollable, any vertical pan on the Flutter view counts, also on a sheet
or dialog covering the content. Set
minimizeBehaviortonever(or hide the bar) while such overlays are open. - Expand at the top after a Flutter fling is triggered explicitly by re-applying the minimize behavior, because UIKit ignores the programmatic scroll position. A scroll back up during a drag expands natively.
- Accessory content is native, described by its fields. Arbitrary Flutter widgets inside it would need a second Flutter engine.
- Search mode. While the native search field is active, the platform
view fills the screen so UIKit shows the field at the top, as natively;
UIKitTabScaffoldpads the content top byUIKitTabBarGeometry.topInset. - iPad. The phone-style bottom bar is shown (compact size class). The top tab bar, sidebar, tab groups and drag and drop are not supported.
- Overlays. Flutter content painted above the bar region (dialogs, sheets, snack bars) is composited in separate layers, as with every platform view.
Platform support #
- iOS 26+: native bar. iOS 15.6 to 25:
CupertinoTabBar. The plugin's minimum iOS version is 15.6; newer APIs are behind#available. - Swift Package Manager and CocoaPods.
- All other platforms:
CupertinoTabBar(the plugin has no native part there).




