Layouts

Layout files can be placed in any folder inside the ./app directory. They serve as the frame for the routes contained within that folder, and they can nest inside of each other if you place them in sub-directories.

_layout.tsx

Wraps all files in this directory and below

index.tsx

Matches "/"

blog

_layout.tsx

A custom sub-layout for all /blog pages

index.tsx

Matches "/blog"

[slug].tsx

Matches a single sub-path of "/blog", like "/blog/hello"

Layouts must render one of the following to show the matched pages that exist in their directory:

This looks something like this at the simplest:

app/_layout.tsx

import { Slot } from 'one'
export default function Layout() {
return (
<Slot />
)
}

As of now, layouts don't support loaders, but we are working to land that shortly, which would enable multiple loaders per-route.

If you need to access the current route params, you can use the useParams hook.

Groups and layouts

If you'd like to logically group together some pages without creating a sub-route, you can use a group folder by naming it like so:

_layout.tsx

Wraps all files in this directory and below

index.tsx

Matches "/"

(blog)

_layout.tsx

A custom sub-layout for all /blog pages

blog.tsx

Matches "/blog"

[slug].tsx

Matches a single sub-path of "/blog", like "/blog/hello"

Groups let you add extra layouts. If done right, this gives you a lot of control over how your app feels.

Nested layouts example

A common pattern that apps have is something like Twitter/X, where you have bottom tabs for your "top level" views, but then on some of the bottom tab sections, you want to have a Stack that remembers its state inside just that tab.

This pattern can be incredibly verbose to link together with React Navigation, and takes a bit of tinkering to figure out with One. Since it is common and a useful example, lets walk through how you'd build on using One's file system routing.

Here's our file structure:

_layout.tsx

Where our Tabs layout is defined

notifications.tsx

Matches "/notifications"

profile.tsx

Matches "/profile"

(feed)

_layout.tsx

Where our Stack layout is defined

index.tsx

Matches "/"

post-[id].tsx

Matches routes like "/post-123"

The top level Layout will define our tabs:

app/_layout.tsx

import { Bell, Home, User } from '@tamagui/lucide-icons'
import { Home } from '~/features/icons'
export function RootLayout() {
return (
<Tabs screenOptions={{ headerShown: false, }} >
<Tabs.Screen name="(feed)" options={{ title: 'Feed', tabBarIcon: ({ color }) => <Home size={20} color={color} />, }} />
<Tabs.Screen name="notifications" options={{ title: 'Notifications', tabBarIcon: ({ color }) => <Bell size={20} color={color} />, }} />
<Tabs.Screen name="profile" options={{ title: 'Profile', tabBarIcon: ({ color }) => <User size={20} color={color} />, }} />
</Tabs>
)
}

This will set us up with three bottom tabs: Feed, Notifications, and Profile. The Notifications and Profile tabs for now will just show their content directly, but inside of the Feed tab, we want to show a stack.

We set up the stack in (feed)/_layout.tsx:

(feed)/_layout.tsx

import { Slot, Stack } from 'one'
export default function FeedLayout() {
return (
<>
{typeof window !== 'undefined' ? (
<Slot />
) : (
<Stack>
<Stack.Screen name="index" options={{ title: 'Feed' }} />
<Stack.Screen name="[id]" options={{ title: 'Post' }} />
</Stack>
)}
</>
)
}

One thing we're showing here is that the layout is diverging between web and native. On web, we are showing a Slot, while on Native we show a Stack. This is because browsers feel better without stacks - the native back/forward button serves as our stack controller.

On Native we are defining the configuration for each sub-screen with the Stack.Screen component. The Stack component is a React Navigation native stack navigator and nothing more, it accepts all the props you'd expect.

You'll notice we are matching the name to the file names names of the sub-routes, without the .tsx extension.

This will get you a nice Stack-inside-Tabs pattern that is common on native apps, all with just two layouts and a few routes.

Edit this page on GitHub.