PageLayout
The skeleton a page is hung on: a header, a footer, one sidebar or two, and the content between them. It arranges the regions and draws no surface of its own.
import { Footer, Header, PageLayout, Sidebar } from 'neba';
<PageLayout header={<Header />} sidebar={<Sidebar>Navigation</Sidebar>} footer={<Footer />}>
Page
</PageLayout>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| header | ReactNode | — | The bar across the top. A Header, usually |
| footer | ReactNode | — | The sheet at the end. A Footer, usually |
| sidebar | ReactNode | — | The leading column. A Sidebar handed to this slot already knows which end it is on and needs no side of its own |
| endSidebar | ReactNode | — | The trailing column, for navigation down one side and a table of contents, an inspector or a filter panel down the other |
| headerSpan | 'full' | 'content' | 'full' | Which of the header and the sidebars takes the top corner. full spans the whole width with the sidebars beginning underneath it(a website; content runs the sidebars the full height with the header between them) an application |
| footerSpan | 'full' | 'content' | 'full' | The same question for the footer, and worth answering separately: a dashboard with a full-height rail still usually wants its copyright line under the content rather than under the rail |
| scroll | 'page' | 'content' | 'page' | What scrolls. page scrolls the document and the header holds its place with sticky; content pins the layout to the window and scrolls only the region between the bars |
| height | 'viewport' | 'auto' | number | string | 'viewport' | How tall the layout is. viewport is the window's, auto is its parent's(an app shell inside a Mockup's screen, a preview) and a length is exactly that. Numbers are pixels |
| collapseBelow | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none' | 'md' | The width below which the sidebars stop being columns and become drawers, with a SidebarTrigger as the way to open them. none keeps them as columns at every width |
| sidebarOpen | boolean | — | Whether the leading sidebar's drawer is open. Use with onSidebarOpenChange for a controlled layout |
| defaultSidebarOpen | boolean | false | Which state it starts in |
| onSidebarOpenChange | (open: boolean) => void | — | Fires as it opens and closes |
| endSidebarOpen | boolean | — | The same three for the trailing sidebar |
| defaultEndSidebarOpen | boolean | false | Which state it starts in |
| onEndSidebarOpenChange | (open: boolean) => void | — | Fires as it opens and closes |
| skipLink | boolean | true | Puts a "Skip to content" link first in the document, drawn only while it holds the focus, so it costs a sighted reader nothing |
| skipLabel | string | — | What that link says. Defaults to the locale's word for it |
| mainId | string | 'main' | The id the skip link jumps to, put on the main |
| mainProps | Omit<ComponentPropsWithoutRef<'main'>, 'id' | 'children'> | — | Anything else the main needs: a className, an aria-label |
| locale | string | 'en' | The language the layout's own words are in. Inherited by every Sidebar and SidebarTrigger inside it, so it is written once per page |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The colour family the skip link and the focus rings light up in. The layout itself draws no surface |
| children | ReactNode | — | The page. Rendered inside the main |
Every native <div> attribute passes through. The shared axes are described under prop conventions.
The children go inside a real <main>, which is what mainId names and what the skip link jumps to.
It draws no gutter and no measure: put a Container inside, where a page can hold a wide dashboard on one route and a narrow article on the next.
Examples
headerSpan
Which of the header and the sidebars takes the top corner. full (the default) spans the bar across the whole width with the sidebars beginning underneath it; content runs the sidebars the full height of the window and puts the bar between them. footerSpan answers the same question separately.
collapseBelow
Below this width both sidebars stop being columns and become drawers. A SidebarTrigger is what opens them, and it appears at exactly the same width. none keeps the columns at every width.
Two sidebars
sidebar is the leading column and endSidebar the trailing one. Each is a Sidebar with its own width, its own drawer and its own trigger, and neither needs a side prop: the slot decides. Give both a label, or a screen reader offers two regions called "complementary".
scroll
page, the default, lets the document scroll: the browser's own address bar hides on a phone, the scroll position is restored on a back navigation, and a Header holds its place with position: sticky. content pins the layout to the height of the window and scrolls only the region between the bars.
<PageLayout scroll="content" header={<Header />} sidebar={<Sidebar>Files</Sidebar>}>
Workspace
</PageLayout>height
viewport is the window's height, so a short page still pushes its footer to the bottom of the screen. auto is the parent's, for a layout that is not the page: an app shell inside a Mockup's screen, a preview. A number or a CSS length is exactly that.
<div className="h-96">
<PageLayout height="auto" scroll="content" header={<Header />}>
Preview
</PageLayout>
</div>Controlling the drawers
sidebarOpen and onSidebarOpenChange control the leading drawer, endSidebarOpen and onEndSidebarOpenChange the trailing one. Reach for them when a route change should close the drawer behind it.
const [open, setOpen] = useState(false);
<PageLayout sidebarOpen={open} onSidebarOpenChange={setOpen} sidebar={<Sidebar>Nav</Sidebar>}>
Page
</PageLayout>;Accessibility
- The children are wrapped in a
<main>, and the header, the footer and the sidebars carry<header>,<footer>and<aside>: thebanner,contentinfoandcomplementarylandmarks. - A "Skip to content" link is the first thing in the document, drawn only while it holds the focus. Turn it off with
skipLink={false}only if the page already has one. localesets the language of the skip link and of every Sidebar and SidebarTrigger inside the layout. Unsupported tags fall back to English;skipLabelwrites the word out instead.- A page with two sidebars must give each one a
label.