Expo Router v55: more native navigation, more powerful web

ProductReact NativeDevelopment9 minutes read

Jakub Tkacz

Jakub Tkacz

Engineering

Hassan Khan

Hassan Khan

Engineering

Expo Router v55 upgrades navigation with a new Stack API, native tabs, dynamic colors, and toolbars plus experimental SSR and data loaders.

Expo Router v55: more native navigation, more powerful web

Expo Router v55 is here (yes, we traveled 49 versions into the future) and it is a meaningful step toward the same goal we have been chasing since Expo Router launched: delivering an even more native feel to apps while supporting more complex web scenarios so you can truly write your code only once.

This release does two big things at once:

It makes native UI a first-class part of routing. The new Stack API, toolbars, native tabs, dynamic platform colors, zoom transitions, and split view support let you build navigation UI the way you build the rest of your app: with React components, composed right where the screen lives.

It expands what “write it once” can mean on web. Expo Router now has a clearer runtime story with expo-server, configurable server headers, and experimental server rendering plus data loaders, so you can ship faster pages, better SEO, and more dynamic content without building a separate web stack.

Note that some of these APIs are still experimental and may be adjusted for best experience. As of 2026/02/xx the APIs shown in this post are up-to-date, but you can always find the more recent examples in Expo Router's documentation.

Below is a tour of what’s new, why it matters, and where to start trying it today.

New Stack API

Header buttons

The new Stack API gives you declarative, component-level control over the native header. We replaced the options API with composable React components you can use directly in your screen. Style the header, set the title, add a search bar - all inside your component tree.

Code
import { Stack } from "expo-router"
export default function IndexPage() {
...
return (
<>
{/* Screen content */}
<Stack.Header style={{ backgroundColor: 'transparent' }} />
<Stack.Screen.Title>Home</Stack.Screen.Title>
<Stack.SearchBar onChangeText={handleSearchChange} />
</>
);
}

Native Tabs

We’ve added support for the Native Tabs component. We’re actively working toward full support across all platforms and aim to mark the API as stable in the next release.

On Android, Native Tabs use Material Design 3 dynamic colors by default, automatically adapting to the user's preferred colors. You can use Material Symbols - similar to SF Symbols on iOS - by specifying the md prop on NativeTabs.Trigger. SafeAreaView is now integrated by default, so content is never hidden behind the tab bar.

Code
<NativeTabs>
<NativeTabs.Trigger name="(library)">
<NativeTabs.Trigger.Icon
sf="photo.fill.on.rectangle.fill"
md="photo_library"
/>
<NativeTabs.Trigger.Label>Library</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
...
</NativeTabs>
Bottom accessory

On iOS, we’ve added support for a bottom accessory - a floating view that appears above the tab bar - available starting in iOS 26. You can use it in Expo Router by adding the NativeTabs.BottomAccessory component inside your NativeTabs layout.

Code
import { NativeTabs } from 'expo-router/unstable-native-tabs';
...
export default function Layout() {
...
return (
<NativeTabs>
...
<NativeTabs.BottomAccessory>
<MiniPlayer isPlaying={isPlaying} setIsPlaying={setIsPlaying} />
</NativeTabs.BottomAccessory>
</NativeTabs>
);
}
While working on this release, we introduced some breaking API changes. You can find a short migration guide here.

Check all the native tabs features in the documentation.

Dynamic Colors

The new Color API gives you direct access to platform colors: Color.ios.* for iOS system colors, Color.android.* for Android system colors, and Color.android.dynamic.* for Material Dynamic colors that adapt to the user's wallpaper.

Code
import { Color } from "expo-router"
export default function Page() {
// This needs to be declared within React Component
const backgroundColor = Platform.select({
android: Color.android.dynamic.surface,
ios: Color.ios.systemBackground,
default: '#fff'
});
...
return (
<ScrollView style={{ backgroundColor }}>
...
</ScrollView>
);
}

Read more about the Color API in the documentation.

Toolbars

Toolbar demo

The new Toolbar API brings native iOS toolbars to Expo Router, with built-in animations and full liquid glass effect included. You can choose from three placements: left or right in the header, or anchored at the bottom of the screen.

Code
import { Stack } from "expo-router";
export default function FoldersScreen() {
...
return (
<>
{/* Screen content */}
<Stack.Header style={{ backgroundColor: 'transparent' }} />
<Stack.Screen.Title large>Folders</Stack.Screen.Title>
<Stack.SearchBar onChangeText={handleSearchChange} />
<Stack.Toolbar placement="right">
<Stack.Toolbar.Button icon="folder.badge.plus" onPress={addFolder} />
<Stack.Toolbar.Button onPress={activateEditMode}>Edit</Stack.Toolbar.Button>
</Stack.Toolbar>
<Stack.Toolbar placement="bottom">
<Stack.Toolbar.SearchBarSlot />
<Stack.Toolbar.Spacer />
<Stack.Toolbar.Button icon="square.and.pencil" onPress={addNew} />
</Stack.Toolbar>
</>
);
}

Read more about toolbars in the documentation.

Zoom transition

Zoom transitions

The new Apple zoom API brings gesture-driven, interruptible, native shared element transitions to any <Link /> in your app, on Apple platforms, with default behavior on other platforms. We deconstructed the underlying UIKit API and reimagined it as a React-first, layered effect.

  1. Full zoom from Link to route.
  2. Zoom an individual view inside a Link to the route.
  3. Zoom an individual view to a shared view on the next route.

This gives you full control with graceful fallbacks that just work™. No new dependencies, no new imports, just the same powerful Link you’re already using.

To get started, set withAppleZoom on Link.Trigger

Code
<Link href="..." asChild>
<Link.Trigger withAppleZoom>
<Pressable style={{ width: '50%', aspectRatio: 1 }}>
<Image source={...} />
</Pressable>
</Link.Trigger>
</Link>

You can read more about zoom transitions in the documentation.

expo-server

The @expo/server package has been renamed to expo-server and now ships as part of the Expo SDK. It provides runtime server APIs for Expo Router projects, along with built-in deployment adapters for EAS Hosting as well as other platforms.

Code
// app/api/user+api.ts
import { deferTask, setResponseHeaders } from 'expo-server';
import telemetry from '@/telemetry';
export function GET() {
deferTask(async () => {
telemetry.track();
});
setResponseHeaders({ 'Cache-Control': 'max-age=60' });
return Response.json({ user: 'anonymous' });
}

The runtime API is supported across all places where you’d write server-side code in Expo Router, including API routes (as shown above), middleware, data loaders, and React Server Components or functions. In production, expo-server not only works in EAS Hosting but also in Express, Vercel, Netlify, Cloudflare Workers, and Bun.

Learn more about expo-server in the documentation.

Server headers

You can now define custom HTTP headers for your routes directly in your app config. This is useful for things like security headers, caching policies, and cookies.

Code
{
"expo": {
"plugins": [
["expo-router", {
"headers": {
"X-Frame-Options": "DENY",
"Cache-Control": "public, max-age=3600"
}
}]
]
}
}

Headers apply to HTML and API route responses (but not static assets!). This is great for when you need cross-origin isolation for features like expo-sqlite on web.

Learn more about server headers in the documentation.

Experimental server rendering

We've added experimental support for server-side rendering your Expo app. Previously, Expo Router only supported static-site generation (SSG), which pre-generates HTML at build time. With server-side rendering (SSR), HTML is generated dynamically on each request, perfect for personalized content, real-time data, and improved SEO..

Code
{
"expo": {
"web": {
"output": "server"
},
"plugins": [
["expo-router", {
"unstable_useServerRendering": true
}]
]
}
}

Learn more about experimental server rendering in the documentation.

Experimental data loaders

We’ve also added experimental support for data loaders. Data loaders let you fetch data on the server and access it in your component via a new useLoaderData hook that works for both static and server rendering.

This lets you co-locate your data fetching with your route component, but with the safety of keeping sensitive data server-side (such as environment variables) and without needing to manage separate API routes. Your client code only ever sees the result of a loader function:

Code
import { useLoaderData } from 'expo-router';
import { setResponseHeaders } from 'expo-server';
// Runs on the server, and has access to `process.env`
export async function loader() {
// You can also use the runtime API from `expo-server`
setResponseHeaders({ 'Cache-Control': 'max-age=60' });
return fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${process.env.API_KEY}` }
}).then(r => r.json());
}
export default function Home() {
const data = useLoaderData<typeof loader>();
return <Text>{data.title}</Text>;
}

Learn more in the documentation.

Static rendering

In static rendering mode (when web.output is set to static in your app config), loaders are executed at build-time, allowing you to prefetch any required data once before deployment.

Server rendering

In server rendering (when web.output is set to server in your app config, and experimental server rendering is enabled), loaders are executed at request time, and eliminate the need for separate API routes when you just need to fetch and display data:

Experimental split view

Experimental split view

In collaboration with the React Native Screens team, we added experimental support for the native Apple UISplitViewController component. You can try it by running your app on an iPad.

Code
import { SplitView } from 'expo-router/unstable-split-view';
export default function RootLayout()
return (
<SplitView>
<SplitView.Column>
{/* Column content */}
</SplitView.Column>
<SplitView.Column>
{/* Column content */}
</SplitView.Column>
</SplitView>
);
}

Read more about split view in the documentation.

Where to start with Expo Router v55

Expo Router v55 is ultimately about one promise: your app should feel native by default, even when your codebase is shared. With SDK 55, that promise gets a lot more concrete.

  • Native navigation UI, built like React. Declarative headers, search, toolbars, tabs, platform colors, and gesture-driven transitions let you build polished navigation without bolting on custom native code.
  • A stronger web foundation. expo-server, server headers, and experimental SSR plus data loaders unlock better performance, better SEO, and more dynamic experiences while keeping your app architecture consistent.
  • Better experiences on more form factors. Features like split view push Router further into “real app” territory on iPad and larger screens.

If you are upgrading, start with the native tabs migration notes, then pick one new API to adopt (Stack Header or Native Tabs are great first wins). And if you try the experimental web features, treat them like you would any new platform capability: kick the tires, ship behind a flag if you need to, and tell us what breaks so we can make them boring and stable.

And show us what you’re building! Tag us on social, send us a DM, or share it in Discord. We want to see what you build!

Expo Router

Create amazing apps, in record time with EAS

Learn more