Reference version

Expo Router Split View

An Expo Router submodule that provides native split view layout.

iOS
Bundled version:
~55.0.3

SplitView is an alpha API available on iOS only in Expo SDK 55 and later. The API is subject to breaking changes and is not ready for production usage yet.

expo-router/unstable-split-view is a submodule of expo-router and exports components to build split view layouts using platform-native system split views.

See the Expo Router reference for more information about the file-based routing library for native and web app.

Platform support

Split View is only available on iOS. On other platforms, the SplitView component automatically falls back to rendering as a standard Slot navigator, ensuring your app works across all platforms without conditional code.

iPhone support

On iPhone, SplitView automatically collapses all columns into a single view. Only one column is visible at a time.

Choosing the initial column

Use the topColumnForCollapsing prop to control which column is displayed when the split view is collapsed:

<SplitView topColumnForCollapsing="primary">{/* ... */}</SplitView>

Accepted values are primary, supplementary, and secondary. When not set, the system uses its default behavior.

Use a ref to programmatically show a specific column with the show method:

import { useRef } from 'react'; import { Pressable, Text } from 'react-native'; import { SplitView } from 'expo-router/unstable-split-view'; import type { SplitHostCommands } from 'react-native-screens/experimental'; export default function Layout() { const ref = useRef<SplitHostCommands>(null); return ( <SplitView ref={ref} topColumnForCollapsing="primary"> <SplitView.Column> <Pressable onPress={() => ref.current?.show('secondary')}> <Text>Show main content</Text> </Pressable> </SplitView.Column> </SplitView> ); }

Known limitations

Cannot be nested

There can only be one SplitView in the navigation hierarchy. Attempting to nest split views will result in an error.

Cannot be used inside other navigators

SplitView cannot be used inside another navigator (except for Slot). It must be used at the root layout level.

Only specific children allowed

SplitView only accepts SplitView.Column and SplitView.Inspector as direct children. Other components will be ignored with a warning.

Header cannot be customized yet

The header (navigation bar) within split view columns cannot be customized. Custom header configurations are not yet supported.

Limited API

The current API surface is minimal and may not cover all use cases. Additional props and configuration options will be added in future releases.

Going back to previous column on iPhone

To go back to a previous column, tap the system back button in the navigation bar. A future release will add more granular programmatic control over back navigation.

Note: We are actively developing SplitView and looking for feedback. You can share your thoughts on Discord, open an issue on GitHub, or use the Feedback button at the bottom of this page.

Installation

To use expo-router/unstable-split-view in your project, you need to install expo-router in your project. Follow the instructions from Expo Router's installation guide:

Install Expo Router

Learn how to install Expo Router in your project.

Using SplitView.Column

SplitView.Column defines additional columns in your split view layout. You can add up to two columns before the main content area.

Two-column layout

A simple sidebar with main content:

app/_layout.tsx
import { Link } from 'expo-router'; import { SplitView } from 'expo-router/unstable-split-view'; import { Text, Pressable } from 'react-native'; import { SafeAreaView } from 'react-native-screens/experimental'; export default function Layout() { return ( <SplitView> <SplitView.Column> <SafeAreaView edges={{ left: true, top: true }} style={{ flex: 1 }}> <Link href="/inbox"> <Pressable style={{ padding: 16 }}> <Text>Inbox</Text> </Pressable> </Link> <Link href="/sent"> <Pressable style={{ padding: 16 }}> <Text>Sent</Text> </Pressable> </Link> </SafeAreaView> </SplitView.Column> </SplitView> ); }

Three-column layout

A sidebar with supporting column and main content:

app/_layout.tsx
import { Link, useGlobalSearchParams } from 'expo-router'; import { SplitView } from 'expo-router/unstable-split-view'; import { SafeAreaView } from 'react-native-screens/experimental'; export default function Layout() { const params = useGlobalSearchParams(); return ( <SplitView> <SplitView.Column> <SafeAreaView edges={{ left: true, top: true }} style={{ flex: 1, gap: 16, padding: 16, }}> <Link href="/?col1=1" style={{ fontWeight: params.col1 === '1' ? 'bold' : 'normal' }}> Option 1 </Link> <Link href="/?col1=2" style={{ fontWeight: params.col1 === '2' ? 'bold' : 'normal' }}> Option 2 </Link> <Link href="/?col1=3" style={{ fontWeight: params.col1 === '3' ? 'bold' : 'normal' }}> Option 3 </Link> </SafeAreaView> </SplitView.Column> <SplitView.Column> <SafeAreaView edges={{ left: true, top: true }} style={{ flex: 1, gap: 16, padding: 16, }}> <Link href={`/?col1=${params.col1}&col2=1`}>Sub-Option 1</Link> <Link href={`/?col1=${params.col1}&col2=2`}>Sub-Option 2</Link> <Link href={`/?col1=${params.col1}&col2=3`}>Sub-Option 3</Link> </SafeAreaView> </SplitView.Column> </SplitView> ); }

Using SplitView.Inspector

SplitView.Inspector adds a supplementary column that slides in from the trailing edge, useful for showing additional details or metadata:

<SplitView> <SplitView.Column>{/* Sidebar */}</SplitView.Column> <SplitView.Inspector> <View style={{ flex: 1, padding: 16 }}> <Text>Inspector Panel</Text> </View> </SplitView.Inspector> </SplitView>

Complete example

Here's a password manager-style app with three columns:

app
_layout.tsx
index.tsx
[type]
  [id].tsx
  index.tsx
app/_layout.tsx
import { Link, Color, useGlobalSearchParams } from 'expo-router'; import { SplitView } from 'expo-router/unstable-split-view'; import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-screens/experimental'; export default function Layout() { return ( <SplitView showInspector> <SplitView.Column> <PasscodeList /> </SplitView.Column> <SplitView.Column> <PasswordElementList /> </SplitView.Column> <SplitView.Inspector> <InspectorContent /> </SplitView.Inspector> </SplitView> ); } function PasscodeList() { return ( <SafeAreaView edges={{ top: true, left: true }} style={style.passcodeList}> <PasscodeCard title="All" param="all" /> <PasscodeCard title="Passkeys" param="passkeys" /> <PasscodeCard title="Codes" param="codes" /> <PasscodeCard title="Security" param="security" /> <PasscodeCard title="Deleted" param="deleted" /> </SafeAreaView> ); } const passkeys = ['Github', 'Google', 'Facebook', 'Twitter', 'Apple', 'Microsoft', 'Amazon']; const security = ['Admin1234', 'Root']; const all = [...passkeys, ...security]; function PasswordElementList() { const params = useGlobalSearchParams(); const data = (() => { switch (params.type) { case 'all': case undefined: return all; case 'passkeys': return passkeys; case 'security': return security; default: return []; } })(); return ( <ScrollView contentInsetAdjustmentBehavior="automatic" style={{ backgroundColor: undefined }}> {data.map(item => ( <PasswordElement key={item} title={item} /> ))} </ScrollView> ); } function PasscodeCard({ param, title }: { param: string; title: string }) { const params = useGlobalSearchParams(); const isActive = params.type === param; return ( <Link href={`/${param}/`} disabled={isActive} style={[ style.passcodeCard, { backgroundColor: isActive ? Color.ios.systemBlue : Color.ios.systemGray6, }, ]} asChild> <Pressable> <Text style={{ color: isActive ? 'white' : 'black', fontSize: 16 }}>{title}</Text> </Pressable> </Link> ); } function PasswordElement({ title }: { title: string }) { const params = useGlobalSearchParams(); const isActive = params.id === title; return ( <Link href={`/${params.type}/${title}/`} asChild> <Pressable style={{ backgroundColor: isActive ? Color.ios.systemBlue : undefined, padding: 12, }}> <SafeAreaView edges={{ left: true }}> <Text style={{ color: isActive ? 'white' : 'black', fontSize: 16 }}>{title}</Text> </SafeAreaView> </Pressable> </Link> ); } function InspectorContent() { return ( <View style={style.inspectorContent}> <Text>Inspector</Text> </View> ); } const style = StyleSheet.create({ passcodeList: { flex: 1, flexWrap: 'wrap', gap: 8, flexDirection: 'row', padding: 8, }, passcodeCard: { width: '48%', padding: 12, borderRadius: 12, justifyContent: 'center', alignItems: 'center', height: 50, }, inspectorContent: { flex: 1, justifyContent: 'center', alignItems: 'center', }, });
app/index.tsx
import { Redirect } from 'expo-router'; export default function Index() { return <Redirect href="/all/" />; }
app/[type]/index.tsx
import { Color } from 'expo-router'; import { Text, View } from 'react-native'; export default function Index() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text style={{ color: Color.ios.label, fontSize: 24, fontWeight: 'bold' }}> Nothing is selected </Text> </View> ); }
app/[type]/[id].tsx
import { useLocalSearchParams } from 'expo-router'; import { Text, View } from 'react-native'; export default function Id() { const { id } = useLocalSearchParams(); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>ID: {id}</Text> </View> ); }

API

import { SplitView } from 'expo-router/unstable-split-view';

Components

SplitView

iOS

Type: React.Element<SplitViewProps>

For full list of supported props, see SplitHostProps

SplitViewProps

children

iOS
Optional • Type: ReactNode

Inherited Props

  • Omit<SplitHostProps, 'children'>

SplitView.Column

iOS

Type: React.Element<SplitViewColumnProps>

SplitViewColumnProps

children

iOS
Optional • Type: ReactNode

SplitView.Inspector

iOS 26+

Type: React.Element<SplitViewColumnProps>