Skip to content

Layout files wrap groups of routes with shared UI - without duplicating components across every page.

Define a Layout

Create a layout.tsx (or .vue / .svelte / .mdx) in any folder under pages/, and it automatically wraps every route in that folder and its subfolders. Nest layouts by nesting folders.

pages/
  dashboard/
    layout.tsx         ← wraps all /dashboard/* pages
    settings/
      layout.tsx       ← wraps all /dashboard/settings/* pages
      profile/
        index.tsx      ← wrapped by both layouts
      index.tsx
    index.tsx

For /dashboard/settings/profile, the render order is:

app.tsx (global wrapper)
└── dashboard/layout.tsx
    └── dashboard/settings/layout.tsx
        └── dashboard/settings/profile/index.tsx

No configuration, no imports - the file system defines the hierarchy.

Child routes cannot escape parent layouts. Once a layout is established at a folder level, all routes beneath it inherit it - keeping the UI hierarchy predictable.

Looking for a layout that wraps every page?

That role belongs to the app file, not to a layout at the root of pages/ - a root-level pages/layout.* is not picked up. Layout files scope shared UI to a folder subtree; the app file wraps the whole app.

Layout File Naming

Only the lowercase form is recognized as a special file. Layout.tsx, LAYOUT.vue, and other variations are treated as regular components.

FrameworkRecognized name
React / SolidJSlayout.tsx
Vuelayout.vue
Sveltelayout.svelte
MDXlayout.mdx

Each source folder runs a single framework and ignores files belonging to others: React/SolidJS folders ignore .vue/.svelte files, Vue folders ignore .tsx.

When you create a new layout file, KosmoJS generates framework-appropriate boilerplate immediately. Some editors may require a brief unfocus/refocus to load the generated content.

Layout Implementation

Each framework renders child routes differently:

tsx
// layout.tsx
import { Outlet } from "react-router";

export default function Layout() {
  return (
    <div className="dashboard">
      <nav>...</nav>
      <main>
        <Outlet />
      </main>
      <footer>...</footer>
    </div>
  );
}

React renders child routes via <Outlet />. SolidJS and MDX use props.children, Svelte renders {@render children()}, and Vue uses <RouterView />.

Data Loading in Layouts

Layout data loading follows the same per-framework patterns as page components, but how a layout's data stays distinct from its child page's differs:

  • React scopes structurally - each route (layouts included) owns its loader, and useLoaderData() returns the calling route's data. No key; the route tree carries the identity.
  • SolidJS keys by the query() cache string you supply ("dashboard/data" here), so the key lives in the query() wrapper, not the hook read.
  • Vue, Svelte, and MDX share one per-route store keyed by route name, so the layout passes its path-qualified name to useLoaderData (a page passes nothing) - the hook can't tell which layout it runs in.
tsx
// layout.tsx
import { Outlet, useLoaderData } from "react-router";
import fetchClients from "_/fetch";

const { GET } = fetchClients["dashboard/data"];

export const loader = () => GET();

export default function Layout() {
  const data = useLoaderData();
  // ...
  return <Outlet />;
}

Loader/preload runs before the layout renders, so its data is available immediately and shared across every child route without a duplicate fetch.

The read is a hook (useLoaderData / createAsync) rather than a prop - props carries only children/<Outlet />.

Keeping a layout's data distinct from its page's is automatic in React (per-route) and Solid (via the query() key); in Vue, Svelte, and MDX you pass the layout's path-qualified name (e.g. "dashboard/layout" for pages/dashboard/layout.*) to the hook.

Global Layout via app File

The app.{tsx,vue,svelte,mdx} at the source folder root wraps every route - the right place for truly global concerns like authentication checks, analytics tracking or error boundaries.

txt
front/
├── app.tsx              ← wraps everything
└── pages/
    ├── dashboard/
    │   └── layout.tsx
    └── index/
        └── index.tsx

Layout Hierarchy Example

For a deeply nested route like /dashboard/settings/security:

txt
front/
├── app.tsx                        ← Level 1: global wrapper
└── pages/
    └── dashboard/
        ├── layout.tsx             ← Level 2: dashboard wrapper
        └── settings/
            ├── layout.tsx         ← Level 3: settings wrapper
            └── security/
                ├── layout.tsx     ← Level 4: security wrapper
                └── index.tsx      ← Level 5: page component

Renders as:

App
└── Dashboard Layout
    └── Settings Layout
        └── Security Layout
            └── Security Page

Best Practices

  • Keep layouts focused. Each layout handles concerns for its own scope - dashboard navigation in the dashboard layout, not global auth state.
  • Fetch shared data at the right level. If multiple child routes need the same data, load it in their common parent layout rather than duplicating the fetch.
  • Use layouts for shared behavior. Beyond UI structure, layouts suit shared logic: permission checks, analytics, or subscription state scoped to a route group.
  • Avoid deep nesting without purpose. Three or four levels is reasonable. Beyond that, consider whether the hierarchy reflects genuine UI structure or accidental complexity.
  • Handle loading states explicitly. Loader/preload data resolves before render, but a createAsync read that suspends (SolidJS) still wants a <Suspense> fallback scoped to the data component - see the data-preload guide.

Common Pitfalls

  • Case sensitivity. Only layout.{tsx,vue,svelte,mdx} are recognized as layout files.
  • Framework file isolation. .vue/.svelte files in a React/SolidJS/MDX folder are ignored, and .tsx/.mdx files in a Vue/Svelte folder are ignored.
  • No layout opt-out. Child routes always inherit parent layouts. Routes that shouldn't share a layout belong in a different directory branch.
  • Data loading uses hooks, not props. All frameworks load layout data through a loader/preload export read with a hook (useLoaderData/createAsync). Keeping a layout's data separate from its page's is automatic in React (per-route) and Solid (via the query() key); in Vue, Svelte, and MDX the layout passes its path-qualified name to useLoaderData (a page passes nothing).

Released under the MIT License.