Reference version

Expo Router Split View

An Expo Router submodule that provides native split view layout.

iOS
Bundled version:
~55.0.0-beta.0

SplitView is currently in alpha and available in canary releases of expo-router. You need to use a development build since it is unavailable in Expo Go.

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.

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.

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 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

SplitView.Column

iOS

Type: React.Element<SplitViewColumnProps>

SplitViewColumnProps

children

iOS
Optional • Type: ReactNode

SplitView.Inspector

iOS 26+

Type: React.Element<SplitViewColumnProps>