Skip to content

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 variantsheader-first, sidebar-first, and top-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 via useCoreAppShell()

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.footer

The 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 exports

Source: packages/ui/src/components/core-app-shell/


API Reference

CoreAppShellConfig

The top-level configuration object that controls the entire layout:

tsx
interface CoreAppShellConfig {
  variant: LayoutVariant;
  dimensions?: CoreAppShellDimensions;
  features?: CoreAppShellFeatures;
}
PropertyTypeRequiredDescription
variantLayoutVariantDetermines the structural layout mode
dimensionsCoreAppShellDimensionsOverride default pixel dimensions
featuresCoreAppShellFeaturesToggle optional layout regions and behaviors

Layout Variants

tsx
type LayoutVariant = 'header-first' | 'sidebar-first' | 'top-nav';
VariantMantine layoutVisual Description
header-firstdefaultHeader 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-firstaltSidebar 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-navdefaultHeader-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

tsx
interface CoreAppShellFeatures {
  desktopCollapseVariant?: DesktopCollapseVariant;
  withUtilityBar?: boolean;
  withAside?: boolean;
  withFooter?: boolean;
  withDoubleSidebar?: boolean;
  persistState?: boolean;
  zIndex?: number;
  disabled?: boolean;
}
PropertyTypeDefaultDescription
desktopCollapseVariant'hide' | 'mini''hide'hide: Sidebar slides out completely (collapsed width = 0). mini: Sidebar shrinks to sidebarMiniWidth showing only icons.
withUtilityBarbooleanAuto-detectedShow the utility bar above the header. If omitted, the bar renders when a utilityBar slot is provided. Set explicitly to false to suppress.
withAsidebooleanAuto-detectedShow the right-hand aside panel. Same auto-detection logic as withUtilityBar.
withFooterbooleanAuto-detectedShow the bottom footer. Same auto-detection logic.
withDoubleSidebarbooleanfalseEnable the Rail + Panel double sidebar mode. When true, the navbar renders sidebarRail and sidebarPanel slots instead of the single sidebar slot.
persistStatebooleantrue (implied)Persist sidebar variant (expanded/mini/hidden) to localStorage via useLocalStorage. Set to false for demos or ephemeral layouts.
zIndexnumber200Base z-index passed to Mantine's AppShell.
disabledbooleanfalseDisables 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

tsx
interface CoreAppShellDimensions {
  utilityBarHeight?: number | string;
  headerHeight?: number | string;
  sidebarWidth?: number | string;
  sidebarMiniWidth?: number | string;
  sidebarRailWidth?: number | string;
  asideWidth?: number | string;
}
PropertyTypeDefaultDescription
utilityBarHeightnumber | string32Height of the utility bar strip above the header
headerHeightnumber | string60Height of the main header
sidebarWidthnumber | string260Width of the expanded sidebar
sidebarMiniWidthnumber | string80Width of the sidebar in mini collapse mode
sidebarRailWidthnumber | string54Width of the icon rail in double-sidebar mode
asideWidthnumber | string260Width 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:

tsx
interface CoreAppShellSlots {
  utilityBar?: ReactNode;
  header?: ReactNode;
  sidebar?: ReactNode;
  sidebarMobile?: ReactNode;
  sidebarRail?: ReactNode;
  sidebarPanel?: ReactNode;
  aside?: ReactNode;
  footer?: ReactNode;
}
SlotLocationNotes
utilityBarAbove the header, hidden on mobile (display: none below sm)Typically used for environment banners, announcements, or top-level links.
headerMain application headerMust contain its own <Burger> for mobile toggle (use useCoreAppShell() context).
sidebarDesktop navbar body (single-sidebar mode)Ignored when withDoubleSidebar is true — use sidebarRail + sidebarPanel instead.
sidebarMobileMobile drawer contentFalls back to sidebar if not provided. Use this to render a simplified mobile-specific navigation.
sidebarRailNarrow icon rail (double-sidebar mode)Only rendered when withDoubleSidebar is true. Separated from sidebarPanel by a 1px border.
sidebarPanelContextual panel beside the rail (double-sidebar mode)Collapsible via toggleNavbarPanel(). Only rendered when withDoubleSidebar is true and navbarPanelOpened is true.
asideRight-hand panelCollapsible via toggleAside(). Only rendered when withAside is enabled.
footerBottom application footerIn 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:

tsx
import { useCoreAppShell } from '@repo/ui/components';
Property / MethodTypeDescription
mobileOpenedbooleanWhether the mobile drawer is currently open
desktopOpenedbooleanWhether the desktop sidebar is expanded (only applies when desktopCollapseVariant is 'hide')
sidebarVariantSidebarVariantCurrent sidebar mode: 'expanded' | 'mini' | 'hidden'
asideOpenedbooleanWhether the aside panel is currently visible
navbarPanelOpenedbooleanWhether the secondary panel in double-sidebar mode is expanded
configCoreAppShellConfigRead-only access to the current layout configuration
toggleMobile()() => voidToggle the mobile drawer open/closed
toggleDesktop()() => voidToggle the desktop sidebar open/closed
toggleAside()() => voidToggle the aside panel visibility
toggleNavbarPanel()() => voidToggle the double-sidebar panel open/closed
setSidebarVariant()(variant: SidebarVariant) => voidProgrammatically 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:

tsx
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:

tsx
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:

tsx
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:

tsx
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.

tsx
import { CorePageContainer } from '@repo/ui/components';

Props

tsx
interface CorePageContainerProps extends ContainerProps {
  headerSlot?: ReactNode;
  children: ReactNode;
  stickyHeader?: boolean;
}
PropTypeDefaultDescription
headerSlotReactNodePage-level header content (title, breadcrumbs, action buttons). Rendered above the main content with a bottom border.
stickyHeaderbooleanfalseWhen true, the page header sticks to the top of the scroll area, offset by the AppShell header height via var(--app-shell-header-offset).
pxMantineSpacing'md'Horizontal padding for both the header and content areas
pyMantineSpacing'md'Vertical padding for both the header and content areas
...restContainerPropsAll other Mantine Container props are forwarded to the content region

Usage

tsx
<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.

In header-first mode, the footer is inset between the sidebar and aside using CSS custom properties:

css
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

Elementheader-firstsidebar-first
AppShell (base)200 (default)200 (default)
Navbar105100
Aside105100
Footer100100

The elevated 105 z-index for navbar/aside in header-first mode ensures they render above the footer, which is positioned at 100.

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                     → sidebarWidth

On mobile and xs breakpoints, the width is always 100% and sidebarWidth respectively, regardless of variant.