Nested Layouts
Layouts provide nested UI and request handling (middleware) to a set of routes:
- Shared request handling: Accomplished by adding an
onRequestmethod. - Shared UI: Accomplished by
export defaulta Qwik component.
Example
Now, combine all the previously discussed concepts to build a full app.
In the proposed example, you will notice a site with 2 pages: https://example.com and https://example.com/about. The goal is to add a common header and footer to all the pages, the only difference between the pages is the content in the middle.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Header โ
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Menu โ <ROUTE_SPECIFIC_CONTENT> โ
โ - home โ โ
โ - about โ โ
โ โ โ
โโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Footer โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
First, create three components: <Header>, <Footer>, and <Menu>.
The developer could copy-paste these components manually into each page component, but that is repetitive and error-prone. Instead, use layouts to automatically reuse common parts.
Routes directory
src/
โโโ components/
โ โโโ header.tsx # Header component implementation
โ โโโ footer.tsx # Footer component implementation
โ โโโ menu.tsx # Menu component implementation
โโโ routes/
โโโ layout.tsx # Layout implementation using: <Header>, <Footer>, and <Menu>
โโโ about/
โ โโโ index.tsx # https://example.com/about
โโโ index.tsx # https://example.com
src/routes/layout.tsx
It will be used for all routes under the src/routes directory. It will render the Header, Menu, and Footer components, and also render the nested routes under the Slot component.
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<>
<Header />
<Menu />
<Slot /> {/* <== This is where the route will be inserted */}
<Footer />
</>
);
});
src/routes/index.tsx
This is the main route for the site. It will be rendered within the Slot component in the src/routes/layout.tsx file. Even though the Header, Menu, or Footer components are not referenced, it will still be rendered with them.
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <>Home</>;
});
src/routes/about/index.tsx
Similar to the src/routes/index.tsx file, the about route will also be rendered within the Slot component in the src/routes/layout.tsx file. Even though the Header, Menu, or Footer components are not referenced, it will still be rendered with them.
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <>About</>;
});
When you run the app, Qwik will render the About nested inside the RootLayout
<RootLayout>
<AboutPage />
</RootLayout>
Here you can read more about advanced routing scenarios like named layout.
Named Slots in Layouts
A layout can define multiple named <Slot /> areas (for example a sidebar, a breadcrumb bar, or a secondary action bar). Because the Qwik City router passes each page as a single component to its parent layout, using named slots requires a special technique: the page's default export must be an inline component (a plain function) instead of a component$.
Why inline components?
When the router composes layouts and pages it builds a tree like:
<Layout>
<Page /> {/* single component$ โ creates a component boundary */}
</Layout>
Qwik's slot system splits the children of a component by their q:slot attribute before rendering. For a component$ child the boundary is opaque, so q:slot attributes inside the page's output are invisible to the layout. An inline component (a plain function) has no such boundary โ Qwik calls it directly and its returned JSX elements are visible to the layout's slot system.
Example
Layout with named slots:
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<div class="layout">
<nav>
<Slot name="breadcrumb" />
</nav>
<main>
<Slot /> {/* default slot โ receives the page body */}
</main>
<aside>
<Slot name="sidebar" />
</aside>
</div>
);
});
Page that fills named slots (inline component as default export):
// Inline component โ NOT component$, so no component boundary is created.
// Qwik calls this function directly and its JSX output is split across
// the layout's named slots based on the q:slot attributes.
export default () => (
<>
<nav q:slot="breadcrumb">Home / Products</nav>
<aside q:slot="sidebar">
<p>Filter by category</p>
</aside>
<div>
<h1>Products</h1>
<p>Main page content goes here.</p>
</div>
</>
);
Elements without a q:slot attribute go into the layout's default <Slot />, and elements with q:slot="name" go into the matching named <Slot name="name" />.
Using hooks inside an inline page
Inline components cannot use Qwik hooks (useSignal, useStore, useTask$, etc.) directly. If your page needs reactive state, extract that part into a component$ and render it inside the inline component:
import { component$, useSignal } from '@builder.io/qwik';
// component$ for the part that needs reactive state
const ProductList = component$(() => {
const filter = useSignal('');
return (
<div>
<input bind:value={filter} placeholder="Searchโฆ" />
<p>Showing results for: {filter.value}</p>
</div>
);
});
// Inline component as default export โ routes content to the layout slots
export default () => (
<>
<nav q:slot="breadcrumb">Home / Products</nav>
<aside q:slot="sidebar">
<p>Static sidebar content</p>
</aside>
<ProductList /> {/* goes into the default slot */}
</>
);
Note: The named-slot technique relies on inline components not creating a component boundary. Keep the inline default export as a thin routing shell and put all business logic inside
component$children.
Nested layouts with named slots
When there are nested layouts (an outer layout wrapping an inner layout wrapping a page), any intermediate layout that sits between the outer layout's named slots and the page must also be an inline component. It must render {children} directly inside a fragment โ not wrapped in any HTML element โ so that q:slot attributes propagate all the way up to the outer layout.
src/routes/
โโโ layout.tsx โ outer layout (component$ โ defines named slots)
โโโ products/
โ โโโ layout.tsx โ inner layout (inline โ must pass {children} through a fragment)
โ โโโ index.tsx โ page (inline โ provides q:slot content)
Outer layout (component$ with named slots โ unchanged):
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<div class="layout">
<nav><Slot name="breadcrumb" /></nav>
<main><Slot /></main>
<aside><Slot name="sidebar" /></aside>
</div>
);
});
Inner layout (inline component โ passes {children} top-level in a fragment):
// Inline component โ renders its own content alongside {children} in a fragment.
// {children} MUST be top-level (not wrapped in any HTML element) so that
// q:slot attributes from nested pages propagate to the outer layout.
export default ({ children }: any) => (
<>
{/* contributes to the outer layout's "breadcrumb" slot */}
<nav q:slot="breadcrumb">
<a href="/">Home</a> / <a href="/products">Products</a>
</nav>
{children} {/* top-level โ q:slot attributes inside are still visible to outer layout */}
</>
);
Page (inline component โ provides its own named-slot content):
export default () => (
<>
<aside q:slot="sidebar">
<p>Category filters</p>
</aside>
<div>
<h1>All Products</h1>
</div>
</>
);
When the router composes these three layers the outer layout's splitProjectedChildren receives a flat list of JSX nodes:
| Element | Source | Goes to |
|---|---|---|
<nav q:slot="breadcrumb">โฆ | inner layout | outer breadcrumb slot |
<aside q:slot="sidebar">โฆ | page | outer sidebar slot |
<div><h1>โฆ | page | outer default slot |
What breaks the propagation:
If the inner layout wraps {children} in an HTML element, Qwik stops flattening at that element boundary and the inner q:slot attributes from the page are no longer visible to the outer layout:
// WRONG โ the <div> wrapper stops q:slot propagation to the outer layout
export default ({ children }: any) => (
<div class="product-area">
{children} {/* q:slot attributes inside children are hidden from the outer layout */}
</div>
)
You will either need to repeat the Slot structure, or move the wrapper div to the child component.