Layouts

Layouts in Catmint wrap pages and persist across sibling navigation. They are defined by layout.tsx files inside the app/ directory. This guide covers the layout convention, nesting behavior, inheritance boundaries, persistence, and route group interaction.

The layout.tsx Convention

Any directory inside app/ can contain a layout.tsx file. The file must export a default React component that accepts a children prop:

// app/layout.tsx
import React from 'react'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My App</title>
      </head>
      <body>
        <header>
          <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>Built with Catmint</footer>
      </body>
    </html>
  )
}

The root app/layout.tsx is special: it wraps every page in your application and is typically where you render the <html>, <head>, and <body> tags.

The children Prop

The children prop contains the rendered output of the matched page or the next nested layout in the chain. You must render it somewhere in your layout component -- without it, the page content will not appear:

// app/dashboard/layout.tsx
import React from 'react'

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <aside>
        <h2>Dashboard</h2>
        <ul>
          <li><a href="/dashboard">Overview</a></li>
          <li><a href="/dashboard/analytics">Analytics</a></li>
          <li><a href="/dashboard/settings">Settings</a></li>
        </ul>
      </aside>
      <section className="flex-1">
        {children}
      </section>
    </div>
  )
}

Nested Layout Chains

Layouts are resolved from the root to the tip of the directory tree. When a request matches a page, Catmint collects all layout.tsx files from the root down to the page's directory and nests them:

app/
  layout.tsx                # Root layout (1st)
  dashboard/
    layout.tsx              # Dashboard layout (2nd)
    analytics/
      layout.tsx            # Analytics layout (3rd)
      page.tsx              # Page content (innermost)

For a request to /dashboard/analytics, the rendering order is:

RootLayout
  -> DashboardLayout
    -> AnalyticsLayout
      -> AnalyticsPage

Each layout wraps the next one in the chain. The innermost layout wraps the page itself. Not every directory needs a layout -- if a directory does not contain a layout.tsx, it is simply skipped in the chain.

Stopping Inheritance with inherit: false

By default, a layout inherits all ancestor layouts above it. To stop the ancestor chain from being collected, pass { inherit: false } as the second argument to the layout() wrapper:

// app/auth/layout.tsx
import React from 'react'
import { layout } from 'catmint/layout'

export default layout(function AuthLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>Sign In</title>
      </head>
      <body>
        <div className="auth-container">
          {children}
        </div>
      </body>
    </html>
  )
}, { inherit: false })

With inherit: false, pages inside app/auth/ will only be wrapped by the AuthLayout. The root layout and any other ancestor layouts are not applied. This is useful for pages like login or onboarding that need a completely different shell.

When using inherit: false, your layout becomes the new root. It must render the <html> and <body> tags itself since the root layout is no longer in the chain.

Layout Persistence

Layouts persist across navigation between sibling routes. When a user navigates from /dashboard/analytics to /dashboard/settings, the DashboardLayout is not re-mounted. Only the page content inside it changes.

This means that state held in a layout component (such as sidebar scroll position, form inputs, or local React state) is preserved across sibling navigation. The layout is only unmounted when the user navigates to a route outside its scope.

Navigation: /dashboard/analytics -> /dashboard/settings

  RootLayout           (persisted)
    DashboardLayout    (persisted - state preserved)
      SettingsPage     (mounted, replaces AnalyticsPage)

Navigation: /dashboard/settings -> /about

  RootLayout           (persisted)
    AboutPage          (mounted, DashboardLayout unmounted)

Layouts and Route Groups

Route groups can contain their own layout.tsx files. This is the primary use case for route groups -- applying different layouts to different sections of your site without affecting URLs:

app/
  layout.tsx                    # Root layout (shared)
  (marketing)/
    layout.tsx                  # Marketing layout
    page.tsx                    # / (marketing shell)
    pricing/
      page.tsx                  # /pricing (marketing shell)
  (app)/
    layout.tsx                  # App layout (authenticated shell)
    dashboard/
      page.tsx                  # /dashboard (app shell)
    settings/
      page.tsx                  # /settings (app shell)

The marketing pages get a public-facing layout with a landing page header, while the app pages get an authenticated layout with a sidebar. Both groups still inherit the root layout unless inherit: false is set.

Example: Marketing vs. App Layouts

// app/(marketing)/layout.tsx
import React from 'react'

export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <header>
        <a href="/">Brand</a>
        <a href="/pricing">Pricing</a>
        <a href="/dashboard">Sign In</a>
      </header>
      {children}
    </div>
  )
}

// app/(app)/layout.tsx
import React from 'react'

export default function AppLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <nav>
        <a href="/dashboard">Dashboard</a>
        <a href="/settings">Settings</a>
      </nav>
      <main>{children}</main>
    </div>
  )
}

Layout Best Practices

  • Keep layouts focused. A layout should handle shared UI concerns (navigation, sidebars, footers) for its section. Avoid putting page-specific logic in layouts.
  • Use route groups for divergent shells. If two sections of your app need entirely different navigation structures, use route groups with separate layouts rather than conditional rendering in a single layout.
  • Reserve inherit: false for full resets. Only break the layout chain when you genuinely need a completely different document shell (e.g., auth pages, embedded widgets, or print views).
  • Leverage persistence for state. Because layouts persist across sibling navigation, they are a natural place for shared client-side state such as search filters or sidebar collapse toggles.

Next Steps

  • Routing -- file-based routing, dynamic segments, and route priority
  • Middleware -- intercept requests before they reach layouts and pages
  • React Server Components -- server and client component boundaries