diff --git a/.changeset/khaki-maps-lay.md b/.changeset/khaki-maps-lay.md
new file mode 100644
index 0000000..bb4b9d7
--- /dev/null
+++ b/.changeset/khaki-maps-lay.md
@@ -0,0 +1,5 @@
+---
+'react-native-bottom-tabs': minor
+---
+
+fix: adjust the behavior of empty string for badge prop
diff --git a/.changeset/ready-paths-train.md b/.changeset/ready-paths-train.md
new file mode 100644
index 0000000..3e7de55
--- /dev/null
+++ b/.changeset/ready-paths-train.md
@@ -0,0 +1,5 @@
+---
+'react-native-bottom-tabs': patch
+---
+
+feat: implement iOS 26 minimizeBehavior feature
diff --git a/README.md b/README.md
index 0154a3a..7f44a3f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock
index 92798c7..b0a038e 100644
--- a/apps/example/ios/Podfile.lock
+++ b/apps/example/ios/Podfile.lock
@@ -1180,7 +1180,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- - react-native-bottom-tabs (0.9.1):
+ - react-native-bottom-tabs (0.9.2):
- DoubleConversion
- glog
- RCT-Folly (= 2024.11.18.00)
@@ -1931,7 +1931,7 @@ SPEC CHECKSUMS:
React-logger: 1935d6e6461e9c8be4c87af56c56a4876021171e
React-Mapbuffer: 212171f037e3b22e6c2df839aa826806da480b85
React-microtasksnativemodule: a0ffd165c39251512d8cf51e9e8f5719dabc38b6
- react-native-bottom-tabs: 7c7ee94435e518759b414e89068956096a1adec7
+ react-native-bottom-tabs: 6ee03990297f7e37f5c1dd6f5259cee851733d4f
react-native-safe-area-context: e54b360402f089600c2fb0d825d1d3d918b99e15
React-nativeconfig: cb207ebba7cafce30657c7ad9f1587a8f32e4564
React-NativeModulesApple: 76a5d35322908fbc88871e6dd20433bea2b8b2db
diff --git a/apps/example/src/Examples/FourTabs.tsx b/apps/example/src/Examples/FourTabs.tsx
index 93ed60f..4c32ece 100644
--- a/apps/example/src/Examples/FourTabs.tsx
+++ b/apps/example/src/Examples/FourTabs.tsx
@@ -52,6 +52,7 @@ export default function FourTabs({
key: 'contacts',
focusedIcon: require('../../assets/icons/person_dark.png'),
title: 'Contacts',
+ badge: ' ',
},
{
key: 'chat',
diff --git a/apps/example/src/Examples/SFSymbols.tsx b/apps/example/src/Examples/SFSymbols.tsx
index 591424d..1e7b48c 100644
--- a/apps/example/src/Examples/SFSymbols.tsx
+++ b/apps/example/src/Examples/SFSymbols.tsx
@@ -47,6 +47,7 @@ export default function SFSymbols() {
return (
);
}
@@ -90,6 +91,7 @@ const renderScene = SceneMap({
#### `navigationState`
State for the tab view. The state should contain:
+
- `routes`: Array of route objects containing `key` and `title` props
- `index`: Current selected tab index
@@ -106,6 +108,7 @@ Callback that is called when the tab index changes.
#### `labeled`
Whether to show labels in tabs. When `false`, only icons will be displayed.
+
- Type: `boolean`
- Default : `true`
- Default : `false`
@@ -113,6 +116,7 @@ Whether to show labels in tabs. When `false`, only icons will be displayed.
#### `sidebarAdaptable`
A tab bar style that adapts to each platform:
+
- iPadOS: Top tab bar that can adapt into a sidebar
- iOS: Bottom tab bar
- macOS/tvOS: Sidebar
@@ -121,19 +125,21 @@ A tab bar style that adapts to each platform:
#### `disablePageAnimations`
Whether to disable animations between tabs.
+
- Type: `boolean`
#### `hapticFeedbackEnabled`
Whether to enable haptic feedback on tab press.
+
- Type: `boolean`
- Default: `false`
-
#### `tabLabelStyle`
Object containing styles for the tab label.
Supported properties:
+
- `fontFamily`
- `fontSize`
- `fontWeight`
@@ -141,11 +147,31 @@ Supported properties:
#### `scrollEdgeAppearance`
Appearance attributes for the tab bar when a scroll view is at the bottom.
+
- Type: `'default' | 'opaque' | 'transparent'`
+#### `minimizeBehavior`
+
+Controls how the tab bar behaves when content is scrolled.
+
+- Type: `'automatic' | 'onScrollDown' | 'onScrollUp' | 'never'`
+- Default: `undefined` (uses system default)
+
+Options:
+
+- `automatic`: Platform determines the behavior
+- `onScrollDown`: Tab bar minimizes when scrolling down
+- `onScrollUp`: Tab bar minimizes when scrolling up
+- `never`: Tab bar never minimizes
+
+:::note
+This feature requires iOS 26.0 or later and is only available on iOS. On older versions, this prop is ignored.
+:::
+
#### `tabBarActiveTintColor`
Color for the active tab.
+
- Type: `ColorValue`
#### `tabBarInactiveTintColor`
@@ -165,11 +191,13 @@ Supported properties:
#### `translucent`
Whether the tab bar is translucent.
+
- Type: `boolean`
#### `activeIndicatorColor`
Color of tab indicator.
+
- Type: `ColorValue`
### Route Configuration
@@ -190,21 +218,29 @@ Each route in the `routes` array can have the following properties:
#### `getLazy`
Function to determine if a screen should be lazy loaded.
+
- Default: Uses `route.lazy`
#### `getLabelText`
Function to get the label text for a tab.
+
- Default: Uses `route.title`
#### `getBadge`
Function to get the badge text for a tab.
+
- Default: Uses `route.badge`
+:::warning
+To display a badge without text (just a dot), you need to pass a string with a space character (`" "`).
+:::
+
#### `getActiveTintColor`
Function to get the active tint color for a tab.
+
- Default: Uses `route.activeTintColor`
#### `getIcon`
@@ -213,7 +249,6 @@ Function to get the icon for a tab.
- Default: Uses `route.focusedIcon` and `route.unfocusedIcon`
-
#### `getHidden`
Function to determine if a tab should be hidden.
@@ -223,4 +258,5 @@ Function to determine if a tab should be hidden.
#### `getTestID`
Function to get the test ID for a tab item.
+
- Default: Uses `route.testID`
diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx
index 4c92ce3..c04a8c9 100644
--- a/docs/docs/docs/guides/usage-with-react-navigation.mdx
+++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx
@@ -158,6 +158,23 @@ Tab views using the sidebar adaptable style have an appearance
Whether to enable haptic feedback on tab press. Defaults to false.
+#### `minimizeBehavior`
+
+Controls how the tab bar behaves when content is scrolled.
+
+- Type: `'automatic' | 'onScrollDown' | 'onScrollUp' | 'never'`
+- Default: `undefined` (uses system default)
+
+Options:
+- `automatic`: Platform determines the behavior
+- `onScrollDown`: Tab bar minimizes when scrolling down
+- `onScrollUp`: Tab bar minimizes when scrolling up
+- `never`: Tab bar never minimizes
+
+:::note
+This feature requires iOS 26.0 or later and is only available on iOS. On older versions, this prop is ignored.
+:::
+
#### `tabLabelStyle`
Object containing styles for the tab label.
@@ -244,10 +261,47 @@ Function that given `{ focused: boolean }` returns `ImageSource` or `AppleIcon`
SF Symbols are only supported on Apple platforms.
:::
+:::warning
+
+Metro transformers that modify the import resolutions of images to something that is *not* React Native's `ImageSource` will break this. A notable community example of such is [react-native-svg-transformer](https://github.com/kristerkari/react-native-svg-transformer).
+
+For best results, avoid the use of such transformers altogether.
+
+If however, it is necessary to use such a transformer, you can modify your `metro.config.js` file to resolve the asset to `ImageSource` with another extension.
+
+For example, with `react-native-svg-transformer`, you can resolve the `svg` extension to use `react-native-svg-transformer`, and use an alternative extension like `svgx` to resolve it normally with React Native's default behavior.
+
+To do so, change the extension of your icon SVG file from `.svg` to `.svgx` and modify your `metro.config.js` file like so:
+
+```diff
+config.transformer.babelTransformerPath = require.resolve(
+ "react-native-svg-transformer"
+);
+config.resolver.assetExts = config.resolver.assetExts.filter(
+ (ext) => ext !== "svg"
+);
+
++ config.resolver.assetExts = [...config.resolver.assetExts, "svgx"];
+
+config.resolver.sourceExts = [...config.resolver.sourceExts, "svg"];
+```
+
+Then change your require calls like so:
+```
+tabBarIcon: () => require('person.svgx')
+```
+
+:::
+
+
#### `tabBarBadge`
Badge to show on the tab icon.
+:::warning
+To display a badge without text (just a dot), you need to pass a string with a space character (`" "`).
+:::
+
#### `tabBarItemHidden`
Whether the tab bar item is hidden.
diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt
index ab9b1dd..41547fd 100644
--- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt
+++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt
@@ -245,7 +245,7 @@ class ReactBottomNavigationView(context: Context) : LinearLayout(context) {
}
}
- if (item.badge.isNotEmpty()) {
+ if (item.badge?.isNotEmpty() == true) {
val badge = bottomNavigation.getOrCreateBadge(index)
badge.isVisible = true
badge.text = item.badge
diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
index 424e27f..0e6df67 100644
--- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
+++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt
@@ -13,7 +13,7 @@ import com.rcttabview.events.TabLongPressEvent
data class TabInfo(
val key: String,
val title: String,
- val badge: String,
+ val badge: String?,
val activeTintColor: Int?,
val hidden: Boolean,
val testID: String?
@@ -32,7 +32,7 @@ class RCTTabViewImpl {
TabInfo(
key = item.getString("key") ?: "",
title = item.getString("title") ?: "",
- badge = item.getString("badge") ?: "",
+ badge = if (item.hasKey("badge")) item.getString("badge") else null,
activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null,
hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false,
testID = item.getString("testID")
diff --git a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt
index 0f221a1..59be7ca 100644
--- a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt
+++ b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt
@@ -163,4 +163,7 @@ class RCTTabViewManager(context: ReactApplicationContext) :
override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) {
}
+
+ override fun setMinimizeBehavior(view: ReactBottomNavigationView?, value: String?) {
+ }
}
diff --git a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt
index 9a375ec..50eb1f5 100644
--- a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt
+++ b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt
@@ -137,6 +137,10 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager);
RCT_EXPORT_VIEW_PROPERTY(sidebarAdaptable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(labeled, BOOL)
diff --git a/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift
new file mode 100644
index 0000000..58d1855
--- /dev/null
+++ b/packages/react-native-bottom-tabs/ios/TabAppearModifier.swift
@@ -0,0 +1,33 @@
+import SwiftUI
+
+struct TabAppearContext {
+ let index: Int
+ let tabData: TabInfo
+ let props: TabViewProps
+ let updateTabBarAppearance: () -> Void
+ let onSelect: (_ key: String) -> Void
+}
+
+struct TabAppearModifier: ViewModifier {
+ let context: TabAppearContext
+
+ func body(content: Content) -> some View {
+ content.onAppear {
+ #if !os(macOS)
+ context.updateTabBarAppearance()
+ #endif
+
+ #if os(iOS)
+ if context.index >= 4, context.props.selectedPage != context.tabData.key {
+ context.onSelect(context.tabData.key)
+ }
+ #endif
+ }
+ }
+}
+
+extension View {
+ func tabAppear(using context: TabAppearContext) -> some View {
+ self.modifier(TabAppearModifier(context: context))
+ }
+}
diff --git a/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift
new file mode 100644
index 0000000..168565a
--- /dev/null
+++ b/packages/react-native-bottom-tabs/ios/TabView/AnyTabView.swift
@@ -0,0 +1,7 @@
+import SwiftUI
+
+public protocol AnyTabView: View {
+ var onLayout: (_ size: CGSize) -> Void { get }
+ var onSelect: (_ key: String) -> Void { get }
+ var updateTabBarAppearance: () -> Void { get }
+}
diff --git a/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift
new file mode 100644
index 0000000..209bfaa
--- /dev/null
+++ b/packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift
@@ -0,0 +1,56 @@
+import SwiftUI
+
+struct LegacyTabView: AnyTabView {
+ @ObservedObject var props: TabViewProps
+
+ var onLayout: (CGSize) -> Void
+ var onSelect: (String) -> Void
+ var updateTabBarAppearance: () -> Void
+
+ @ViewBuilder
+ var body: some View {
+ TabView(selection: $props.selectedPage) {
+ ForEach(props.children.indices, id: \.self) { index in
+ renderTabItem(at: index)
+ }
+ .measureView { size in
+ onLayout(size)
+ }
+ }
+ .hideTabBar(props.tabBarHidden)
+ }
+
+ @ViewBuilder
+ private func renderTabItem(at index: Int) -> some View {
+ if let tabData = props.items[safe: index] {
+ let isFocused = props.selectedPage == tabData.key
+
+ if !tabData.hidden || isFocused {
+ let icon = props.icons[index]
+ let child = props.children[safe: index] ?? PlatformView()
+ let context = TabAppearContext(
+ index: index,
+ tabData: tabData,
+ props: props,
+ updateTabBarAppearance: updateTabBarAppearance,
+ onSelect: onSelect
+ )
+
+ RepresentableView(view: child)
+ .ignoresSafeArea(.container, edges: .all)
+ .tabItem {
+ TabItem(
+ title: tabData.title,
+ icon: icon,
+ sfSymbol: tabData.sfSymbol,
+ labeled: props.labeled
+ )
+ .accessibilityIdentifier(tabData.testID ?? "")
+ .tabBadge(tabData.badge)
+ .tabAppear(using: context)
+ }
+ .tag(tabData.key)
+ }
+ }
+ }
+}
diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
new file mode 100644
index 0000000..b0771d4
--- /dev/null
+++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
@@ -0,0 +1,56 @@
+import SwiftUI
+
+@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
+struct NewTabView: AnyTabView {
+ @ObservedObject var props: TabViewProps
+
+ var onLayout: (CGSize) -> Void
+ var onSelect: (String) -> Void
+ var updateTabBarAppearance: () -> Void
+
+ @ViewBuilder
+ var body: some View {
+ TabView(selection: $props.selectedPage) {
+ ForEach(props.children.indices, id: \.self) { index in
+ if let tabData = props.items[safe: index] {
+ let isFocused = props.selectedPage == tabData.key
+
+ if !tabData.hidden || isFocused {
+ let icon = props.icons[index]
+
+ let platformChild = props.children[safe: index] ?? PlatformView()
+ let child = RepresentableView(view: platformChild)
+ let context = TabAppearContext(
+ index: index,
+ tabData: tabData,
+ props: props,
+ updateTabBarAppearance: updateTabBarAppearance,
+ onSelect: onSelect
+ )
+
+ Tab(value: tabData.key) {
+ child
+ .ignoresSafeArea(.container, edges: .all)
+ .tabAppear(using: context)
+ } label: {
+ TabItem(
+ title: tabData.title,
+ icon: icon,
+ sfSymbol: tabData.sfSymbol,
+ labeled: props.labeled
+ )
+ }
+ .badge(
+ (tabData.badge == nil) ? nil : tabData.badge!.isEmpty ? nil : Text(tabData.badge!)
+ )
+ .accessibilityIdentifier(tabData.testID ?? "")
+ }
+ }
+ }
+ }
+ .measureView { size in
+ onLayout(size)
+ }
+ .hideTabBar(props.tabBarHidden)
+ }
+}
diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
index f87934f..4fb8050 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
@@ -3,16 +3,35 @@ import React
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
-/**
- SwiftUI implementation of TabView used to render React Native views.
- */
+/// SwiftUI implementation of TabView used to render React Native views.
struct TabViewImpl: View {
@ObservedObject var props: TabViewProps
-#if os(macOS)
- @Weak var tabBar: NSTabView?
-#else
- @Weak var tabBar: UITabBar?
-#endif
+ #if os(macOS)
+ @Weak var tabBar: NSTabView?
+ #else
+ @Weak var tabBar: UITabBar?
+ #endif
+
+ @ViewBuilder
+ var tabContent: some View {
+ if #available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) {
+ NewTabView(
+ props: props,
+ onLayout: onLayout,
+ onSelect: onSelect
+ ) {
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ } else {
+ LegacyTabView(
+ props: props,
+ onLayout: onLayout,
+ onSelect: onSelect
+ ) {
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ }
+ }
var onSelect: (_ key: String) -> Void
var onLongPress: (_ key: String) -> Void
@@ -20,127 +39,82 @@ struct TabViewImpl: View {
var onTabBarMeasured: (_ height: Int) -> Void
var body: some View {
- TabView(selection: $props.selectedPage) {
- ForEach(props.children.indices, id: \.self) { index in
- renderTabItem(at: index)
- }
- .measureView { size in
- onLayout(size)
- }
- }
-#if !os(tvOS) && !os(macOS) && !os(visionOS)
- .onTabItemEvent { index, isLongPress in
- let item = props.filteredItems[safe: index]
- guard let key = item?.key else { return }
-
- if isLongPress {
- onLongPress(key)
- emitHapticFeedback(longPress: true)
- } else {
- onSelect(key)
- emitHapticFeedback()
- }
- }
-#endif
- .introspectTabView { tabController in
-#if os(macOS)
- tabBar = tabController
-#else
- tabBar = tabController.tabBar
- if !props.tabBarHidden {
- onTabBarMeasured(
- Int(tabController.tabBar.frame.size.height)
- )
- }
-#endif
- }
-#if !os(macOS)
- .configureAppearance(props: props, tabBar: tabBar)
-#endif
- .tintColor(props.selectedActiveTintColor)
- .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false)
- .onChange(of: props.selectedPage ?? "") { newValue in
-#if !os(macOS)
- if props.disablePageAnimations {
- UIView.setAnimationsEnabled(false)
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- UIView.setAnimationsEnabled(true)
+ tabContent
+ .tabBarMinimizeBehavior(props.minimizeBehavior)
+ #if !os(tvOS) && !os(macOS) && !os(visionOS)
+ .onTabItemEvent { index, isLongPress in
+ let item = props.filteredItems[safe: index]
+ guard let key = item?.key else { return }
+
+ if isLongPress {
+ onLongPress(key)
+ emitHapticFeedback(longPress: true)
+ } else {
+ onSelect(key)
+ emitHapticFeedback()
+ }
}
+ #endif
+ .introspectTabView { tabController in
+ #if os(macOS)
+ tabBar = tabController
+ #else
+ tabBar = tabController.tabBar
+ if !props.tabBarHidden {
+ onTabBarMeasured(
+ Int(tabController.tabBar.frame.size.height)
+ )
+ }
+ #endif
+ }
+ #if !os(macOS)
+ .configureAppearance(props: props, tabBar: tabBar)
+ #endif
+ .tintColor(props.selectedActiveTintColor)
+ .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false)
+ .onChange(of: props.selectedPage ?? "") { newValue in
+ #if !os(macOS)
+ if props.disablePageAnimations {
+ UIView.setAnimationsEnabled(false)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ UIView.setAnimationsEnabled(true)
+ }
+ }
+ #endif
+ #if os(tvOS) || os(macOS) || os(visionOS)
+ onSelect(newValue)
+ #endif
}
-#endif
-#if os(tvOS) || os(macOS) || os(visionOS)
- onSelect(newValue)
-#endif
- }
- }
-
- @ViewBuilder
- private func renderTabItem(at index: Int) -> some View {
- let tabData = props.items[safe: index]
- let isHidden = tabData?.hidden ?? false
- let isFocused = props.selectedPage == tabData?.key
-
- if !isHidden || isFocused {
- let child = props.children[safe: index] ?? PlatformView()
- let icon = props.icons[index]
-
- RepresentableView(view: child)
- .ignoresSafeArea(.container, edges: .all)
- .tabItem {
- TabItem(
- title: tabData?.title,
- icon: icon,
- sfSymbol: tabData?.sfSymbol,
- labeled: props.labeled
- )
- .accessibilityIdentifier(tabData?.testID ?? "")
- }
- .tag(tabData?.key)
- .tabBadge(tabData?.badge)
- .hideTabBar(props.tabBarHidden)
- .onAppear {
-#if !os(macOS)
- updateTabBarAppearance(props: props, tabBar: tabBar)
-#endif
-
-#if os(iOS)
- guard index >= 4,
- let key = tabData?.key,
- props.selectedPage != key else { return }
- onSelect(key)
-#endif
- }
- }
}
func emitHapticFeedback(longPress: Bool = false) {
-#if os(iOS)
- if !props.hapticFeedbackEnabled {
- return
- }
+ #if os(iOS)
+ if !props.hapticFeedbackEnabled {
+ return
+ }
- if longPress {
- UINotificationFeedbackGenerator().notificationOccurred(.success)
- } else {
- UISelectionFeedbackGenerator().selectionChanged()
- }
-#endif
+ if longPress {
+ UINotificationFeedbackGenerator().notificationOccurred(.success)
+ } else {
+ UISelectionFeedbackGenerator().selectionChanged()
+ }
+ #endif
}
}
#if !os(macOS)
-private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) {
- guard let tabBar else { return }
+ private func updateTabBarAppearance(props: TabViewProps, tabBar: UITabBar?) {
+ guard let tabBar else { return }
- tabBar.isHidden = props.tabBarHidden
+ tabBar.isHidden = props.tabBarHidden
- if props.scrollEdgeAppearance == "transparent" {
- configureTransparentAppearance(tabBar: tabBar, props: props)
- return
- }
+ if props.scrollEdgeAppearance == "transparent" {
+ configureTransparentAppearance(tabBar: tabBar, props: props)
+ return
+ }
- configureStandardAppearance(tabBar: tabBar, props: props)
-}
+ configureStandardAppearance(tabBar: tabBar, props: props)
+ }
#endif
private func createFontAttributes(
@@ -169,85 +143,85 @@ private func createFontAttributes(
}
#if os(tvOS)
-let tabBarDefaultFontSize: CGFloat = 30.0
+ let tabBarDefaultFontSize: CGFloat = 30.0
#else
-let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize
+ let tabBarDefaultFontSize: CGFloat = UIFont.smallSystemFontSize
#endif
#if !os(macOS)
-private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) {
- tabBar.barTintColor = props.barTintColor
-#if !os(visionOS)
- tabBar.isTranslucent = props.translucent
-#endif
- tabBar.unselectedItemTintColor = props.inactiveTintColor
-
- guard let items = tabBar.items else { return }
-
- let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize
- let attributes = createFontAttributes(
- size: fontSize,
- family: props.fontFamily,
- weight: props.fontWeight,
- inactiveTintColor: nil
- )
+ private func configureTransparentAppearance(tabBar: UITabBar, props: TabViewProps) {
+ tabBar.barTintColor = props.barTintColor
+ #if !os(visionOS)
+ tabBar.isTranslucent = props.translucent
+ #endif
+ tabBar.unselectedItemTintColor = props.inactiveTintColor
+
+ guard let items = tabBar.items else { return }
+
+ let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize
+ let attributes = createFontAttributes(
+ size: fontSize,
+ family: props.fontFamily,
+ weight: props.fontWeight,
+ inactiveTintColor: nil
+ )
- items.forEach { item in
- item.setTitleTextAttributes(attributes, for: .normal)
+ items.forEach { item in
+ item.setTitleTextAttributes(attributes, for: .normal)
+ }
}
-}
-private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) {
- let appearance = UITabBarAppearance()
+ private func configureStandardAppearance(tabBar: UITabBar, props: TabViewProps) {
+ let appearance = UITabBarAppearance()
- // Configure background
- switch props.scrollEdgeAppearance {
- case "opaque":
- appearance.configureWithOpaqueBackground()
- default:
- appearance.configureWithDefaultBackground()
- }
+ // Configure background
+ switch props.scrollEdgeAppearance {
+ case "opaque":
+ appearance.configureWithOpaqueBackground()
+ default:
+ appearance.configureWithDefaultBackground()
+ }
- if props.translucent == false {
- appearance.configureWithOpaqueBackground()
- }
+ if props.translucent == false {
+ appearance.configureWithOpaqueBackground()
+ }
- if props.barTintColor != nil {
- appearance.backgroundColor = props.barTintColor
- }
+ if props.barTintColor != nil {
+ appearance.backgroundColor = props.barTintColor
+ }
- // Configure item appearance
- let itemAppearance = UITabBarItemAppearance()
- let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize
+ // Configure item appearance
+ let itemAppearance = UITabBarItemAppearance()
+ let fontSize = props.fontSize != nil ? CGFloat(props.fontSize!) : tabBarDefaultFontSize
- var attributes = createFontAttributes(
- size: fontSize,
- family: props.fontFamily,
- weight: props.fontWeight,
- inactiveTintColor: props.inactiveTintColor
- )
+ var attributes = createFontAttributes(
+ size: fontSize,
+ family: props.fontFamily,
+ weight: props.fontWeight,
+ inactiveTintColor: props.inactiveTintColor
+ )
- if let inactiveTintColor = props.inactiveTintColor {
- attributes[.foregroundColor] = inactiveTintColor
- }
+ if let inactiveTintColor = props.inactiveTintColor {
+ attributes[.foregroundColor] = inactiveTintColor
+ }
- if let inactiveTintColor = props.inactiveTintColor {
- itemAppearance.normal.iconColor = inactiveTintColor
- }
+ if let inactiveTintColor = props.inactiveTintColor {
+ itemAppearance.normal.iconColor = inactiveTintColor
+ }
- itemAppearance.normal.titleTextAttributes = attributes
+ itemAppearance.normal.titleTextAttributes = attributes
- // Apply item appearance to all layouts
- appearance.stackedLayoutAppearance = itemAppearance
- appearance.inlineLayoutAppearance = itemAppearance
- appearance.compactInlineLayoutAppearance = itemAppearance
+ // Apply item appearance to all layouts
+ appearance.stackedLayoutAppearance = itemAppearance
+ appearance.inlineLayoutAppearance = itemAppearance
+ appearance.compactInlineLayoutAppearance = itemAppearance
- // Apply final appearance
- tabBar.standardAppearance = appearance
- if #available(iOS 15.0, *) {
- tabBar.scrollEdgeAppearance = appearance.copy()
+ // Apply final appearance
+ tabBar.standardAppearance = appearance
+ if #available(iOS 15.0, *) {
+ tabBar.scrollEdgeAppearance = appearance.copy()
+ }
}
-}
#endif
extension View {
@@ -255,11 +229,11 @@ extension View {
func getSidebarAdaptable(enabled: Bool) -> some View {
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) {
if enabled {
-#if compiler(>=6.0)
- self.tabViewStyle(.sidebarAdaptable)
-#else
- self
-#endif
+ #if compiler(>=6.0)
+ self.tabViewStyle(.sidebarAdaptable)
+ #else
+ self
+ #endif
} else {
self
}
@@ -271,12 +245,12 @@ extension View {
@ViewBuilder
func tabBadge(_ data: String?) -> some View {
if #available(iOS 15.0, macOS 15.0, visionOS 2.0, tvOS 15.0, *) {
- if let data, !data.isEmpty {
-#if !os(tvOS)
- self.badge(data)
-#else
- self
-#endif
+ if let data {
+ #if !os(tvOS)
+ self.badge(data)
+ #else
+ self
+ #endif
} else {
self
}
@@ -285,39 +259,39 @@ extension View {
}
}
-#if !os(macOS)
- @ViewBuilder
- func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View {
- self
- .onChange(of: props.barTintColor) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.scrollEdgeAppearance) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.translucent) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.inactiveTintColor) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.selectedActiveTintColor) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.fontSize) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.fontFamily) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.fontWeight) { _ in
- updateTabBarAppearance(props: props, tabBar: tabBar)
- }
- .onChange(of: props.tabBarHidden) { newValue in
- tabBar?.isHidden = newValue
- }
- }
-#endif
+ #if !os(macOS)
+ @ViewBuilder
+ func configureAppearance(props: TabViewProps, tabBar: UITabBar?) -> some View {
+ self
+ .onChange(of: props.barTintColor) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.scrollEdgeAppearance) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.translucent) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.inactiveTintColor) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.selectedActiveTintColor) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.fontSize) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.fontFamily) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.fontWeight) { _ in
+ updateTabBarAppearance(props: props, tabBar: tabBar)
+ }
+ .onChange(of: props.tabBarHidden) { newValue in
+ tabBar?.isHidden = newValue
+ }
+ }
+ #endif
@ViewBuilder
func tintColor(_ color: PlatformColor?) -> some View {
@@ -333,22 +307,39 @@ extension View {
}
}
+ @ViewBuilder
+ func tabBarMinimizeBehavior(_ behavior: MinimizeBehavior?) -> some View {
+ #if compiler(>=6.2)
+ if #available(iOS 26.0, *) {
+ if let behavior {
+ self.tabBarMinimizeBehavior(behavior.convert())
+ } else {
+ self
+ }
+ } else {
+ self
+ }
+ #else
+ self
+ #endif
+ }
+
@ViewBuilder
func hideTabBar(_ flag: Bool) -> some View {
-#if !os(macOS)
- if flag {
- if #available(iOS 16.0, tvOS 16.0, *) {
- self.toolbar(.hidden, for: .tabBar)
+ #if !os(macOS)
+ if flag {
+ if #available(iOS 16.0, tvOS 16.0, *) {
+ self.toolbar(.hidden, for: .tabBar)
+ } else {
+ // We fallback to isHidden on UITabBar
+ self
+ }
} else {
- // We fallback to isHidden on UITabBar
self
}
- } else {
+ #else
self
- }
-#else
- self
-#endif
+ #endif
}
// Allows TabView to use unfilled SFSymbols.
diff --git a/packages/react-native-bottom-tabs/ios/TabViewProps.swift b/packages/react-native-bottom-tabs/ios/TabViewProps.swift
index 131c7cf..833aca9 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewProps.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewProps.swift
@@ -1,5 +1,28 @@
import SwiftUI
+internal enum MinimizeBehavior: String {
+ case automatic
+ case never
+ case onScrollUp
+ case onScrollDown
+
+#if compiler(>=6.2)
+ @available(iOS 26.0, *)
+ func convert() -> TabBarMinimizeBehavior {
+ switch self {
+ case .automatic:
+ return .automatic
+ case .never:
+ return .never
+ case .onScrollUp:
+ return .onScrollUp
+ case .onScrollDown:
+ return .onScrollDown
+ }
+ }
+#endif
+}
+
/**
Props that component accepts. SwiftUI view gets re-rendered when ObservableObject changes.
*/
@@ -10,6 +33,7 @@ class TabViewProps: ObservableObject {
@Published var icons: [Int: PlatformImage] = [:]
@Published var sidebarAdaptable: Bool?
@Published var labeled: Bool = false
+ @Published var minimizeBehavior: MinimizeBehavior?
@Published var scrollEdgeAppearance: String?
@Published var barTintColor: PlatformColor?
@Published var activeTintColor: PlatformColor?
diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
index 135c9a9..9a30471 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
@@ -8,7 +8,7 @@ import SwiftUI
public final class TabInfo: NSObject {
public let key: String
public let title: String
- public let badge: String
+ public let badge: String?
public let sfSymbol: String
public let activeTintColor: PlatformColor?
public let hidden: Bool
@@ -17,7 +17,7 @@ public final class TabInfo: NSObject {
public init(
key: String,
title: String,
- badge: String,
+ badge: String?,
sfSymbol: String,
activeTintColor: PlatformColor?,
hidden: Bool,
@@ -102,6 +102,12 @@ public final class TabInfo: NSObject {
}
}
+ @objc public var minimizeBehavior: NSString? {
+ didSet {
+ props.minimizeBehavior = MinimizeBehavior(rawValue: minimizeBehavior as? String ?? "")
+ }
+ }
+
@objc public var translucent: Bool = true {
didSet {
props.translucent = translucent
@@ -263,7 +269,7 @@ public final class TabInfo: NSObject {
TabInfo(
key: itemDict["key"] as? String ?? "",
title: itemDict["title"] as? String ?? "",
- badge: itemDict["badge"] as? String ?? "",
+ badge: itemDict["badge"] as? String,
sfSymbol: itemDict["sfSymbol"] as? String ?? "",
activeTintColor: RCTConvert.uiColor(itemDict["activeTintColor"] as? NSNumber),
hidden: itemDict["hidden"] as? Bool ?? false,
diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx
index dd71356..ed30dd1 100644
--- a/packages/react-native-bottom-tabs/src/TabView.tsx
+++ b/packages/react-native-bottom-tabs/src/TabView.tsx
@@ -54,6 +54,11 @@ interface Props {
* Describes the appearance attributes for the tabBar to use when an observable scroll view is scrolled to the bottom. (iOS only)
*/
scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
+
+ /**
+ * Behavior for minimizing the tab bar. (iOS 26+)
+ */
+ minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never';
/**
* Active tab color.
*/
@@ -170,10 +175,10 @@ const TabView = ({
renderScene,
onIndexChange,
onTabLongPress,
- getBadge,
rippleColor,
tabBarActiveTintColor: activeTintColor,
tabBarInactiveTintColor: inactiveTintColor,
+ getBadge = ({ route }: { route: Route }) => route.badge,
getLazy = ({ route }: { route: Route }) => route.lazy,
getLabelText = ({ route }: { route: Route }) => route.title,
getIcon = ({ route, focused }: { route: Route; focused: boolean }) =>
diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts
index 1a7224d..4eca038 100644
--- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts
+++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts
@@ -52,6 +52,7 @@ export interface TabViewProps extends ViewProps {
disablePageAnimations?: boolean;
activeIndicatorColor?: ColorValue;
hapticFeedbackEnabled?: boolean;
+ minimizeBehavior?: string;
fontFamily?: string;
fontWeight?: string;
fontSize?: Int32;