Core App Shell — Layout Engine
Architectural Foundation: Mantine AppShell · @mantine/hooks
Description: Configuration-driven layout engine wrapping Mantine's AppShell, providing three layout variants (header-first, sidebar-first, top-nav), double sidebar support, responsive mobile drawers, and state persistence via Context API.
Package:
@repo/ui· Module Path:@repo/ui/components> Dependencies: React 18+, Mantine v8 (AppShell),@mantine/hooks
Table of Contents
Overview
The Core App Shell is a configuration-driven layout engine that wraps Mantine's AppShell component. It provides a single <CoreAppShell> component that renders enterprise-grade application frames — complete with headers, sidebars, aside panels, utility bars, and footers — controlled entirely through a declarative config object and slot-based content injection.
Key capabilities:
- Three layout variants —
header-first,sidebar-first, andtop-nav— covering the most common enterprise SaaS patterns - Double sidebar — Google-style rail + contextual panel navigation
- Smart defaults — Slots auto-detect presence; no explicit feature flags needed for basic layouts
- Responsive out-of-the-box — Mobile drawer, desktop collapse, and mini-sidebar are all built-in
- State persistence — Optional
localStorage-backed sidebar state via@mantine/hooks - Context API — All layout toggle methods (
toggleMobile,toggleDesktop,setSidebarVariant, etc.) are available to any descendant component viauseCoreAppShell()
Architecture
Composition Model
The layout engine uses a Provider → Inner composition pattern:
CoreAppShell (Public API)
└── CoreAppShellProvider (Context — state management)
└── CoreAppShellInner (Layout rendering — consumes context)
└── Mantine <AppShell> (CSS Grid engine)
├── AppShell.Header ← slots.utilityBar + slots.header
├── AppShell.Navbar ← slots.sidebar | slots.sidebarRail + slots.sidebarPanel
├── AppShell.Main ← children
├── AppShell.Aside ← slots.aside
└── AppShell.Footer ← slots.footerThe outer CoreAppShell is a thin wrapper that instantiates the provider and passes config down. The inner component subscribes to context and derives all layout calculations (navbar width, header height, collapse states) from the live config + user interactions.
File Structure
packages/ui/src/components/core-app-shell/
├── types.ts # All TypeScript interfaces and union types
├── core-app-shell-context.tsx # Context provider + useCoreAppShell hook
├── core-app-shell.tsx # Main component (Public API + Inner renderer)
├── core-page-container.tsx # Companion page-level content wrapper
└── index.ts # Barrel exportsSource: packages/ui/src/components/core-app-shell/
API Reference
CoreAppShellConfig
The top-level configuration object that controls the entire layout:
interface CoreAppShellConfig {
variant: LayoutVariant;
dimensions?: CoreAppShellDimensions;
features?: CoreAppShellFeatures;
}| Property | Type | Required | Description |
|---|---|---|---|
variant | LayoutVariant | ✅ | Determines the structural layout mode |
dimensions | CoreAppShellDimensions | — | Override default pixel dimensions |
features | CoreAppShellFeatures | — | Toggle optional layout regions and behaviors |
Layout Variants
type LayoutVariant = 'header-first' | 'sidebar-first' | 'top-nav';| Variant | Mantine layout | Visual Description |
|---|---|---|
header-first | default | Header spans the full viewport width. Sidebar and aside sit below the header, stretching to the bottom of the screen. Footer is inset between the sidebar and aside. This is the most common enterprise/dashboard pattern (e.g., Azure Portal, Jira). |
sidebar-first | alt | Sidebar spans the full viewport height. Header sits to the right of the sidebar. Produces a "desktop application" feel (e.g., VS Code, Slack). Footer spans full width beneath the sidebar. |
top-nav | default | Header-only layout with no visible desktop sidebar. The sidebar is hidden on desktop but remains accessible as a mobile drawer on small screens. Ideal for documentation sites or marketing pages. |
IMPORTANT
When variant is set to top-nav, the desktop navbar is visually hidden via collapsed.desktop: true and width 0. However, the <AppShell.Navbar> DOM element remains mounted with responsive width props so the mobile drawer continues to function. This is an intentional design choice to avoid conditional DOM removal.
Features
interface CoreAppShellFeatures {
desktopCollapseVariant?: DesktopCollapseVariant;
withUtilityBar?: boolean;
withAside?: boolean;
withFooter?: boolean;
withDoubleSidebar?: boolean;
persistState?: boolean;
zIndex?: number;
disabled?: boolean;
}| Property | Type | Default | Description |
|---|---|---|---|
desktopCollapseVariant | 'hide' | 'mini' | 'hide' | hide: Sidebar slides out completely (collapsed width = 0). mini: Sidebar shrinks to sidebarMiniWidth showing only icons. |
withUtilityBar | boolean | Auto-detected | Show the utility bar above the header. If omitted, the bar renders when a utilityBar slot is provided. Set explicitly to false to suppress. |
withAside | boolean | Auto-detected | Show the right-hand aside panel. Same auto-detection logic as withUtilityBar. |
withFooter | boolean | Auto-detected | Show the bottom footer. Same auto-detection logic. |
withDoubleSidebar | boolean | false | Enable the Rail + Panel double sidebar mode. When true, the navbar renders sidebarRail and sidebarPanel slots instead of the single sidebar slot. |
persistState | boolean | true (implied) | Persist sidebar variant (expanded/mini/hidden) to localStorage via useLocalStorage. Set to false for demos or ephemeral layouts. |
zIndex | number | 200 | Base z-index passed to Mantine's AppShell. |
disabled | boolean | false | Disables the AppShell layout entirely (renders children without structural chrome). |
TIP
Smart defaults: You rarely need to set withUtilityBar, withAside, or withFooter explicitly. The engine auto-detects presence by checking if the corresponding slot is provided and truthy. Only set them to false when you want to suppress a slot that is being passed.
Dimensions
interface CoreAppShellDimensions {
utilityBarHeight?: number | string;
headerHeight?: number | string;
sidebarWidth?: number | string;
sidebarMiniWidth?: number | string;
sidebarRailWidth?: number | string;
asideWidth?: number | string;
}| Property | Type | Default | Description |
|---|---|---|---|
utilityBarHeight | number | string | 32 | Height of the utility bar strip above the header |
headerHeight | number | string | 60 | Height of the main header |
sidebarWidth | number | string | 260 | Width of the expanded sidebar |
sidebarMiniWidth | number | string | 80 | Width of the sidebar in mini collapse mode |
sidebarRailWidth | number | string | 54 | Width of the icon rail in double-sidebar mode |
asideWidth | number | string | 260 | Width of the right-hand aside panel |
NOTE
All dimension values accept both pixel numbers (e.g., 260) and CSS strings (e.g., '20rem'). When both headerHeight and utilityBarHeight are numbers, they are summed directly. When either is a string, the engine wraps them in a calc() expression automatically.
Slots
Content is injected via the slots prop — a flat object of named ReactNode values:
interface CoreAppShellSlots {
utilityBar?: ReactNode;
header?: ReactNode;
sidebar?: ReactNode;
sidebarMobile?: ReactNode;
sidebarRail?: ReactNode;
sidebarPanel?: ReactNode;
aside?: ReactNode;
footer?: ReactNode;
}| Slot | Location | Notes |
|---|---|---|
utilityBar | Above the header, hidden on mobile (display: none below sm) | Typically used for environment banners, announcements, or top-level links. |
header | Main application header | Must contain its own <Burger> for mobile toggle (use useCoreAppShell() context). |
sidebar | Desktop navbar body (single-sidebar mode) | Ignored when withDoubleSidebar is true — use sidebarRail + sidebarPanel instead. |
sidebarMobile | Mobile drawer content | Falls back to sidebar if not provided. Use this to render a simplified mobile-specific navigation. |
sidebarRail | Narrow icon rail (double-sidebar mode) | Only rendered when withDoubleSidebar is true. Separated from sidebarPanel by a 1px border. |
sidebarPanel | Contextual panel beside the rail (double-sidebar mode) | Collapsible via toggleNavbarPanel(). Only rendered when withDoubleSidebar is true and navbarPanelOpened is true. |
aside | Right-hand panel | Collapsible via toggleAside(). Only rendered when withAside is enabled. |
footer | Bottom application footer | In header-first mode, the footer is inset between sidebar and aside. In sidebar-first mode, it spans the full width. |
Context API
The useCoreAppShell() hook provides access to all layout state and toggle methods from any descendant component:
import { useCoreAppShell } from '@repo/ui/components';| Property / Method | Type | Description |
|---|---|---|
mobileOpened | boolean | Whether the mobile drawer is currently open |
desktopOpened | boolean | Whether the desktop sidebar is expanded (only applies when desktopCollapseVariant is 'hide') |
sidebarVariant | SidebarVariant | Current sidebar mode: 'expanded' | 'mini' | 'hidden' |
asideOpened | boolean | Whether the aside panel is currently visible |
navbarPanelOpened | boolean | Whether the secondary panel in double-sidebar mode is expanded |
config | CoreAppShellConfig | Read-only access to the current layout configuration |
toggleMobile() | () => void | Toggle the mobile drawer open/closed |
toggleDesktop() | () => void | Toggle the desktop sidebar open/closed |
toggleAside() | () => void | Toggle the aside panel visibility |
toggleNavbarPanel() | () => void | Toggle the double-sidebar panel open/closed |
setSidebarVariant() | (variant: SidebarVariant) => void | Programmatically set the sidebar to 'expanded', 'mini', or 'hidden' |
WARNING
useCoreAppShell() must be called from within a <CoreAppShell> subtree. Calling it outside the provider will throw: "useCoreAppShell must be used within CoreAppShellProvider". If you need context access in the header slot, pass a component (not inline JSX) so it mounts inside the provider tree.
Usage Examples
Minimal Setup
The simplest possible layout — a header and sidebar with all defaults:
import { CoreAppShell, CoreAppShellConfig, useCoreAppShell } from '@repo/ui/components';
import { Group, Text, Box, Stack, Button, Burger } from '@repo/ui/components';
function MyHeader() {
const { mobileOpened, toggleMobile } = useCoreAppShell();
return (
<Group h="100%" px="md">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Text fw={700}>My Application</Text>
</Group>
);
}
const config: CoreAppShellConfig = {
variant: 'header-first',
};
function App() {
return (
<CoreAppShell
config={config}
slots={{
header: <MyHeader />,
sidebar: (
<Stack p="md" gap="xs">
<Button variant="subtle" fullWidth>
Dashboard
</Button>
<Button variant="subtle" fullWidth>
Settings
</Button>
</Stack>
),
}}
>
<Text>Main content area</Text>
</CoreAppShell>
);
}Header-First with Utility Bar
A full enterprise layout with utility bar, aside, and footer:
import { CoreAppShell, CoreAppShellConfig, useCoreAppShell } from '@repo/ui/components';
import { Group, Text, Box, Burger } from '@repo/ui/components';
function AppHeader() {
const { mobileOpened, toggleMobile } = useCoreAppShell();
return (
<Group h="100%" px="md" justify="space-between">
<Group>
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Text fw={700} size="lg">
Enterprise Dashboard
</Text>
</Group>
</Group>
);
}
const config: CoreAppShellConfig = {
variant: 'header-first',
features: {
desktopCollapseVariant: 'hide',
persistState: true,
},
dimensions: {
headerHeight: 60,
utilityBarHeight: 32,
sidebarWidth: 280,
asideWidth: 300,
},
};
function App() {
return (
<CoreAppShell
config={config}
slots={{
utilityBar: (
<Group h="100%" px="md" justify="flex-end">
<Text size="xs">v2.4.1 · Production</Text>
</Group>
),
header: <AppHeader />,
sidebar: <MySidebar />,
aside: <MyAside />,
footer: (
<Group h="100%" px="md">
<Text size="sm">© 2026 Acme Corp</Text>
</Group>
),
}}
>
<MyPageContent />
</CoreAppShell>
);
}Double Sidebar (Rail + Panel)
Google-style navigation with an icon rail and a collapsible contextual panel:
import { CoreAppShell, CoreAppShellConfig, useCoreAppShell } from '@repo/ui/components';
import { Stack, Box, Text, Burger, Group } from '@repo/ui/components';
import { Home, Settings, BarChart2 } from 'lucide-react';
function AppHeader() {
const { mobileOpened, toggleMobile } = useCoreAppShell();
return (
<Group h="100%" px="md">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Text fw={700}>Admin Panel</Text>
</Group>
);
}
const config: CoreAppShellConfig = {
variant: 'sidebar-first',
features: {
withDoubleSidebar: true,
},
dimensions: {
sidebarRailWidth: 54,
sidebarWidth: 260,
},
};
function App() {
return (
<CoreAppShell
config={config}
slots={{
header: <AppHeader />,
sidebarRail: (
<Stack align="center" gap="lg" pt="md">
<Home size={24} />
<BarChart2 size={24} />
<Settings size={24} />
</Stack>
),
sidebarPanel: (
<Box p="md">
<Text fw={700} mb="sm">
Navigation
</Text>
{/* Contextual links based on active rail icon */}
</Box>
),
sidebarMobile: (
<Box p="md">
<Text fw={700}>Mobile Nav</Text>
{/* Simplified mobile navigation */}
</Box>
),
}}
>
<Text>Main content</Text>
</CoreAppShell>
);
}NOTE
When withDoubleSidebar is true, the sidebar slot is ignored on desktop. The navbar renders sidebarRail (fixed-width icon column) and sidebarPanel (collapsible contextual panel) side-by-side. On mobile, sidebarMobile takes priority, falling back to sidebar if not provided.
Interactive Config Builder
The showcase demo at apps/showcase/src/pages/showcase-original/shell-demo/ demonstrates a live, interactive config builder where every feature toggle and variant switch updates the layout in real-time. The key pattern is managing config state externally and passing it as a prop:
import { useState, useMemo } from 'react';
import { CoreAppShell, CoreAppShellConfig, LayoutVariant, DesktopCollapseVariant } from '@repo/ui/components';
function ShellDemo() {
const [layoutVariant, setLayoutVariant] = useState<LayoutVariant>('header-first');
const [collapseVariant, setCollapseVariant] = useState<DesktopCollapseVariant>('hide');
const [withDoubleSidebar, setWithDoubleSidebar] = useState(false);
const config: CoreAppShellConfig = useMemo(
() => ({
variant: layoutVariant,
features: {
desktopCollapseVariant: collapseVariant,
withDoubleSidebar,
persistState: false,
},
}),
[layoutVariant, collapseVariant, withDoubleSidebar],
);
return (
<CoreAppShell config={config} slots={{ header: <MyHeader />, sidebar: <MySidebar /> }}>
{/* Config controls live here — they can use useCoreAppShell() for toggle methods */}
</CoreAppShell>
);
}CorePageContainer
A companion component for structuring page-level content within the <AppShell.Main> area. It provides a sticky page header and a contained, padded content region.
import { CorePageContainer } from '@repo/ui/components';Props
interface CorePageContainerProps extends ContainerProps {
headerSlot?: ReactNode;
children: ReactNode;
stickyHeader?: boolean;
}| Prop | Type | Default | Description |
|---|---|---|---|
headerSlot | ReactNode | — | Page-level header content (title, breadcrumbs, action buttons). Rendered above the main content with a bottom border. |
stickyHeader | boolean | false | When true, the page header sticks to the top of the scroll area, offset by the AppShell header height via var(--app-shell-header-offset). |
px | MantineSpacing | 'md' | Horizontal padding for both the header and content areas |
py | MantineSpacing | 'md' | Vertical padding for both the header and content areas |
| ...rest | ContainerProps | — | All other Mantine Container props are forwarded to the content region |
Usage
<CoreAppShell config={config} slots={slots}>
<CorePageContainer
stickyHeader
headerSlot={
<Group justify="space-between">
<Text component="h1" size="xl" fw={700}>
Users
</Text>
<Button>Add User</Button>
</Group>
}
>
<UserTable />
</CorePageContainer>
</CoreAppShell>Design Decisions & Caveats
Mobile Navbar Lifecycle
The <AppShell.Navbar> DOM element is always mounted, even when the layout variant is top-nav. The desktop content is hidden via visibleFrom="sm" and mobile content via hiddenFrom="sm". This ensures Mantine's native drawer engine works correctly on mobile without conditional DOM removal breaking the transition animations.
Footer Positioning in header-first Mode
In header-first mode, the footer is inset between the sidebar and aside using CSS custom properties:
left: var(--app-shell-navbar-offset, 0px);
right: var(--app-shell-aside-offset, 0px);In sidebar-first mode, the footer spans the full viewport width (left: 0; right: 0).
Z-Index Strategy
| Element | header-first | sidebar-first |
|---|---|---|
| AppShell (base) | 200 (default) | 200 (default) |
| Navbar | 105 | 100 |
| Aside | 105 | 100 |
| Footer | 100 | 100 |
The elevated 105 z-index for navbar/aside in header-first mode ensures they render above the footer, which is positioned at 100.
Sidebar Width Calculation
The navbar width is dynamically computed based on multiple state variables:
navbarWidth.sm =
isTopNav → 0
isDoubleSidebar + panelOpen → sidebarWidth
isDoubleSidebar + panelClosed → sidebarRailWidth
sidebarVariant === 'mini' → sidebarMiniWidth
default → sidebarWidthOn mobile and xs breakpoints, the width is always 100% and sidebarWidth respectively, regardless of variant.