Layout
Full-page app-shell layout with optional header, footer, and side navigation regions.
import { ... } from '@corndev/ui' 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
| On | Attribute | Values |
|---|---|---|
.corn-layout | data-main-scroll | false |
.corn-layout | data-narrow | beside|overlay |
.corn-layout | data-divided | present or absent |
.corn-layout-left-nav | data-open | present or absent |
.corn-layout-right-nav | data-open | present or absent |
CSS variables
| Variable | Default | Description |
|---|---|---|
--corn-layout-side-nav-width | 240px | Both side navs’ width, unless one has its own |
--corn-layout-left-nav-width | the shared width | The left nav’s width |
--corn-layout-right-nav-width | the shared width | The right nav’s width |
--corn-layout-divider | var(--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
| Option | Type | Default | Description |
|---|---|---|---|
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) => void | undefined | With narrow: 'overlay': called whenever a nav opens or closes — null when Escape, a click outside or closeNav() closes it. |
leftNavLabel | string | 'Navigation' | The left nav’s name while it is open as an overlay, which makes it a dialog. |
rightNavLabel | string | 'Side panel' | The same, for the right nav. |
leftNavWidth | number|string | undefined | The left nav’s width — a number is pixels. Sets --corn-layout-left-nav-width. |
rightNavWidth | number|string | undefined | The right nav’s width. Sets --corn-layout-right-nav-width. |
divided | boolean | false | Draw 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. |
mainScroll | boolean | true | Whether 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
| Prop | Type | Default | Description |
|---|---|---|---|
header | React.ReactNode | undefined | Content for the top bar region |
footer | React.ReactNode | undefined | Content for the bottom bar region |
leftNav | React.ReactNode | undefined | Content for the left sidebar |
rightNav | React.ReactNode | undefined | Content 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'|null | null | With narrow="overlay": the nav open over the page at narrow widths. Changes nothing at wide widths. |
onOpenNavChange | (side: 'left'|'right'|null) => void | undefined | With narrow="overlay": called with null when Escape or a click outside closes the open nav. Your toggle sets openNav itself. |
leftNavLabel | string | 'Navigation' | The left nav’s name while it is open as an overlay, which makes it a dialog. |
rightNavLabel | string | 'Side panel' | The same, for the right nav. |
leftNavWidth | number|string | undefined | The left nav’s width — a number is pixels. Sets --corn-layout-left-nav-width. |
rightNavWidth | number|string | undefined | The right nav’s width. Sets --corn-layout-right-nav-width. |
mainScroll | boolean | true | Whether 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. |
divided | boolean | false | Draw 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. |
children | React.ReactNode | undefined | Main content |
className | string | undefined |
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 whenopenNavnames 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.
Nav widths
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.