On this page
Import
import { Scaffold } from '@envsemble/envui';
Usage
Render one Scaffold at the application's root, around the router's outlet, and keep it mounted as the pages change.
topBarholds the top bar, usually a TopBar.navigationholds the forms, such as{ bar: <NavigationBar … />, rail: <NavigationRail … />, drawer: <NavigationDrawer … /> }, with the same destinations.childrenis the body, the current page, usually a Page or Panes.- One form shows at a time, chosen by the Scaffold's own width, in CSS; give only the forms the application uses, and don't pick one in script from the window's width.
import { useState, type CSSProperties } from 'react';
import {
NavigationBar,
NavigationDrawer,
IconButton,
List,
Page,
PageHeader,
NavigationRail,
Scaffold,
TopBar,
type Destination,
} from '@envsemble/envui';
import { IconCategory } from '@envsemble/envui/icons/Category';
import { IconHome } from '@envsemble/envui/icons/Home';
import { IconInbox } from '@envsemble/envui/icons/Inbox';
import { IconSearch } from '@envsemble/envui/icons/Search';
/** Stands in for the window: a Scaffold fills its container, the window's height by default. */
const preview: CSSProperties = { blockSize: '22rem' };
/** What each destination shows: a list longer than the window, which scrolls under the bars. */
const contents: Record<string, { label: string; description: string }[]> = {
Home: [
{ label: 'Website refresh', description: 'Updated today by Priya Shah' },
{ label: 'Quarterly report', description: 'Updated yesterday by Sam Lee' },
{ label: 'Onboarding guide', description: 'Updated on 4 March by Alex Morgan' },
{ label: 'Mobile release', description: 'Updated on 2 March by Jordan Kim' },
{ label: 'Data migration', description: 'Updated on 27 February by Chen Wei' },
{ label: 'Annual review', description: 'Updated on 20 February by Maria Garcia' },
{ label: 'Pricing page', description: 'Updated on 18 February by Sam Lee' },
{ label: 'Support handbook', description: 'Updated on 12 February by Priya Shah' },
{ label: 'Design review', description: 'Updated on 9 February by Alex Morgan' },
{ label: 'Hiring plan', description: 'Updated on 3 February by Maria Garcia' },
{ label: 'Partner newsletter', description: 'Updated on 28 January by Jordan Kim' },
{ label: 'Security audit', description: 'Updated on 21 January by Chen Wei' },
{ label: 'Team offsite', description: 'Updated on 15 January by Sam Lee' },
{ label: 'Budget for the year', description: 'Updated on 8 January by Priya Shah' },
],
Inbox: [
{ label: 'Priya Shah', description: 'Shared the launch plan with you' },
{ label: 'Sam Lee', description: 'Commented on the quarterly report' },
{ label: 'Jordan Kim', description: 'Invited you to Mobile release' },
],
Projects: [
{ label: 'Website refresh', description: 'Due on 14 March' },
{ label: 'Quarterly report', description: 'Due on 31 March' },
{ label: 'Onboarding guide', description: 'Due on 6 April' },
{ label: 'Mobile release', description: 'Due on 20 April' },
{ label: 'Data migration', description: 'Due on 4 May' },
{ label: 'Annual review', description: 'Due on 29 May' },
],
};
export default function NavigationForms() {
const [current, setCurrent] = useState('Home');
// One array of destinations, which each navigation form takes.
const destinations: Destination[] = [
{ label: 'Home', icon: <IconHome /> },
{ label: 'Inbox', icon: <IconInbox />, count: 3 },
{ label: 'Projects', icon: <IconCategory /> },
].map((destination) => ({
...destination,
current: destination.label === current,
onClick: () => setCurrent(destination.label),
}));
return (
// The Scaffold shows the form for its own width: the bar in a compact width, the rail in a
// medium one and the drawer from expanded.
<Scaffold
scroll="body"
landmarks={false}
style={preview}
topBar={
<TopBar
title="Northwind"
actions={<IconButton icon={<IconSearch />} aria-label="Search" />}
/>
}
navigation={{
bar: <NavigationBar destinations={destinations} />,
rail: <NavigationRail destinations={destinations} />,
drawer: <NavigationDrawer destinations={destinations} />,
}}
>
<Page header={<PageHeader title={current} headingLevel={3} />}>
<List.Root aria-label={current}>
{contents[current]?.map((item) => (
<List.Item key={item.label} label={item.label} description={item.description} />
))}
</List.Root>
</Page>
</Scaffold>
);
}Which form shows
navigationForms sets which form shows at each window class, compact, medium, expanded and large, or none; the default is { compact: 'bar', medium: 'rail', expanded: 'drawer', large: 'drawer' }.
- The window classes are those of the Scaffold's own width:
compactbelow 37.5 rem,mediumfrom 37.5 to 52.5 rem,expandedfrom 52.5 to 75 rem andlargefrom 75 rem. { compact: 'none' }suits an application that opens its navigation from the top bar on a phone.- Where the form a window class asks for wasn't given, the nearest given form shows: a rail for a drawer and the other way round, then the bar.
<Scaffold
navigationForms={{ compact: 'none' }}
topBar={<TopBar title="Settings" />}
navigation={{ drawer: <NavigationDrawer aria-label="Settings" destinations={destinations} /> }}
>
…
</Scaffold>
Narrowing the navigation
A button that narrows the navigation sets navigationForms={{ expanded: 'rail', large: 'rail' }}, and the application keeps the choice, such as in localStorage.
<Scaffold
navigationForms={narrow ? { expanded: 'rail', large: 'rail' } : undefined}
topBar={
<TopBar
start={
<IconButton
icon={narrow ? <IconLeftPanelOpen /> : <IconLeftPanelClose />}
aria-label={narrow ? 'Show full navigation' : 'Narrow navigation'}
onClick={toggle}
/>
}
title="Northwind"
/>
}
navigation={{
bar: <NavigationBar aria-label="Sections" destinations={destinations} />,
rail: <NavigationRail aria-label="Sections" destinations={destinations} />,
drawer: <NavigationDrawer aria-label="Sections" destinations={destinations} />,
}}
>
…
</Scaffold>
Top bar span and backgrounds
topBarSpanisbody, the default, where the top bar sits above the body beside the rail or drawer, which run the full height, orfull, where it runs across the whole width above the navigation, as a window's title bar must.background="page"on a TopBar, a NavigationRail or a NavigationDrawer puts it on the page's background instead of the navigation's, the default; on a surface, such as a Sheet, both values take the surface's color. A Sheet is on the navigation's background, so the navigation keeps its color in the sheet of a narrow window.
<Scaffold topBar={<TopBar title="Northwind" background="page" />} navigation={{ drawer }}>
…
</Scaffold>
Scrolling
scroll is document, the default, where the page scrolls: a phone collapses its address bar, pull to refresh and a tap on the status bar work, the browser restores the scroll position, the top bar and the bar stick to the window's edges and the rail or the drawer stays in view, in a Scaffold at least as tall as the window.
- With
scroll="body", the body alone scrolls under the top bar, in a Scaffold that fills its container's height,100dvhunless a class gives it another, with the keyboard scrolling the body as it would the document. - It suits a preview or an embedded frame, a window with its own title bar, or a body of Panes whose panes scroll on their own. Give the Scaffold a block size then, as these examples do.
bodyRefreaches the body, to read or restore its scroll position.
Landmarks and skip links
The top bar is the banner landmark and the body the main one, with the id bodyId, main by default, the target of the skip link.
skipToContentLabelandskipToNavigationLabelname the skip links.landmarks={false}renders divs and no skip links, for a preview of an application inside another page's main content.namenames the node for dev mode, asdata-env-name.
Composition
The parts are the root, env-scaffold, the top bar, env-scaffold-top-bar, the navigation's places, env-scaffold-bar, env-scaffold-rail and env-scaffold-drawer, the body, env-scaffold-body, and the skip links, env-scaffold-skip-links.
- The page's title and actions go in its PageHeader, not the top bar, which belongs to the frame; the app frame pattern puts them together.
Accessibility
You provide
- The navigation forms, each a named
nav, with the current destination marked. - The page's one
h1, usually its PageHeader's title, and focus anddocument.titleafter a navigation.
envUI provides
- The top bar as the banner and the body as the main landmark;
landmarks={false}renders divs and no skip links, for a preview inside another page. - Skip links, named in the provider's language.
- Only the form that shows in the accessibility tree and the tab order.
Props
Scaffold
The invisible structure of an application's screen, after Material's: a top bar region, a navigation region and the body. It never paints. It shows one navigation form at a time by its own width, CSS only: a NavigationBar at the bottom of a compact window, a NavigationRail at the start of a medium one and a NavigationDrawer at the start from expanded. The document scrolls by default, with the top bar, the bar and the rail or the drawer kept in view; `scroll="body"` scrolls the body alone. Skip links to the body and the navigation come first.
| Prop | Type | Description |
|---|---|---|
topBar | ReactNode | The top bar region, above the body: usually a TopBar. |
navigation | ScaffoldNavigation | The navigation forms, such as `{ bar: <NavigationBar … />, rail: <NavigationRail … />, drawer: <NavigationDrawer … /> }` with the same destinations. One shows at a time, chosen by the Scaffold's own width; give only the forms the application uses. |
navigationFormsDefault { compact: 'bar', medium: 'rail', expanded: 'drawer', large: 'drawer' } | ScaffoldForms | Which form shows at each window class, or none, such as `{ compact: 'none' }` for an application that opens its navigation from the top bar on a phone. Where the form a window class asks for wasn't given, the nearest given form shows: a rail for a drawer and the other way round, then the bar. |
topBarSpanDefault 'body' | 'body''full' | body: the top bar sits above the body, beside the rail or drawer, which run the full height. full: the top bar runs across the whole width, above the navigation, as a window's title bar must. |
scrollDefault 'document' | 'document''body' | What scrolls. document: the page, as on any website, so a phone collapses its address bar, pull to refresh and a tap on the status bar work and the browser restores the scroll position; the top bar and the bar stick to the window's edges and the rail or the drawer stays in view, in a Scaffold at least as tall as the window. body: the body alone, under the top bar, in a Scaffold that fills its container's height, 100dvh unless a class gives it another, with the keyboard scrolling the body as it would the document: for a preview or an embedded frame, a window with its own title bar, or a body of Panes whose panes scroll on their own. |
bodyIdDefault 'main', or an id of its own where `landmarks` is false | string | The body's id, the target of the skip link to the content. |
bodyRef | Ref< | The body's element: with `scroll="body"` the element that scrolls, to read or restore its scroll position on. |
skipToContentLabelDefault 'Skip to content', or EnvUIProvider's messages.scaffold.skipToContent | string | The name of the skip link to the body. |
skipToNavigationLabelDefault 'Skip to navigation', or EnvUIProvider's messages.scaffold.skipToNavigation | string | The name of the skip link to the navigation. |
landmarksDefault true | boolean | false renders the regions without landmarks, a div for the banner and the main, and no skip links, for a preview of an application inside another page's main content. |
name | string | A name for this node, such as "app", which dev mode and the docs show; it renders as `data-env-name`. |
children | ReactNode | The body: the page's main content, usually a Page or Panes. |
Also accepts the native props of <div>, such as ref, className and aria-* attributes.
Part names
Each part's class and state attributes are public and versioned, for an application's CSS to style.
env-scaffold- The scaffold: the top bar, navigation and body regions; it never paints.
env-scaffold-bar- The slot of the navigation bar, at the bottom of a compact window.
env-scaffold-body- The body, the main region, which scrolls.
env-scaffold-drawer- The slot of the navigation drawer, at the start from expanded.
env-scaffold-navigation- The navigation region, which holds a slot per form.
env-scaffold-rail- The slot of the navigation rail, at the start of a medium window.
env-scaffold-skip-link- One link that skips to the body or the navigation.
env-scaffold-skip-links- The skip links, shown on keyboard focus.
env-scaffold-top-bar- The top bar region, the banner.
data-env-layoutscaffolddata-env-name- Any value, or present
data-navigation-compactbar·drawer·none·raildata-navigation-expandedbar·drawer·none·raildata-navigation-formbar·drawer·railon env-scaffold-bar, env-scaffold-drawer, env-scaffold-raildata-navigation-largebar·drawer·none·raildata-navigation-mediumbar·drawer·none·raildata-scrollbody·documentdata-skipnavigationon env-scaffold-skip-linkdata-top-bar-spanbody·full
Strings
EnvUIProvider's messages overrides each, and a component's own label prop wins over it.
messages.scaffold.skipToContent"Skip to content"The skip link to the body, the page's main content.messages.scaffold.skipToNavigation"Skip to navigation"The skip link to the navigation that shows.
Changelog
- 1.0.0
Scaffold, which replaces AppShell and WindowFrame: a top bar region, the navigation forms by window class, and the body, with the document or the body scrolling.
- 1.0.0-prerelease.5
skipLabelandskipNavigationLabelareskipToContentLabelandskipToNavigationLabel, after their messages.formsisnavigationForms, andNavigationFormisScaffoldNavigationForm, so neither reads as a form of fields.resolveNavigationFormsis internal.