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.tsxFor /dashboard/settings/profile, the render order is:
app.tsx (global wrapper)
└── dashboard/layout.tsx
└── dashboard/settings/layout.tsx
└── dashboard/settings/profile/index.tsxNo 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.
| Framework | Recognized name |
|---|---|
| React / SolidJS | layout.tsx |
| Vue | layout.vue |
| Svelte | layout.svelte |
| MDX | layout.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:
// 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, anduseLoaderData()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 thequery()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.
// 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.
front/
├── app.tsx ← wraps everything
└── pages/
├── dashboard/
│ └── layout.tsx
└── index/
└── index.tsxLayout Hierarchy Example
For a deeply nested route like /dashboard/settings/security:
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 componentRenders as:
App
└── Dashboard Layout
└── Settings Layout
└── Security Layout
└── Security PageBest 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
createAsyncread 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/.sveltefiles in a React/SolidJS/MDX folder are ignored, and.tsx/.mdxfiles 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/preloadexport 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 thequery()key); in Vue, Svelte, and MDX the layout passes its path-qualified name touseLoaderData(a page passes nothing).