wwwwwwwwwwwwwwwwwww

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. If you need to access the current route params, you can use the useParams hook.

Root Layout

The root layout on web controls the entire page, from <html> on down. You can optionally return html, body, and head elements in your app/_layout.tsx and they will "just work" - on web, it will be output, and on native it won't so as to avoid rendering errors:

app/_layout.tsx

import { Slot } from 'one'
export default function Layout() {
return (
<html lang="en-US">
<head>
<meta charSet="utf-8" />
</head>
<body>
<Slot />
</body>
</html>
)
}

useServerHeadInsertion

The root _layout.tsx can use a special hook that allows it to insert tags into your <head /> tag after server rendering finishes. This is an edge-case that is mostly only useful for CSS-in-JS libraries. You can use it like so:

import { Slot, useServerHeadInsertion } from 'one'
export default function Layout() {
useServerHeadInsertion(() => {
// this will run after the entire page renders
// return a single React.ReactElement that will be spliced into your <head />
return <style>{renderCSS()}</style>
})
return (
<html>
<Slot />
</html>
)
}

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.

Limitations

As of today layouts don't support loaders. This is on our radar, please chime in if this is helpful for you.

Also note that useParams won't work in layouts, as they are never nested inside a route. Instead, you can use useActiveParams.

Edit this page on GitHub.