Expo Router Navigation skill
Navigation and routing for Expo Router.
by expo·MIT license·★ 2,657 Stars on the repo·GitHub ↗
npx degit expo/skills/plugins/expo/skills/expo-router#main ~/.claude/skills/expo-routerChecked ·commit main
Files of Expo Router Navigation
Show the full text241 lines
Expo Router Navigation
Navigation and routing for Expo Router apps. For screen styling, colors, controls, media, and visual effects, use the expo-native-ui skill; for motion and gestures, use expo-animation.
References
Consult these resources as needed:
references/
route-structure.md Route conventions, dynamic routes, groups, folder organization
tabs.md NativeTabs, migration from JS tabs, iOS 26 features
toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only)
form-sheet.md Form sheets in expo-router: configuration, footers and background interaction.
search.md Search bar with headers, useSearch hook, filtering patterns
zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+)
Code Style
- Always use kebab-case for file names, e.g.
comment-card.tsx - Always remove old route files when moving or restructuring navigation
- Never use special characters in file names
- Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors.
Routes
See ./references/route-structure.md for detailed route conventions.
- Routes belong in the
appdirectory. - Never co-locate components, types, or utilities in the app directory. This is an anti-pattern.
- Ensure the app always has a route that matches "/", it may be inside a group route.
Library Preferences
Colorfromexpo-routerfor native semantic colors, not rawPlatformColor(type-safe, auto-adapts to light/dark). Seeexpo-native-uifor the full color palette pattern.- In SDK 56+, never import from
@react-navigation/*directly — useexpo-router/react-navigationinstead (covers@react-navigation/native,/core,/elements,/routers)
Behavior
- Prefer
Stack.SearchBarto add a search bar to a screen
Navigation
Link
Use <Link href="/path" /> from 'expo-router' for navigation between routes.
import { Link } from 'expo-router';
// Basic link
<Link href="/path" />
// Wrapping custom components
<Link href="/path" asChild>
<Pressable>...</Pressable>
</Link>
Whenever possible, include a <Link.Preview> to follow iOS conventions. Add context menus and previews frequently to enhance navigation.
Stack
- ALWAYS use
_layout.tsxfiles to define stacks - Use Stack from 'expo-router/stack' for native navigation stacks
Page Title
Set the page title with Stack.Title:
<Stack.Title>Home</Stack.Title>
Context Menus
Add long press context menus to Link components:
import { Link } from "expo-router";
<Link href="/settings" asChild>
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Menu>
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={handleSharePress}
/>
<Link.MenuAction
title="Block"
icon="nosign"
destructive
onPress={handleBlockPress}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} />
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => {}}
/>
</Link.Menu>
</Link.Menu>
</Link>;
Link Previews
Use link previews frequently to enhance navigation:
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
</Link>
Link preview can be used with context menus.
Modal
Present a screen as a modal:
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
Prefer this to building a custom modal component.
Sheet
Present a screen as a dynamic form sheet:
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>
- Using
contentStyle: { backgroundColor: "transparent" }makes the background liquid glass on iOS 26+.
Common route structure
A standard app layout with tabs and stacks inside each tab:
app/
_layout.tsx — <NativeTabs />
(index,search)/
_layout.tsx — <Stack />
index.tsx — Main list
search.tsx — Search view
// app/_layout.tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { ThemeProvider, DarkTheme, DefaultTheme } from "expo-router/react-navigation";
import { useColorScheme } from "react-native";
export default function Layout() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<NativeTabs>
<NativeTabs.Trigger name="(index)">
<NativeTabs.Trigger.Icon sf="list.dash" md="list" />
<NativeTabs.Trigger.Label>Items</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search" />
</NativeTabs>
</ThemeProvider>
);
}
Create a shared group route so both tabs can push common screens:
// app/(index,search)/_layout.tsx
import { Stack } from "expo-router/stack";
import { colors } from "@/theme/colors";
export default function Layout({ segment }) {
const screen = segment.match(/\((.*)\)/)?.[1]!;
const titles: Record<string, string> = { index: "Items", search: "Search" };
return (
<Stack
screenOptions={{
headerTransparent: true,
headerShadowVisible: false,
headerLargeTitleShadowVisible: false,
headerLargeStyle: { backgroundColor: "transparent" },
headerTitleStyle: { color: colors.label },
headerLargeTitleEnabled: true,
headerBlurEffect: "none",
headerBackButtonDisplayMode: "minimal",
}}
>
<Stack.Screen name={screen} options={{ title: titles[screen] }} />
<Stack.Screen name="i/[id]" options={{ headerLargeTitleEnabled: false }} />
</Stack>
);
}
headerLargeTitleEnabled is the SDK 56+ option name; older SDKs use headerLargeTitle, which is deprecated upstream.
Submitting Feedback
If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:
npx --yes submit-expo-feedback@latest --category skills --subject "expo-router" "<actionable feedback>"
Only submit when you have something specific and actionable to report. Include as much relevant context as possible. If an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above.
| 1 | |
| 2 | name expo-router |
| 3 | description Navigation and routing for Expo Router. Covers file-based routes, groups and dynamic routes, folder organization, Link with previews and context menus, native Stack, page titles, modals and form sheets, NativeTabs, headers and toolbars, and header search bars. |
| 4 | version 1.0.1 |
| 5 | license MIT |
| 6 | |
| 7 | |
| 8 | # Expo Router Navigation |
| 9 | |
| 10 | Navigation and routing for Expo Router apps. For screen styling, colors, controls, media, and visual effects, use the `expo-native-ui` skill; for motion and gestures, use `expo-animation`. |
| 11 | |
| 12 | ## References |
| 13 | |
| 14 | Consult these resources as needed: |
| 15 | |
| 16 | |
| 17 | references/ |
| 18 | route-structure.md Route conventions, dynamic routes, groups, folder organization |
| 19 | tabs.md NativeTabs, migration from JS tabs, iOS 26 features |
| 20 | toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only) |
| 21 | form-sheet.md Form sheets in expo-router: configuration, footers and background interaction. |
| 22 | search.md Search bar with headers, useSearch hook, filtering patterns |
| 23 | zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+) |
| 24 | |
| 25 | |
| 26 | ## Code Style |
| 27 | |
| 28 | Always use kebab-case for file names, e.g. `comment-card.tsx` |
| 29 | Always remove old route files when moving or restructuring navigation |
| 30 | Never use special characters in file names |
| 31 | Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors. |
| 32 | |
| 33 | ## Routes |
| 34 | |
| 35 | See `./references/route-structure.md` for detailed route conventions. |
| 36 | |
| 37 | Routes belong in the `app` directory. |
| 38 | Never co-locate components, types, or utilities in the app directory. This is an anti-pattern. |
| 39 | Ensure the app always has a route that matches "/", it may be inside a group route. |
| 40 | |
| 41 | ## Library Preferences |
| 42 | |
| 43 | `Color` from `expo-router` for native semantic colors, not raw `PlatformColor` (type-safe, auto-adapts to light/dark). See `expo-native-ui` for the full color palette pattern. |
| 44 | In SDK 56+, never import from `@react-navigation/*` directly — use `expo-router/react-navigation` instead (covers `@react-navigation/native`, `/core`, `/elements`, `/routers`) |
| 45 | |
| 46 | ## Behavior |
| 47 | |
| 48 | Prefer `Stack.SearchBar` to add a search bar to a screen |
| 49 | |
| 50 | # Navigation |
| 51 | |
| 52 | ## Link |
| 53 | |
| 54 | Use `<Link href="/path" />` from 'expo-router' for navigation between routes. |
| 55 | |
| 56 | |
| 57 | import { Link } from 'expo-router'; |
| 58 | |
| 59 | // Basic link |
| 60 | <Link href="/path" /> |
| 61 | |
| 62 | // Wrapping custom components |
| 63 | <Link href="/path" asChild> |
| 64 | <Pressable>...</Pressable> |
| 65 | </Link> |
| 66 | |
| 67 | |
| 68 | Whenever possible, include a `<Link.Preview>` to follow iOS conventions. Add context menus and previews frequently to enhance navigation. |
| 69 | |
| 70 | ## Stack |
| 71 | |
| 72 | ALWAYS use `_layout.tsx` files to define stacks |
| 73 | Use Stack from 'expo-router/stack' for native navigation stacks |
| 74 | |
| 75 | ### Page Title |
| 76 | |
| 77 | Set the page title with `Stack.Title`: |
| 78 | |
| 79 | |
| 80 | <Stack.Title>Home</Stack.Title> |
| 81 | |
| 82 | |
| 83 | ## Context Menus |
| 84 | |
| 85 | Add long press context menus to Link components: |
| 86 | |
| 87 | |
| 88 | import { Link } from "expo-router"; |
| 89 | |
| 90 | <Link href="/settings" asChild> |
| 91 | <Link.Trigger> |
| 92 | <Pressable> |
| 93 | <Card /> |
| 94 | </Pressable> |
| 95 | </Link.Trigger> |
| 96 | <Link.Menu> |
| 97 | <Link.MenuAction |
| 98 | title="Share" |
| 99 | icon="square.and.arrow.up" |
| 100 | onPress={handleSharePress} |
| 101 | /> |
| 102 | <Link.MenuAction |
| 103 | title="Block" |
| 104 | icon="nosign" |
| 105 | destructive |
| 106 | onPress={handleBlockPress} |
| 107 | /> |
| 108 | <Link.Menu title="More" icon="ellipsis"> |
| 109 | <Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} /> |
| 110 | <Link.MenuAction |
| 111 | title="Delete" |
| 112 | icon="trash" |
| 113 | destructive |
| 114 | onPress={() => {}} |
| 115 | /> |
| 116 | </Link.Menu> |
| 117 | </Link.Menu> |
| 118 | </Link>; |
| 119 | |
| 120 | |
| 121 | ## Link Previews |
| 122 | |
| 123 | Use link previews frequently to enhance navigation: |
| 124 | |
| 125 | |
| 126 | <Link href="/settings"> |
| 127 | <Link.Trigger> |
| 128 | <Pressable> |
| 129 | <Card /> |
| 130 | </Pressable> |
| 131 | </Link.Trigger> |
| 132 | <Link.Preview /> |
| 133 | </Link> |
| 134 | |
| 135 | |
| 136 | Link preview can be used with context menus. |
| 137 | |
| 138 | ## Modal |
| 139 | |
| 140 | Present a screen as a modal: |
| 141 | |
| 142 | |
| 143 | <Stack.Screen name="modal" options={{ presentation: "modal" }} /> |
| 144 | |
| 145 | |
| 146 | Prefer this to building a custom modal component. |
| 147 | |
| 148 | ## Sheet |
| 149 | |
| 150 | Present a screen as a dynamic form sheet: |
| 151 | |
| 152 | |
| 153 | <Stack.Screen |
| 154 | name="sheet" |
| 155 | options={{ |
| 156 | presentation: "formSheet", |
| 157 | sheetGrabberVisible: true, |
| 158 | sheetAllowedDetents: [0.5, 1.0], |
| 159 | contentStyle: { backgroundColor: "transparent" }, |
| 160 | }} |
| 161 | /> |
| 162 | |
| 163 | |
| 164 | Using `contentStyle: { backgroundColor: "transparent" }` makes the background liquid glass on iOS 26+. |
| 165 | |
| 166 | ## Common route structure |
| 167 | |
| 168 | A standard app layout with tabs and stacks inside each tab: |
| 169 | |
| 170 | |
| 171 | app/ |
| 172 | _layout.tsx — <NativeTabs /> |
| 173 | (index,search)/ |
| 174 | _layout.tsx — <Stack /> |
| 175 | index.tsx — Main list |
| 176 | search.tsx — Search view |
| 177 | |
| 178 | |
| 179 | |
| 180 | // app/_layout.tsx |
| 181 | import { NativeTabs } from "expo-router/unstable-native-tabs"; |
| 182 | import { ThemeProvider, DarkTheme, DefaultTheme } from "expo-router/react-navigation"; |
| 183 | import { useColorScheme } from "react-native"; |
| 184 | |
| 185 | export default function Layout() { |
| 186 | const colorScheme = useColorScheme(); |
| 187 | return ( |
| 188 | <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}> |
| 189 | <NativeTabs> |
| 190 | <NativeTabs.Trigger name="(index)"> |
| 191 | <NativeTabs.Trigger.Icon sf="list.dash" md="list" /> |
| 192 | <NativeTabs.Trigger.Label>Items</NativeTabs.Trigger.Label> |
| 193 | </NativeTabs.Trigger> |
| 194 | <NativeTabs.Trigger name="(search)" role="search" /> |
| 195 | </NativeTabs> |
| 196 | </ThemeProvider> |
| 197 | ); |
| 198 | } |
| 199 | |
| 200 | |
| 201 | Create a shared group route so both tabs can push common screens: |
| 202 | |
| 203 | |
| 204 | // app/(index,search)/_layout.tsx |
| 205 | import { Stack } from "expo-router/stack"; |
| 206 | import { colors } from "@/theme/colors"; |
| 207 | |
| 208 | export default function Layout({ segment }) { |
| 209 | const screen = segment.match(/\((.*)\)/)?.[1]!; |
| 210 | const titles: Record<string, string> = { index: "Items", search: "Search" }; |
| 211 | |
| 212 | return ( |
| 213 | <Stack |
| 214 | screenOptions={{ |
| 215 | headerTransparent: true, |
| 216 | headerShadowVisible: false, |
| 217 | headerLargeTitleShadowVisible: false, |
| 218 | headerLargeStyle: { backgroundColor: "transparent" }, |
| 219 | headerTitleStyle: { color: colors.label }, |
| 220 | headerLargeTitleEnabled: true, |
| 221 | headerBlurEffect: "none", |
| 222 | headerBackButtonDisplayMode: "minimal", |
| 223 | }} |
| 224 | > |
| 225 | <Stack.Screen name={screen} options={{ title: titles[screen] }} /> |
| 226 | <Stack.Screen name="i/[id]" options={{ headerLargeTitleEnabled: false }} /> |
| 227 | </Stack> |
| 228 | ); |
| 229 | } |
| 230 | |
| 231 | |
| 232 | `headerLargeTitleEnabled` is the SDK 56+ option name; older SDKs use `headerLargeTitle`, which is deprecated upstream. |
| 233 | |
| 234 | ## Submitting Feedback |
| 235 | If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve: |
| 236 | |
| 237 | npx --yes submit-expo-feedback@latest --category skills --subject "expo-router" "<actionable feedback>" |
| 238 | |
| 239 | Only submit when you have something specific and actionable to report. Include as much relevant context as possible. |
| 240 | If an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above. |
| 241 |
Discussion
Alternatives
Browse more free Claude skills or everything in Development.