Layout
Introduction
Nest a <Layout> inside another <Layout>'s content or nested children to build composite
shells, e.g. an outer Layout with a page header/footer and an inner Layout
holding a sidenav rail.
Usage
Header + Content + Footer
With no sider, the parts stack in a single column.
import { Layout } from "../components/ui";
export default function MyPage() {
return (
<Layout
header={<SiteHeader />}
content={<Article />}
footer={<SiteFooter />}
/>
);
}
Header + Sider + Content
Passing sider wraps it and content in a row. siderWidth picks a rail
width (sm 14rem, md 16rem default, lg 18rem); siderHideBelow hides
the rail under a breakpoint for small screens.
import { Layout } from "../components/ui";
export default function MyPage() {
return (
<Layout
header={<SiteHeader />}
sider={<Sidenav />}
siderWidth="sm"
siderHideBelow="md"
content={<Article />}
/>
);
}
Sticky header and sider
stickyHeader pins the header to the top of the page scroll. stickySider
pins the sider below it and scrolls its own overflow — useful for a long
nav next to short content, or vice versa. fullHeight fills the viewport
for the outermost page shell.
import { Layout } from "../components/ui";
export default function DocsPage() {
return (
<Layout
fullHeight
stickyHeader
stickySider
header={<SiteHeader />}
sider={<DocsSidenav />}
siderHideBelow="md"
content={<Article />}
/>
);
}
Nested Layout
Nest a <Layout> inside content when the header/footer span the full
width but only part of the body needs a sider rail:
import { Layout } from "../components/ui";
export default function MyPage() {
return (
<Layout
header={<SiteHeader />}
content={
<Layout sider={<Sidenav />} content={<Article />} />
}
footer={<SiteFooter />}
/>
);
}
CMS Page Builder
This component is available as a layout block in the
Page Builder (content/pages/*.json). header,
sider, content, and footer are each a list of component blocks — but
which block types are offered depends on how deep the layout itself sits,
same as any other container in the CMS's ~4-level nesting cap:
-
Root-level — a
layoutplaced directly in a page's top-levelcontent— accepts any block type in these four parts, including containers (Stack, Card, …) and another nestedlayout:{ "type": "layout", "siderWidth": "sm", "siderHideBelow": "md", "header": [ { "type": "heading", "text": "Dashboard", "as": "h3", "size": "lg" } ], "sider": [ { "type": "stack", "direction": "vertical", "gap": "2", "children": [ { "type": "link", "text": "Overview", "href": "#" }, { "type": "link", "text": "Reports", "href": "#" } ] } ], "content": [ { "type": "text", "content": "Pick a page from the rail on the left." } ], "footer": [ { "type": "text", "size": "sm", "content": "© 2026 Acme" } ] } -
Nested one level deeper — a
layoutplaced inside another container's children (e.g. a Stack or Card) — restrictsheader/sider/content/footerto leaf components only (Button, Badge, Text, Link, Icon, …): the CMS editing UI's nesting cap bottoms out there, so it can't offer another container inside them:{ "type": "stack", "children": [ { "type": "layout", "header": [ { "type": "heading", "text": "Card sub-panel", "as": "h4", "size": "sm" } ], "content": [ { "type": "text", "content": "Leaf blocks only at this depth." } ] } ] }
Leaving a part's list empty (or omitting it) skips that part's wrapper
element entirely — an untouched optional field like siderWidth/siderHideBelow
arrives as "" from the CMS and is treated as unset, falling back to the
component default. This depth limit is a constraint of the CMS editing UI
only — app/components/page-registry.tsx's renderer itself has no depth
limit, so a hand-edited JSON file can nest a layout (or anything else)
arbitrarily deep and it will still render correctly.
Props
Layout
| Prop | Type | Description |
|---|---|---|
header | `string | JSX.Element` | Rendered inside a semantic <header> above the body. (Shorthand API only) |
sider | `string | JSX.Element` | Rendered inside a semantic <aside> rail. Its presence switches the body to a row of sider + content. (Shorthand API only) |
content | `string | JSX.Element` | Rendered inside a semantic <main>. children are appended after it. (Shorthand API only) |
footer | `string | JSX.Element` | Rendered inside a semantic <footer> below the body. (Shorthand API only) |
fullHeight | boolean | Fill the viewport height — for the outermost page shell. |
stickyHeader | boolean | Pin the header to the top of the page scroll. |
stickySider | boolean | Pin the sider below a sticky header; it scrolls internally. |
siderWidth | `"sm" | "md" | "lg"` | Sider rail width: sm (14rem), md (16rem, default), lg (18rem). |
siderHideBelow | `"sm" | "md" | "lg"` | Hide the sider under this breakpoint. Pair with an in-flow disclosure. |
hasSider | boolean | Manually force layout direction to row. (Compound API) |
headerClass | string | Extra class for the <header> part. (Shorthand API only) |
siderClass | string | Extra class for the <aside> part. (Shorthand API only) |
contentClass | string | Extra class for the <main> part. (Shorthand API only) |
footerClass | string | Extra class for the <footer> part. (Shorthand API only) |
bodyClass | string | Extra class for the row wrapper around sider + content (only rendered when sider is set). |
children | any | Children to render inside the layout shell. |
class | string | Custom CSS classes for the root element. |