Nested Layouts

Layouts provide nested UI and request handling (middleware) to a set of routes:

  • Shared request handling: Accomplished by adding an onRequest method.
  • Shared UI: Accomplished by export default a 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.

src/routes/layout.tsx
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.

src/routes/index.tsx
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.

src/routes/about/index.tsx
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:

src/routes/layout.tsx
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):

src/routes/index.tsx
// 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:

src/routes/index.tsx
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):

src/routes/layout.tsx
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):

src/routes/products/layout.tsx
// 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):

src/routes/products/index.tsx
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:

ElementSourceGoes to
<nav q:slot="breadcrumb">โ€ฆinner layoutouter breadcrumb slot
<aside q:slot="sidebar">โ€ฆpageouter sidebar slot
<div><h1>โ€ฆpageouter 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:

src/routes/products/layout.tsx โ€” โŒ wrapping children hides q:slot attributes
// 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.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • manucorporat
  • adamdbradley
  • Oyemade
  • mhevery
  • nnelgxorz
  • the-r3aper7
  • mrhoodz
  • aendel
  • jemsco
  • copilot