Layouts
A _layout.tsx file wraps every route in its directory and all subdirectories. Layouts are the right place for the HTML shell, navigation, sidebars, headers, footers, and any CSS that should apply globally or section-wide.
Defining a layout
Create _layout.tsx and export a handler via defineLayout. The handler receives children (the rendered page content) and ctx (the full request context):
// src/routes/_layout.tsx
import { defineLayout } from 'davaux'
import type { LayoutProps } from 'davaux'
export default defineLayout(({ children, ctx }: LayoutProps) => {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{ctx.head.title ?? 'My App'}</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>{children}</main>
</body>
</html>
)
})
children is typed as Promise<string> — the inner page HTML, already rendered. The JSX runtime awaits it and injects it without HTML-escaping, so {children} works directly. In template literal layouts (returning a string rather than JSX), use const content = await children explicitly.
Nesting layouts
You can have _layout.tsx files at any directory level. Davaux applies them from innermost (closest to the route file) to outermost (closest to the root), so each layout wraps the output of the one inside it:
src/routes/
├── _layout.tsx ← root layout (HTML shell, global nav)
├── index.page.tsx
└── dashboard/
├── _layout.tsx ← dashboard layout (sidebar)
└── settings.page.tsx
When a request hits /dashboard/settings, Davaux:
- Renders
settings.page.tsx - Wraps it with
dashboard/_layout.tsx - Wraps that with the root
_layout.tsx
Each layout receives the output of the inner layout (or page) as children.
Isolated layouts
By default every _layout.tsx in the ancestor chain applies — a route deep in src/routes/dashboard/ inherits the root layout, the dashboard layout, and any intermediate layouts.
Sometimes you need a completely independent root layout that doesn't inherit from anything above it. Use +layout.tsx for this:
// src/routes/(auth)/+layout.tsx
import { defineLayout } from 'davaux'
export default defineLayout(async ({ children }) => {
const content = await children
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Sign in</title>
</head>
<body class="auth-shell">
<main>{content}</main>
</body>
</html>
)
})
Routes inside (auth)/ receive only this layout. The root _layout.tsx is not applied — the + signals that this layout is the root of its own chain.
+layout.tsx uses the exact same defineLayout API. The only difference is inheritance: _layout inherits from parents, +layout does not.
When to use each
Use _layout.tsx | Use +layout.tsx |
|---|---|
| Layout should inherit the site chrome (nav, footer) | Layout needs its own complete HTML shell |
| Subdirectory variation on a shared theme | Completely different design (auth, dashboard, marketing) |
| Adding a sidebar or breadcrumbs to a section | Minimal, fullscreen, or embed layouts |
Route groups and layouts
Route groups — directories named with parentheses like (auth)/ — let you apply different layouts to routes that share the same URL depth. The folder name is stripped from the URL entirely, so grouping never changes any route's address.
A typical structure with multiple independent layouts:
src/routes/
(app)/
_layout.tsx ← site chrome — nav, footer, shared styles
about.page.tsx → /about
blog/
index.page.tsx → /blog
[slug].page.tsx → /blog/:slug
(home)/
+layout.tsx ← isolated — homepage has its own full-bleed layout
index.page.tsx → /
(auth)/
+layout.tsx ← isolated — minimal centered shell, no nav
login.page.tsx → /login
register.page.tsx → /register
dashboard/
+layout.tsx ← isolated — sidebar layout, no site nav
index.page.tsx → /dashboard
settings.page.tsx → /dashboard/settings
Without route groups, all routes in src/routes/ share a single _layout.tsx. With groups, each concern can have its own layout — or no layout at all — while keeping URLs unchanged.
If you don't need multiple root layouts, skip groups entirely. A single src/routes/_layout.tsx with nested _layout.tsx files in subdirectories is the simplest setup and works well for most apps.
Using ctx.head in a layout
Page handlers set values on ctx.head before the layout runs. Read them in your layout to produce proper <head> tags:
export default defineLayout(({ children, ctx }: LayoutProps) => {
return (
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{ctx.head.title ?? 'My App'}</title>
{ctx.head.description && (
<meta name="description" content={ctx.head.description} />
)}
{Object.entries(ctx.head.meta ?? {}).map(([name, content]) => (
<meta name={name} content={content} />
))}
{ctx.head.stylesheets?.map((href) => (
<link rel="stylesheet" href={href} />
))}
</head>
<body>
{children}
{ctx.head.scripts?.map((src) => (
<script src={src} defer />
))}
</body>
</html>
)
})
CSS imports
Import .css files as side effects anywhere in your routes, layouts, or islands. Davaux collects them all and bundles them into a single /_davaux/styles.css file served automatically — no configuration needed.
// src/routes/about.page.tsx
import './about.css'
import { definePage } from 'davaux'
export default definePage((ctx) => <main>...</main>)
// src/islands/Counter.tsx
import './Counter.css'
import { createSignal } from 'davaux/client'
export default function Counter() { ... }
// src/routes/_layout.tsx
import './layout.css'
import { defineLayout } from 'davaux'
export default defineLayout(({ children, ctx }) => {
return <html>...</html>
})
The <link> tag pointing to the compiled stylesheet is injected into the page automatically, before any island scripts.
Nested layout CSS composes correctly — subdirectory layouts can import section-specific styles without affecting the root layout.
TypeScript
To suppress TypeScript errors on CSS imports, add a src/env.d.ts file:
// src/env.d.ts
declare module '*.css'
Full HTML shell example
A complete root layout suitable for a typical application:
// src/routes/_layout.tsx
import { defineLayout, createLink } from 'davaux'
import type { LayoutProps } from 'davaux'
import './app.css'
export default defineLayout(({ children, ctx }: LayoutProps) => {
const { Link } = createLink(ctx)
const year = new Date().getFullYear()
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<title>{ctx.head.title ? `${ctx.head.title} — My App` : 'My App'}</title>
{ctx.head.description && (
<meta name="description" content={ctx.head.description} />
)}
{ctx.head.stylesheets?.map((href) => (
<link rel="stylesheet" href={href} />
))}
</head>
<body>
<header class="site-header">
<Link href="/" class="logo">My App</Link>
<nav>
<Link href="/docs" activeClass="active">Docs</Link>
<Link href="/blog" activeClass="active">Blog</Link>
<Link href="/login">Sign in</Link>
</nav>
</header>
<div class="page-content">
{children}
</div>
<footer>
<p>© {year} My App. All rights reserved.</p>
</footer>
{ctx.head.scripts?.map((src) => (
<script src={src} defer />
))}
</body>
</html>
)
})