Layout

Full-page app-shell layout with optional header, footer, and side navigation regions.

import { ... } from '@corndev/ui'
MyApp

Page content

The layout handles header, footer, and sidebar regions.

HTML

<div class="corn-layout">
  <header class="corn-layout-header">…</header>
  <div class="corn-layout-body">
    <aside class="corn-layout-left-nav">…</aside>
    <main class="corn-layout-main">…</main>
  </div>
  <footer class="corn-layout-footer">…</footer>
</div>

<!-- An IDE-shaped shell: a 56px rail that stays beside main when narrow, and a main that does not scroll itself. -->
<div class="corn-layout" data-narrow="beside" data-main-scroll="false" style="--corn-layout-left-nav-width: 56px">
  <div class="corn-layout-body">
    <aside class="corn-layout-left-nav">…</aside>
    <main class="corn-layout-main">…</main>
  </div>
</div>

Classes

Class
.corn-layout
.corn-layout-header
.corn-layout-footer
.corn-layout-body
.corn-layout-left-nav
.corn-layout-right-nav
.corn-layout-main
.corn-layout-narrow-only
.corn-layout-wide-only

Attributes

OnAttributeValues
.corn-layoutdata-main-scrollfalse
.corn-layoutdata-narrowbeside|overlay
.corn-layoutdata-dividedpresent or absent
.corn-layout-left-navdata-openpresent or absent
.corn-layout-right-navdata-openpresent or absent

CSS variables

VariableDefaultDescription
--corn-layout-side-nav-width240pxBoth side navs’ width, unless one has its own
--corn-layout-left-nav-widththe shared widthThe left nav’s width
--corn-layout-right-nav-widththe shared widthThe right nav’s width
--corn-layout-dividervar(--corn-border-primary)The line between regions when divided is set

Vanilla JS

Required for behaviour — adds with narrow: 'overlay', opens a side nav over the page at narrow widths as a modal dialog — focus held inside, the page inert, Escape and an outside click closing it, focus returned to the toggle.

import { createLayout } from '@corndev/ui';
import '@corndev/ui/theme';

const el = document.querySelector('#target');
const instance = createLayout(el, { /* options */ });

Options

OptionTypeDefaultDescription
narrow'stack'|'beside'|'overlay''stack'What the side navs do below 960px. stack: above and below main, full width — right for a document. beside: stay where they are — right for an icon rail. overlay: hide, and open over the page with openNav() — right for a tool whose nav would otherwise be laid across the editor.
onOpenNavChange(side: 'left'|'right'|null) => voidundefinedWith narrow: 'overlay': called whenever a nav opens or closes — null when Escape, a click outside or closeNav() closes it.
leftNavLabelstring'Navigation'The left nav’s name while it is open as an overlay, which makes it a dialog.
rightNavLabelstring'Side panel'The same, for the right nav.
leftNavWidthnumber|stringundefinedThe left nav’s width — a number is pixels. Sets --corn-layout-left-nav-width.
rightNavWidthnumber|stringundefinedThe right nav’s width. Sets --corn-layout-right-nav-width.
dividedbooleanfalseDraw the regions apart: a line under the header, above the footer and along each nav’s inner edge, in --corn-layout-divider, with the navs on the page’s own background rather than a fill.
mainScrollbooleantrueWhether main scrolls itself. false makes the layout exactly the viewport’s height and main a non-scrolling flex column, so panes inside it — an editor, a response — scroll on their own.

React

Layout — from @corndev/ui-react.

Props

PropTypeDefaultDescription
headerReact.ReactNodeundefinedContent for the top bar region
footerReact.ReactNodeundefinedContent for the bottom bar region
leftNavReact.ReactNodeundefinedContent for the left sidebar
rightNavReact.ReactNodeundefinedContent for the right sidebar
narrow'stack'|'beside'|'overlay''stack'What the side navs do below 960px. stack: above and below main, full width — right for a document. beside: stay where they are — right for an icon rail. overlay: hide, and open over the page when openNav names one — right for a tool whose nav would otherwise be laid across the editor.
openNav'left'|'right'|nullnullWith narrow="overlay": the nav open over the page at narrow widths. Changes nothing at wide widths.
onOpenNavChange(side: 'left'|'right'|null) => voidundefinedWith narrow="overlay": called with null when Escape or a click outside closes the open nav. Your toggle sets openNav itself.
leftNavLabelstring'Navigation'The left nav’s name while it is open as an overlay, which makes it a dialog.
rightNavLabelstring'Side panel'The same, for the right nav.
leftNavWidthnumber|stringundefinedThe left nav’s width — a number is pixels. Sets --corn-layout-left-nav-width.
rightNavWidthnumber|stringundefinedThe right nav’s width. Sets --corn-layout-right-nav-width.
mainScrollbooleantrueWhether main scrolls itself. false makes the layout exactly the viewport’s height and main a non-scrolling flex column, so panes inside it — an editor, a response — scroll on their own.
dividedbooleanfalseDraw the regions apart: a line under the header, above the footer and along each nav’s inner edge, in --corn-layout-divider, with the navs on the page’s own background rather than a fill.
childrenReact.ReactNodeundefinedMain content
classNamestringundefined

React usage

import { Layout, Bar, Navigation } from '@corndev/ui-react';
import '@corndev/ui/theme';

<Layout
  header={<Bar>App header</Bar>}
  footer={<Bar position="bottom">Footer</Bar>}
>
  <main>Page content</main>
</Layout>

With side navigation

<Layout
  header={<Bar>Header</Bar>}
  leftNav={
    <Navigation orientation="vertical" label="Main navigation">
      <Navigation.Item href="/" active>Home</Navigation.Item>
      <Navigation.Item href="/about">About</Navigation.Item>
    </Navigation>
  }
>
  <p>Page content</p>
</Layout>

With both sidebars

<Layout
  header={<Bar>Header</Bar>}
  leftNav={<Navigation orientation="vertical">...</Navigation>}
  rightNav={<aside>Details panel</aside>}
>
  <p>Main content</p>
</Layout>

Narrow widths

Below 960px a 240px column leaves almost nothing for main, so by default the side navs stack above and below it, full width. That is right for a document and wrong for a tool, so narrow chooses:

  • stack — the default, as above.
  • beside — the navs stay beside main. For an icon rail, which is already narrow.
  • overlay — the navs hide, and open over the page when openNav names one.
const [openNav, setOpenNav] = useState(null);

<Layout
  narrow="overlay"
  openNav={openNav}
  onOpenNavChange={setOpenNav}
  header={
    <Bar>
      <button
        className="corn-layout-narrow-only"
        aria-expanded={openNav === 'left'}
        onClick={() => setOpenNav(o => (o ? null : 'left'))}
      >
        Menu
      </button>
    </Bar>
  }
  leftNav={<Navigation orientation="vertical">…</Navigation>}
>
  …
</Layout>

Give the toggle className="corn-layout-narrow-only" and it disappears at wide widths, where the navs are beside main and there is nothing to open — no breakpoint of your own to keep in step with Layout’s. corn-layout-wide-only is the opposite. Both work on anything, in plain markup too.

An open overlay is a modal dialog, through the same code Modal and Drawer run: focus moves in and Tab stays inside, the page behind — header included — is inert, Escape or a click outside calls onOpenNavChange(null), and closing hands focus back to your toggle. It is named by leftNavLabel / rightNavLabel. At wide widths openNav changes nothing: the navs are beside main as usual.

Each nav takes its own width — a 56px icon rail and a 300px project list are an ordinary pair:

<Layout leftNavWidth={56} rightNavWidth={300} leftNav={<Rail />} rightNav={<Projects />} />

In markup, set --corn-layout-left-nav-width and --corn-layout-right-nav-width; both fall back to --corn-layout-side-nav-width, then 240px. The width includes the border.

Panes that scroll themselves

Main scrolls itself by default. For an editor beside a response, each scrolling on its own, turn that off: the layout becomes exactly the viewport’s height and main a flex column that does not scroll.

<Layout mainScroll={false} header={…}>
  <Toolbar />
  <SplitPanes style={{ flex: 1, minHeight: 0 }} />
</Layout>

Give the scrolling child flex: 1 and min-height: 0, and overflow: auto on whatever should scroll.

Divided regions

By default the navs are set apart by a fill, which in a dark theme is only a few percent off the page. divided draws the regions apart instead: a line under the header, above the footer and along each nav’s inner edge, with the navs on the page’s own background.

<Layout divided header={…} leftNav={…}>…</Layout>

The line is --corn-layout-divider, which defaults to --corn-border-primary. Set it in your theme to tune region dividers apart from control borders.