Skip to content

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.

tsx
import { Footer, Header, PageLayout, Sidebar } from 'neba';

<PageLayout header={<Header />} sidebar={<Sidebar>Navigation</Sidebar>} footer={<Footer />}>
  Page
</PageLayout>;

Props

PropTypeDefaultDescription
headerReactNodeThe bar across the top. A Header, usually
footerReactNodeThe sheet at the end. A Footer, usually
sidebarReactNodeThe leading column. A Sidebar handed to this slot already knows which end it is on and needs no side of its own
endSidebarReactNodeThe 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
sidebarOpenbooleanWhether the leading sidebar's drawer is open. Use with onSidebarOpenChange for a controlled layout
defaultSidebarOpenbooleanfalseWhich state it starts in
onSidebarOpenChange(open: boolean) => voidFires as it opens and closes
endSidebarOpenbooleanThe same three for the trailing sidebar
defaultEndSidebarOpenbooleanfalseWhich state it starts in
onEndSidebarOpenChange(open: boolean) => voidFires as it opens and closes
skipLinkbooleantruePuts a "Skip to content" link first in the document, drawn only while it holds the focus, so it costs a sighted reader nothing
skipLabelstringWhat that link says. Defaults to the locale's word for it
mainIdstring'main'The id the skip link jumps to, put on the main
mainPropsOmit<ComponentPropsWithoutRef<'main'>, 'id' | 'children'>Anything else the main needs: a className, an aria-label
localestring'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
childrenReactNodeThe 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.

tsx
<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.

tsx
<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.

tsx
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>: the banner, contentinfo and complementary landmarks.
  • 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.
  • locale sets the language of the skip link and of every Sidebar and SidebarTrigger inside the layout. Unsupported tags fall back to English; skipLabel writes the word out instead.
  • A page with two sidebars must give each one a label.

Released under the MIT License