diff --git a/.changeset/sidebar-adaptable-large-iphone.md b/.changeset/sidebar-adaptable-large-iphone.md
new file mode 100644
index 00000000..a78a6ca7
--- /dev/null
+++ b/.changeset/sidebar-adaptable-large-iphone.md
@@ -0,0 +1,5 @@
+---
+'react-native-bottom-tabs': minor
+---
+
+Enable `sidebarAdaptable` on iOS for large enough screens.
diff --git a/docs/docs/docs/getting-started/how-is-it-different.mdx b/docs/docs/docs/getting-started/how-is-it-different.mdx
index 42df26b6..8060fbb5 100644
--- a/docs/docs/docs/getting-started/how-is-it-different.mdx
+++ b/docs/docs/docs/getting-started/how-is-it-different.mdx
@@ -37,7 +37,7 @@ No need for custom scroll to top handling.
### Sidebar Adaptable
-TabView can turn in to a side bar on tvOS, iPadOS and macOS. This is controlled by `sidebarAdaptable` prop.
+TabView can turn into a sidebar on tvOS, iPadOS, macOS, and on iOS when the screen is large enough. This is controlled by the `sidebarAdaptable` prop. On iOS 27 (built with Xcode 27 or later), the bottom tab bar becomes a sidebar on large screens such as an unfolded iPhone Duo and switches back as the window shrinks.
diff --git a/docs/docs/docs/guides/standalone-usage.mdx b/docs/docs/docs/guides/standalone-usage.mdx
index d9f02c43..53ca4531 100644
--- a/docs/docs/docs/guides/standalone-usage.mdx
+++ b/docs/docs/docs/guides/standalone-usage.mdx
@@ -120,10 +120,14 @@ Whether to show labels in tabs. When `false`, only icons will be displayed.
A tab bar style that adapts to each platform:
- iPadOS: Top tab bar that can adapt into a sidebar
-- iOS: Bottom tab bar
+- iOS: Bottom tab bar. On iOS 27+ (built with Xcode 27 or later), large enough screens such as an unfolded iPhone Duo show a sidebar instead
- macOS/tvOS: Sidebar
- visionOS: Ornament with sidebar for secondary tabs
+Set `sidebarAdaptable` to `true` to opt in. Defaults to `false`.
+
+On iOS 27+, the placement follows the window size: the sidebar appears once the window is at least 920pt wide with a regular vertical size class (for example an unfolded iPhone Duo, or Resize Mode in Xcode's Device Hub), and the bottom tab bar returns when the window shrinks. Regular phones, including landscape, keep the bottom tab bar.
+
#### `disablePageAnimations`
Whether to disable animations between tabs.
diff --git a/docs/docs/docs/guides/usage-with-react-navigation.mdx b/docs/docs/docs/guides/usage-with-react-navigation.mdx
index 78e49d55..8a56abaf 100644
--- a/docs/docs/docs/guides/usage-with-react-navigation.mdx
+++ b/docs/docs/docs/guides/usage-with-react-navigation.mdx
@@ -169,10 +169,14 @@ A tab bar style that adapts to each platform.
Tab views using the sidebar adaptable style have an appearance
- iPadOS displays a top tab bar that can adapt into a sidebar.
-- iOS displays a bottom tab bar.
+- iOS displays a bottom tab bar. On iOS 27+ (built with Xcode 27 or later), large enough screens such as an unfolded iPhone Duo display a sidebar instead.
- macOS and tvOS always show a sidebar.
- visionOS shows an ornament and also shows a sidebar for secondary tabs within a `TabSection`.
+Set `sidebarAdaptable` on `Tab.Navigator` to opt in. Defaults to `false`.
+
+On iOS 27+, the placement follows the window size: the sidebar appears once the window is at least 920pt wide with a regular vertical size class (for example an unfolded iPhone Duo, or Resize Mode in Xcode's Device Hub), and the bottom tab bar returns when the window shrinks. Regular phones, including landscape, keep the bottom tab bar.
+
#### `hapticFeedbackEnabled`
Whether to enable haptic feedback on tab press. Defaults to false.
diff --git a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
index 2f18f210..845e21f1 100644
--- a/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
+++ b/packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
@@ -40,7 +40,7 @@ struct NewTabView: AnyTabView {
Tab(value: tabData.key, role: tabData.role?.convert()) {
RepresentableView(view: child.view)
- .ignoresSafeArea(.container, edges: .all)
+ .modifier(TabContentLayoutModifier(onLayout: onLayout))
.tabAppear(using: context)
.hideTabBar(props.tabBarHidden)
} label: {
@@ -61,13 +61,22 @@ struct NewTabView: AnyTabView {
}
}
.environment(\.layoutDirection, effectiveLayoutDirection)
- .measureView { size in
- onLayout(size)
- }
.modifier(ConditionalBottomAccessoryModifier(props: props))
}
}
+@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
+private struct TabContentLayoutModifier: ViewModifier {
+ @Environment(\.tabBarPlacement) private var tabBarPlacement
+ var onLayout: (CGSize) -> Void
+
+ func body(content: Content) -> some View {
+ content
+ .ignoresSafeArea(.container, edges: tabBarPlacement == .sidebar ? .vertical : .all)
+ .measureView(onLayout: onLayout)
+ }
+}
+
struct ConditionalBottomAccessoryModifier: ViewModifier {
@ObservedObject var props: TabViewProps
diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
index 68053e1f..19db1c48 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift
@@ -426,6 +426,7 @@ extension View {
if enabled {
#if compiler(>=6.0)
self.tabViewStyle(.sidebarAdaptable)
+ .modifier(AdaptiveSidebarPlacementModifier())
#else
self
#endif
@@ -564,3 +565,25 @@ extension View {
}
}
}
+
+private struct AdaptiveSidebarPlacementModifier: ViewModifier {
+ @ViewBuilder
+ func body(content: Content) -> some View {
+ #if os(iOS) && compiler(>=6.4)
+ if #available(iOS 27.0, *) {
+ content
+ .defaultTabBarPlacement(.sidebar)
+ .introspectTabView { controller in
+ guard controller.traitCollection.userInterfaceIdiom == .phone else { return }
+ // Tile the selected scene next to the sidebar so React Native measures the
+ // visible area instead of drawing underneath an overlapping sidebar.
+ controller.sidebar.preferredLayout = .tile
+ }
+ } else {
+ content
+ }
+ #else
+ content
+ #endif
+ }
+}
diff --git a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
index e5663453..cb441767 100644
--- a/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
+++ b/packages/react-native-bottom-tabs/ios/TabViewProvider.swift
@@ -80,6 +80,9 @@ public final class TabInfo: NSObject {
@objc public var sidebarAdaptable: Bool = false {
didSet {
props.sidebarAdaptable = sidebarAdaptable
+ #if os(iOS)
+ setNeedsLayout()
+ #endif
}
}
@@ -208,6 +211,39 @@ public final class TabInfo: NSObject {
override public func layoutSubviews() {
super.layoutSubviews()
setupView()
+ #if os(iOS)
+ updateSidebarSizeClassOverride()
+ #endif
+ }
+#endif
+
+#if os(iOS)
+ /// Minimum width, in points, at which an iPhone window is wide enough for the sidebar.
+ /// UIKit keeps the iPhone sidebar collapsed below roughly 905pt even in a regular width, and
+ /// hides the tab bar at the same time, so stay a little above that. Phones never reach this
+ /// width in portrait, and landscape phones are excluded by their compact vertical size class.
+ private static let sidebarMinimumWidth: CGFloat = 920
+
+ private func updateSidebarSizeClassOverride() {
+ guard #available(iOS 27.0, *), let hostingController else { return }
+
+ let overrides = hostingController.traitOverrides
+ guard sidebarAdaptable, traitCollection.userInterfaceIdiom == .phone else {
+ if overrides.contains(UITraitHorizontalSizeClass.self) {
+ hostingController.traitOverrides.remove(UITraitHorizontalSizeClass.self)
+ }
+ return
+ }
+
+ let fitsSidebar =
+ traitCollection.verticalSizeClass == .regular
+ && bounds.width >= Self.sidebarMinimumWidth
+ let desired: UIUserInterfaceSizeClass = fitsSidebar ? .regular : .compact
+
+ if !overrides.contains(UITraitHorizontalSizeClass.self)
+ || overrides.horizontalSizeClass != desired {
+ hostingController.traitOverrides.horizontalSizeClass = desired
+ }
}
#endif
diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json
index b422488c..9b4c2fb2 100644
--- a/packages/react-native-bottom-tabs/package.json
+++ b/packages/react-native-bottom-tabs/package.json
@@ -45,7 +45,9 @@
},
"jest": {
"preset": "react-native",
- "modulePathIgnorePatterns": ["/lib/"]
+ "modulePathIgnorePatterns": [
+ "/lib/"
+ ]
},
"keywords": [
"react-native",
diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx
index 0acfb842..3b2e292c 100644
--- a/packages/react-native-bottom-tabs/src/TabView.tsx
+++ b/packages/react-native-bottom-tabs/src/TabView.tsx
@@ -50,7 +50,9 @@ interface Props {
* that varies depending on the platform:
* Tab views using the sidebar adaptable style have an appearance
* - iPadOS displays a top tab bar that can adapt into a sidebar.
- * - iOS displays a bottom tab bar.
+ * - iOS displays a bottom tab bar. On iOS 27+ (built with Xcode 27+), large enough
+ * screens such as an unfolded iPhone Duo display a sidebar instead, and the
+ * bottom tab bar returns when the window shrinks.
* - macOS and tvOS always show a sidebar.
* - visionOS shows an ornament and also shows a sidebar for secondary tabs within a `TabSection`.
*/