Layouts
Layouts are page-shell components — they own the chrome (sidebar,
header, footer) and slot in your route content (typically a
react-router <Outlet />).
Everything lives under a single subpath, @omnitron-dev/prism/layouts.
There are no /layouts/dashboard, /layouts/auth, or
/layouts/core deep imports — the package exports map only declares
./layouts, so deep imports will not resolve.
import {
// Application shell
DashboardLayout,
// Auth shells
AuthCenteredLayout,
AuthSplitLayout,
AuthSimpleLayout,
// Context / config
LayoutProvider,
useLayoutContext,
useLayoutConfig,
useNavActive,
// Core primitives (build-your-own)
LayoutSection,
MainSection,
HeaderSection,
UserMenu,
} from '@omnitron-dev/prism/layouts';| Export | What it is |
DashboardLayout | Full app shell — sidenav / topnav / combo + header + content |
DashboardContent | Padded content wrapper for use inside the dashboard <main> |
PanelLayout / SidebarToggle | Nested navigation with independent collapse state and a shared edge control |
Sidenav / MobileSidenav | The desktop / mobile sidebar (rendered by DashboardLayout) |
AuthCenteredLayout | Card-centered auth shell (sign-in / sign-up / reset) |
AuthSplitLayout | Split-screen auth shell (illustration + form) |
AuthSimpleLayout | Minimal auth/error/maintenance shell |
LayoutProvider | Context provider holding layout config + nav state |
LayoutSection / MainSection | Structural primitives for custom shells |
HeaderSection / HeaderToolbar | Slot-based sticky header |
UserMenu | Avatar-trigger account dropdown for the header rightArea |
There is no single <AuthLayout> component (only an
AuthLayoutProps / AuthLayoutVariant type), and there is no
<CoreLayout> — the minimal shell is composed from LayoutSection +
HeaderSection, or you reach for AuthSimpleLayout.
Secondary navigation with <PanelLayout>
Use one application DashboardLayout and place PanelLayout inside it for
corpus contents, document navigation, graph indexes or administration tools.
Each panel can collapse independently. The main editor remains mounted when a
panel collapses or its mobile drawer opens. Render the panel as a function to
provide compact navigation while retaining the same component and its state.
A plain React node remains mounted but hidden when collapsed. Mobile drawers
retain their body between openings.
import { PanelLayout } from '@omnitron-dev/prism/layouts';
<PanelLayout
title="Document contents"
labels={{ expand: 'Expand contents', collapse: 'Collapse contents', close: 'Close contents' }}
persistKey="research.document-contents"
navigationKey={selectedDocumentId}
panel={({ collapsed }) => <DocumentContents compact={collapsed} />}
>
<DocumentEditor />
</PanelLayout>PanelLayout does not require a router. navigationKey closes the mobile
drawer after the host changes selection; route-based applications can pass
useLocation().key. It does not reset the desktop preference. Use distinct
persistKey values for the application sidebar and each kind of nested panel.
The default desktop breakpoint is md, expanded width is 264 px, and sticky
offset is 88 px. The compact rail is 64 px wide. Its height is identical in both
modes: by default calc(100dvh - (offset + 24px)). Override height for embedded
surfaces such as a graph, and collapsedWidth, layoutQuery, width, offset
and sx as needed. DocSidebar supports compact, full tooltip/accessibility
labels, active destination identity and optional item shortLabel values.
Hide auxiliary search controls instead of unmounting them when retaining their
input state matters. The dashboard and nested panels share SidebarToggle and
the themed Scrollbar; scrollbar styling also applies to nested overflow
regions through Prism's CSS baseline. On a fine pointer, thumbs reveal on hover
or keyboard focus inside the region without changing layout. Touch input keeps
the native controls visible; forced-colors mode keeps native colors and dimensions.
It uses native MUI controls and the shared Prism settings store, including its storage-failure fallback. The panel body may remount when crossing the mobile breakpoint; lift durable drafts and search state into the owning feature.
DashboardLayout.labels localizes opening/closing navigation, collapse/expand,
main landmark and skip link. slotProps.main.id and className integrate with
the application's focus and fragment navigation. Its mobile drawer closes on
actual router navigation; collapsed destinations retain accessible names and
aria-current values. A public website and a workspace should render separate
shells, rather than placing the full application inside a bounded marketing
container.
<DashboardLayout>
Admin / operator console shell. It wraps its children in a
LayoutProvider and renders a header plus a
sidenav, a topnav, or both — driven by initialConfig.navigationMenuType
('sidenav' by default).
import { DashboardLayout, UserMenu } from '@omnitron-dev/prism/layouts';
import type { LayoutNavData } from '@omnitron-dev/prism/layouts';
import { Outlet } from 'react-router-dom';
const navData: LayoutNavData = [
{
id: 'main',
subheader: 'Overview',
items: [
{ id: 'dashboard', title: 'Dashboard', path: '/', icon: <DashboardIcon /> },
{ id: 'apps', title: 'Apps', path: '/apps', icon: <AppsIcon /> },
],
},
{
id: 'infra',
subheader: 'Infrastructure',
items: [
{
id: 'infrastructure',
title: 'Infrastructure',
icon: <GridIcon />,
children: [
{ id: 'containers', title: 'Containers', path: '/containers' },
{ id: 'nodes', title: 'Nodes', path: '/nodes' },
],
},
{
id: 'settings',
title: 'Settings',
path: '/settings',
icon: <CogIcon />,
allowedRoles: ['admin'],
},
],
},
];
function ConsoleShell() {
return (
<DashboardLayout
logo={<Logo size={32} />}
navData={navData}
headerSlots={{
centerArea: <CommandPaletteTrigger />,
rightArea: (
<>
<NotificationsBell />
<UserMenu user={{ name: 'Ada Lovelace', email: 'ada@example.com' }} />
</>
),
}}
initialConfig={{ navigationMenuType: 'sidenav', sidenavVariant: 'default' }}
persistKey="console"
>
<Outlet />
</DashboardLayout>
);
}Bounded workspaces
For terminals, editors and infrastructure consoles, set contentMode="workspace". The shell uses 100dvh, keeps its header and footer in the available height, and gives the main region minHeight: 0 and hidden overflow. Children should use flex sizing with minHeight: 0 and minWidth: 0; put scrolling on the panel that owns it. To scroll ordinary pages inside the same shell, use slotProps={{ main: { sx: { overflowY: 'auto' } } }}. Default document mode retains normal page scrolling for other applications.
Props
| Prop | Type | Default | Notes |
children | ReactNode | — | Page content (your <Outlet />) |
contentMode | 'document' | 'workspace' | 'document' | Document flow, or a viewport-bounded shell whose panels own scrolling |
navData | LayoutNavData | — | Navigation tree — LayoutNavSection[] (see below) |
headerSlots | HeaderSlots | — | Header content per slot (leftArea / centerArea / rightArea / topArea / bottomArea) |
logo | ReactNode | — | Logo shown in the sidenav (and as the mobile header left slot) |
footer | ReactNode | — | Rendered at the bottom of the main column |
sidenavFooter | ReactNode | — | Footer content pinned inside the sidenav |
layoutQuery | Breakpoint | 'lg' | Breakpoint at/above which the desktop sidenav shows and the mobile drawer/menu-button hides |
initialConfig | Partial<LayoutConfig> | — | Initial layout config (nav type, sidenav variant, nav color, …) |
persistKey | string | — | When set, sidenav collapsed state persists to localStorage under this key. Use a unique key per layout (e.g. 'main', 'admin') |
slotProps | { header?, main? } | — | header forwards to HeaderSection; main accepts { sx, disablePadding, maxWidth } |
There is no brand / navItems / topRight / topLeft /
sidebarWidth / defaultCollapsed / persistCollapse /
showBreadcrumbs / userRoles prop. The header is composed via
headerSlots; the logo goes through logo; nav uses navData; and
nav-item visibility is expressed per-item with allowedRoles /
allowedPermissions (see LayoutNavItem).
LayoutNavData
Navigation is a list of sections, each with an optional subheader and a list of items:
type LayoutNavData = LayoutNavSection[];
interface LayoutNavSection {
id: string;
subheader?: string; // group heading
items: LayoutNavItem[];
}LayoutNavItem
interface LayoutNavItem {
id: string; // unique item ID (required)
title: string; // display label
path?: string; // route (leaf items)
icon?: ReactNode; // icon element
info?: ReactNode; // trailing info/label
caption?: string; // secondary description text
disabled?: boolean;
external?: boolean; // external link
children?: LayoutNavItem[]; // nested menu — expands inline
allowedRoles?: string[]; // visibility filter (roles)
allowedPermissions?: string[]; // visibility filter (permissions)
deepMatch?: boolean; // active on child routes (default: true)
badge?: string | number; // count / "new" indicator
selectionPrefix?: string | string[]; // keep active across disjoint URL spaces
}Active-state rules (implemented in isNavItemActive):
Exact match —
pathequals the current pathname (query/hash and trailing slashes are stripped first).selectionPrefix— active when the path starts with any listed prefix. Accepts a string or an array (any prefix wins) so one entry can span two URL spaces, e.g.selectionPrefix: ['/communities', '/c/'].deepMatch(defaulttrue) — active whenpathis a segment-aligned prefix of the pathname (/admin/orgsstays highlighted on/admin/orgs/list, but/productdoes not match/products). SetdeepMatch: falseto require an exact match.
Items with children render as inline-expandable groups; a parent
stays highlighted when one of its descendants is active.
Behaviour
Navigation type —
initialConfig.navigationMenuTypeselects'sidenav'(default),'topnav', or'combo'(both). Sidenav and combo renderSidenav+MobileSidenav; topnav and combo render theTopnavbar.Sidenav variants —
initialConfig.sidenavVariantis'default','mini', or'stacked'; widths come fromDRAWER_WIDTHS(full: 280,mini: 73,stackedExpanded: 300,stackedCollapsed: 72).Responsive — at/above
layoutQuerythe desktopSidenavshows and the main content is offset by the drawer width; below it, the sidebar becomes aMobileSidenavdrawer toggled by the injected header menu button.Collapse persistence — toggling collapse persists to the settings store under
persistKey(and is restored on mount). WithoutpersistKey, collapse is in-memory only.Accessibility — renders a
SkipLinkto#main-content, arole="main"landmark, and ARIA labels for the header / nav.
DashboardContent
Optional padded wrapper for content rendered inside the dashboard
<main> (handy when you don't pass slotProps.main):
import { DashboardContent } from '@omnitron-dev/prism/layouts';
<DashboardContent maxWidth="xl">
<PageHeader title="Apps" />
<AppsTable />
</DashboardContent>| Prop | Type | Default |
children | ReactNode | — |
disablePadding | boolean | false |
maxWidth | Breakpoint | false | — |
sx | SxProps<Theme> | — |
Layout density
Density is a setting on Prism's settings store, not a layout prop —
read/write it with useSettingsStore:
import { useSettingsStore } from '@omnitron-dev/prism';
function DensityToggle() {
const density = useSettingsStore((s) => s.density);
const setDensity = useSettingsStore((s) => s.setDensity);
return (
<select value={density} onChange={(e) => setDensity(e.target.value as any)}>
<option value="compact">Compact</option>
<option value="standard">Standard</option>
<option value="comfortable">Comfortable</option>
</select>
);
}ComponentDensity is 'compact' | 'standard' | 'comfortable'. Tables,
lists, and forms respond to it via theme spacing tokens.
Auth layouts
There are three auth shells. They share a help-link header pattern but
differ in how the form is framed. None of them require a LayoutProvider.
<AuthCenteredLayout>
Card-centered form on a subtle gradient — the default for sign-in / sign-up / reset / verify.
import { AuthCenteredLayout } from '@omnitron-dev/prism/layouts';
function SignInPage() {
return (
<AuthCenteredLayout logo={<Logo />}>
<SignInForm />
</AuthCenteredLayout>
);
}| Prop | Type | Default |
children | ReactNode | — |
logo | ReactNode | — |
maxWidth | number | string | 420 |
sx | SxProps<Theme> | — |
Companion: AuthCenteredContent — a standalone card wrapper
(children, maxWidth = 420, sx) for composing your own centered shell.
<AuthSplitLayout>
Split-screen: branded illustration pane on the left (hidden below md),
form on the right. Includes a built-in help link in the header.
import { AuthSplitLayout } from '@omnitron-dev/prism/layouts';
function SignInPage() {
return (
<AuthSplitLayout
logo={<Logo />}
title="Welcome to Platform"
subtitle="Sign in to continue to your dashboard"
illustration={<img src="/auth-illustration.svg" alt="" />}
>
<SignInForm />
</AuthSplitLayout>
);
}| Prop | Type | Default |
children | ReactNode | — |
logo | ReactNode | — |
title | string | 'Welcome' |
subtitle | string | — |
illustration | ReactNode | — |
showHelp | boolean | true |
helpUrl | string | '/help' |
helpText | string | 'Need help?' |
settingsButton | ReactNode | — |
maxWidth | number | string | 480 |
illustrationSx | SxProps<Theme> | — |
sx | SxProps<Theme> | — |
The illustration pane is hidden below the md breakpoint; the form
then takes the full width. Companions AuthSplitIllustration
(title / subtitle / illustration / sx) and AuthSplitContent
(children / maxWidth / sx) let you assemble a custom split.
<AuthSimpleLayout>
Minimal shell with just a help-link header — for error pages (404 / 500), maintenance pages, or compact auth forms.
import { AuthSimpleLayout } from '@omnitron-dev/prism/layouts';
function NotFoundPage() {
return (
<AuthSimpleLayout logo={<Logo />} compact>
<NotFoundContent />
</AuthSimpleLayout>
);
}| Prop | Type | Default |
children | ReactNode | — |
logo | ReactNode | — |
showHelp | boolean | true |
helpUrl | string | '/help' |
helpText | string | 'Need help?' |
settingsButton | ReactNode | — |
compact | boolean | false |
maxWidth | Breakpoint | false | 'lg' |
sx | SxProps<Theme> | — |
With compact, content is centered in a 448px column; otherwise it
fills a <Container maxWidth={maxWidth}>. Companion:
SimpleCompactContent (children / sx) for the compact wrapper alone.
<LayoutProvider>
DashboardLayout mounts a LayoutProvider for you. Use it directly
only when you build a custom dashboard-style shell from the primitives
and need access to layout config + nav state.
import { LayoutProvider, useLayoutContext } from '@omnitron-dev/prism/layouts';
<LayoutProvider initialConfig={{ navigationMenuType: 'combo' }} persistKey="admin">
<CustomShell />
</LayoutProvider>| Prop | Type | Default |
children | ReactNode | — |
initialConfig | Partial<LayoutConfig> | — |
persistKey | string | — |
LayoutConfig holds: navigationMenuType ('sidenav' \| 'topnav' \| 'combo'),
sidenavVariant ('default' \| 'mini' \| 'stacked'), sidenavCollapsed,
topnavVariant, navColor ('default' \| 'vibrant' \| 'integrate'),
drawerOpen, drawerWidth, and compactLayout.
Hooks
| Hook | Returns |
useLayoutContext() | Full context: config, isMobile, setConfig, toggleDrawer, toggleSidenavCollapse, setNavigationMenuType, setSidenavVariant, setNavColor, openItems / toggleItem / isItemOpen, isNavItemActive, isNestedItemActive. Throws outside a LayoutProvider. |
useLayoutConfig() | Just the current LayoutConfig. |
useSidenavVisible() | true for 'sidenav' / 'combo'. |
useTopnavVisible() | true for 'topnav' / 'combo'. |
useNavActive(pathname) | { isItemActive, isNestedActive } for a given pathname (pass useLocation().pathname from your router). |
import { useLayoutContext } from '@omnitron-dev/prism/layouts';
function CollapseToggle() {
const { config, toggleSidenavCollapse } = useLayoutContext();
return (
<IconButton onClick={toggleSidenavCollapse}>
{config.sidenavCollapsed ? <MenuOpenIcon /> : <MenuIcon />}
</IconButton>
);
}Core primitives (custom shells)
When none of the pre-built shells fit, compose your own from the core
primitives. There is no <CoreLayout> — LayoutSection is the root
container, HeaderSection is the slotted header, and MainSection is a
padded <main>.
import {
LayoutSection,
MainSection,
HeaderSection,
Sidenav,
} from '@omnitron-dev/prism/layouts';
function CustomShell() {
return (
<LayoutSection
headerSection={
<HeaderSection
slots={{ leftArea: <Logo />, rightArea: <UserMenu user={user} /> }}
/>
}
sidebarSection={<Sidenav navData={navData} />}
footerSection={<Footer />}
>
<MainSection>
<Outlet />
</MainSection>
</LayoutSection>
);
}LayoutSection
| Prop | Type | Notes |
headerSection | ReactNode | Rendered at the top |
sidebarSection | ReactNode | Renders a flex sidebar+main row when present |
footerSection | ReactNode | Rendered at the bottom of the main column |
children | ReactNode | Main content |
cssVars | Record<string, string | number> | Override layout CSS variables |
sx | SxProps<Theme> | — |
MainSection takes just children + sx (padded <main> flex column).
HeaderSection
A sticky, slot-based AppBar with a scroll blur/elevation effect. Used
internally by the dashboard and split/simple auth layouts; usable
standalone.
| Prop | Type | Default | Notes |
slots | HeaderSlots | — | topArea / leftArea / centerArea / rightArea / bottomArea |
slotProps | per-slot SxProps + container | — | Style each slot |
disableSticky | boolean | false | Use position="static" |
disableOffset | boolean | false | Disable the scroll-blur background layer |
disableElevation | boolean | false | Disable the scroll shadow |
containerized | boolean | false | Wrap the toolbar in a max-width Container |
maxWidth | Breakpoint | false | 'lg' | Container width when containerized |
sx | SxProps<Theme> | — | — |
Companion HeaderToolbar (children / sx) is the bare toolbar
without the AppBar wrapper.
UserMenu
Avatar-trigger account dropdown, designed for the header rightArea.
import { UserMenu } from '@omnitron-dev/prism/layouts';
<UserMenu
user={{ name: 'Ada Lovelace', email: 'ada@example.com', role: 'Admin' }}
menuItems={[
{ key: 'profile', label: 'Profile', onClick: goToProfile },
{ key: 'logout', label: 'Sign out', destructive: true, onClick: signOut },
]}
showEmail
/>| Prop | Type | Default |
user | UserMenuUser ({ name; email?; avatarUrl?; role? }) | — (required) |
menuItems | UserMenuItem[] | — |
header / footer | ReactNode | — |
avatarSize | 'small' | 'medium' | 'large' | 'medium' |
showEmail | boolean | — |
showRole | boolean | — |
sx / popoverSx | SxProps<Theme> | — |
onOpen / onClose | () => void | — |
Each UserMenuItem is { key, label, icon?, onClick?, destructive?, disabled?, dividerAfter? }.
Mixing layouts in one app
Layouts are just route-element wrappers — react-router 7 handles the rest:
<Routes>
{/* Auth flow */}
<Route element={<GuestGuard><AuthCenteredLayout logo={<Logo />}><Outlet /></AuthCenteredLayout></GuestGuard>}>
<Route path="/sign-in" element={<SignInForm />} />
<Route path="/sign-up" element={<SignUpForm />} />
</Route>
{/* Authenticated app */}
<Route element={<AuthGuard><ConsoleShell /></AuthGuard>}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/apps" element={<AppsPage />} />
</Route>
{/* Error pages */}
<Route path="*" element={<AuthSimpleLayout logo={<Logo />} compact><NotFound /></AuthSimpleLayout>} />
</Routes>(ConsoleShell is the DashboardLayout wrapper shown at the top of
this page; it renders <Outlet /> for the matched child routes.)
See also
Blocks — composites that often sit inside layouts
Components catalog — components used in custom layouts
Theme — density / spacing tokens