Layouts
Layouts
Layouts wrap pages with persistent, shared UI (headers, sidebars, navigation). Dex supports both application-wide global layouts and page-selected named layouts.
Global Layout
Create web/layouts/global.tsx to wrap every page in your app:
// web/layouts/global.tsx
export default function GlobalLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex flex-col">
<header className="border-b p-4">
<nav className="flex gap-4">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main className="flex-1">{children}</main>
<footer className="border-t p-4 text-center text-sm">© 2026 My Site</footer>
</div>
)
}The global layout is passed to <FileRouter GlobalLayout={GlobalLayout} /> in your app bootstrap (core/bootstrap/web.tsx).
Named Layouts
Create specialized layouts in web/layouts/:
web/layouts/
├─ global.tsx # App-wide shell
├─ blog.tsx # Blog layout
└─ admin.tsx # Admin sidebar layoutExample blog layout in web/layouts/blog.tsx:
// web/layouts/blog.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="border-b pb-4 mb-6">
<h1 className="text-2xl font-bold">Engineering Blog</h1>
</div>
<div>{children}</div>
</div>
)
}Selecting a Layout from a Page
Pages opt into a named layout by exporting layout matching the layout file name:
// web/pages/blog/index.tsx
import { Link } from '@dex/router/client'
export const metadata = { title: "Blog Posts" }
// Resolves web/layouts/blog.tsx
export const layout = 'blog'
export default function BlogIndex() {
return (
<div>
<h2>Latest Articles</h2>
<Link to="/blog/welcome">Read Welcome Post</Link>
</div>
)
}You can also export a function returning the layout name:
export const layout = () => 'blog'Layout Hierarchy
When a page declares a named layout, Dex composes them in order:
- GlobalLayout (outermost container)
- Named Layout (e.g.
BlogLayout) - Page Component (innermost view)
Rendered structure:
<GlobalLayout>
<BlogLayout>
<BlogIndexPage />
</BlogLayout>
</GlobalLayout>Nested Context with <Outlet /> and useOutletContext
Layouts can pass down state and callbacks to nested child pages using <Outlet context={...} />:
// web/layouts/dashboard.tsx
import { Outlet } from '@dex/router/client'
import { useState } from 'react'
export type DashboardContext = {
currentProject: string
setCurrentProject: (name: string) => void
}
export default function DashboardLayout() {
const [currentProject, setCurrentProject] = useState('Concord')
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r p-4">
<h2>Projects</h2>
<p>Active: {currentProject}</p>
</aside>
<main className="flex-1 p-8">
<Outlet<DashboardContext> context={{ currentProject, setCurrentProject }} />
</main>
</div>
)
}Child pages can consume this context via useOutletContext<T>():
// web/pages/dashboard/settings.tsx
import { useOutletContext } from '@dex/router/client'
import type { DashboardContext } from '../../layouts/dashboard'
export const layout = 'dashboard'
export default function DashboardSettings() {
const { currentProject, setCurrentProject } = useOutletContext<DashboardContext>()
return (
<div>
<h3>Settings for {currentProject}</h3>
<button onClick={() => setCurrentProject('Next Project')}>
Switch Project
</button>
</div>
)
}See Also
- Pages and Layouts Guide — Core mental model
- Client Navigation — Navigation and links
- Router Reference — Full API reference