import { AppShell } from '@uhg-abyss/web/ui/AppShell';AppShell is the layout primitive for a persistent application frame: a header, navigation rail, and footer that stay fixed in place while AppShell.Main scrolls independently. It handles the responsive behavior between regions — like collapsing a rail into a mobile drawer — so pages don't need to hand-roll fixed-position layout CSS. See it in action in the full page example.
Basic layout
Use the root AppShell props to configure shell dimensions. AppShell.Main reads the active section sizes through internal CSS variables and applies the correct offsets automatically.
<AppShell header={{ height: 64 }} sidebar={{ width: { sm: 220, lg: 260 }, breakpoint: 'sm' }} footer={{ height: 56 }} padding={{ base: 16, sm: 24 }}> <AppShell.Header>{/* fixed header content */}</AppShell.Header> <AppShell.Sidebar>{/* fixed or drawer navigation */}</AppShell.Sidebar> <AppShell.Main>{/* page content */}</AppShell.Main> <AppShell.Footer>{/* fixed footer content */}</AppShell.Footer></AppShell>Set spacing with the padding prop on AppShell, not by styling AppShell.Main directly. padding is read by every region to calculate its offset from AppShell.Main. Styling AppShell.Main on its own bypasses that calculation and the regions will not account for the extra space.
Layout modes
Use the layout prop to control how the header and footer stack relative to the rails.
Default layout
The default layout keeps the header and footer full-width while the sidebar and aside fill the remaining height between them.
<AppShell layout="default" header={{ height: 64 }} sidebar={{ width: 240, breakpoint: 'sm' }} footer={{ height: 56 }}> {/* shell regions */}</AppShell>Alt layout
Use layout="alt" when the sidebar or aside needs to span the full viewport height. Header and footer become inset automatically.
<AppShell layout="alt" header={{ height: 64 }} sidebar={{ width: 240, breakpoint: 'sm' }} aside={{ width: 200, breakpoint: 'lg' }} footer={{ height: 56 }}> {/* shell regions */}</AppShell>Sidebar and aside collapse
Use collapsed.desktop to collapse the desktop rail, and collapsed.mobile to close the mobile drawer.
const [mobileOpen, setMobileOpen] = useState(false);
// ...
<AppShell sidebar={{ width: 240, breakpoint: 'sm', collapsed: { desktop: false, mobile: !mobileOpen, }, }}> <AppShell.Sidebar onClose={() => setMobileOpen(false)}> {/* navigation */} </AppShell.Sidebar> <AppShell.Main>{/* content */}</AppShell.Main></AppShell>;Below the configured breakpoint, AppShell.Sidebar and AppShell.Aside render their content inside the existing Drawer component, controlled by collapsed.mobile.
Breakpoints
The breakpoint value on sidebar and aside props, as well as the keys of any responsive size object (e.g. width, height, and padding), accept the same breakpoint names as your theme: xs, sm, md, and lg. These resolve against theme.breakpoints, so overriding breakpoints with createTheme also changes where AppShell switches modes.
base is the one key that is not a breakpoint. It is the unconditional value applied before any breakpoint override, i.e., the mobile-first default. Every other key (xs, sm, md, lg) layers an override on top of base starting at that breakpoint's width.
<AppShell sidebar={{ // 240px by default, 288px once the viewport reaches the `lg` breakpoint width: { base: 240, lg: 288 }, // switches to the mobile drawer below the `md` breakpoint breakpoint: 'md', }}> {/* shell regions */}</AppShell>Choose the breakpoint to match where your actual header/sidebar/aside content changes shape (for example, where a navigation row is added or removed), not an arbitrary value. AppShell does not measure content, so a height or width that does not match your real content at a given breakpoint will overlap adjacent regions.
Mobile drawer toggle
The mobile drawer's open state is a plain controlled prop (collapsed.mobile), not something AppShell manages internally .
const [sidebarOpen, setSidebarOpen] = useState(false);
// ...
<AppShell sidebar={{ width: 240, breakpoint: 'sm', collapsed: { mobile: !sidebarOpen }, }}> <AppShell.Header> <Button onClick={() => setSidebarOpen(true)}>Menu</Button> </AppShell.Header> <AppShell.Sidebar onClose={() => setSidebarOpen(false)}> {/* navigation */} </AppShell.Sidebar> <AppShell.Main>{/* content */}</AppShell.Main></AppShell>;AppShell.Section
Use AppShell.Section to create fixed and growable regions inside AppShell.Sidebar or AppShell.Aside.
<AppShell.Sidebar> <AppShell.Section> <BrandLogo /> </AppShell.Section>
<AppShell.Section grow component={ScrollArea}> <NavigationLinks /> </AppShell.Section>
<AppShell.Section> <UserProfile /> </AppShell.Section></AppShell.Sidebar>The grow props makes a section fill the remaining space among its sibling sections. This is useful for a scrollable middle section between a fixed header and footer inside the rail, as in the example above.
The component prop replaces AppShell.Section's own root element with the component you pass rather than rendering it as a child. This matters specifically for grow sections that need to scroll. AppShell.Section supplies the flex/min-height: 0 sizing that lets it correctly shrink to the rail's available space, and component puts that sizing directly on the swapped-in component's own root instead of leaving it to size itself as a nested child. This is why component={ScrollArea} above scrolls correctly; ScrollArea doesn't size itself to its container by default, so nesting it as a plain child would not fill the growing section.
Borders and shadows
Use withBorder on AppShell to add a divider border to all regions by default. Override per region by passing withBorder directly on AppShell.Header, AppShell.Footer, AppShell.Sidebar, or AppShell.Aside.
// All regions get a border<AppShell withBorder header={{ height: 64 }} sidebar={{ width: 240, breakpoint: 'sm' }}> {/* shell regions */}</AppShell>
// Only the aside gets a border; sidebar gets no border<AppShell header={{ height: 64 }} sidebar={{ width: 240, breakpoint: 'sm' }}> <AppShell.Sidebar withBorder={false}>{/* navigation */}</AppShell.Sidebar> <AppShell.Aside withBorder>{/* context panel */}</AppShell.Aside> <AppShell.Main>{/* content */}</AppShell.Main></AppShell>Use withBoxShadow on AppShell.Sidebar or AppShell.Aside when a desktop rail should cast a directional shadow. Both rails default to no box shadow.
<AppShell.Aside withBoxShadow>{/* context panel */}</AppShell.Aside>Z-index
All regions share a zIndex value from AppShell by default. Override it per region by passing zIndex directly on AppShell.Header, AppShell.Footer, AppShell.Sidebar, or AppShell.Aside.
// All regions share the same z-index<AppShell zIndex={200}>{/* shell regions */}</AppShell>
// Sidebar and Aside stack above Header<AppShell> <AppShell.Header>{/* header content */}</AppShell.Header> <AppShell.Sidebar zIndex={300}>{/* navigation */}</AppShell.Sidebar> <AppShell.Aside zIndex={300}>{/* context panel */}</AppShell.Aside></AppShell>Disabling the shell
Use the disabled prop to strip all fixed positioning and offsets from the shell. This is useful for embedded views, print layouts, or pages that should not have a persistent frame.
<AppShell disabled header={{ height: 64 }} sidebar={{ width: 240, breakpoint: 'sm' }}> {/* Header, Sidebar, and Footer are hidden; Main renders inline */}</AppShell>Offset control
By default, both the header and footer contribute to the top and bottom offset of AppShell.Main. Pass offset: false to a region config to exclude it from the offset calculation without hiding it. This is useful when a region overlaps the page intentionally.
<AppShell header={{ height: 64, offset: false }} footer={{ height: 56 }} sidebar={{ width: 240, breakpoint: 'sm' }}> {/* AppShell.Main top padding does not include the header height */}</AppShell>CSS variables
AppShell computes region sizes into CSS custom properties on .abyss-app-shell-root. These are an internal implementation detail rather than a documented public API, but referencing them can be useful for advanced consumer styling that needs to line up with the active shell dimensions.
| Variable | Description |
|---|---|
--abyss-app-shell-header-height | Header height |
--abyss-app-shell-header-offset | Header's contribution to AppShell.Main's top offset |
--abyss-app-shell-footer-height | Footer height |
--abyss-app-shell-footer-offset | Footer's contribution to AppShell.Main's bottom offset |
--abyss-app-shell-sidebar-width | Sidebar width |
--abyss-app-shell-sidebar-offset | Sidebar's contribution to AppShell.Main's left offset |
--abyss-app-shell-aside-width | Aside width |
--abyss-app-shell-aside-offset | Aside's contribution to AppShell.Main's right offset |
Transition customization
Use transitionDuration (a number in milliseconds) and transitionTimingFunction to control how smoothly the shell animates between open and collapsed states.
<AppShell transitionDuration={300} transitionTimingFunction="ease-in-out" sidebar={{ width: 240, breakpoint: 'sm' }}> {/* shell regions */}</AppShell>Full example
See a complete application layout with AppShell orchestrating the header, sidebar, aside, main content, and footer regions. The full page example demonstrates mobile drawer navigation, rail toggles, borders, shadows, and layout switching.
AppShell Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
aside | AppShellRailConfiguration | undefined | - | - | Aside configuration. |
children | React.ReactNode | undefined | - | - | AppShell content. |
disabled | boolean | undefined | false | - | Hides all shell sections except AppShell.Main. |
footer | AppShellOffsetConfiguration | undefined | - | - | Footer configuration. |
header | AppShellOffsetConfiguration | undefined | - | - | Header configuration. |
layout | 'default' | 'alt' | undefined | 'default' | - | Controls how the shell sections stack. |
padding | number | string | AppShellResponsiveValue | undefined | 0 | - | Padding applied to AppShell.Main. |
sidebar | AppShellRailConfiguration | undefined | - | - | Sidebar configuration. |
transitionDuration | number | undefined | 150 | - | Transition duration in milliseconds. |
transitionTimingFunction | string | undefined | 'cubic-bezier(0.4, 0, 0.2, 1)' | - | Transition timing function. |
withBorder | boolean | undefined | true | - | Removes borders from all shell sections when false. |
zIndex | number | undefined | 99 | - | z-index applied to fixed shell sections. |
Below are the link(s) to the relevant GitHub type files:
AppShell.Header Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
asElement | string | undefined | 'header' | - | Overrides the rendered root element. Useful when nesting a component that already provides its own semantic landmark (e.g. Header.Container) to avoid duplicate/nested landmarks. |
children | React.ReactNode | undefined | - | - | Header content. |
withBorder | boolean | undefined | - | - | Overrides the root withBorder value for this section. |
zIndex | number | undefined | - | - | Overrides the root z-index value for this section. |
Below are the link(s) to the relevant GitHub type files:
AppShell.Sidebar Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
asElement | string | undefined | 'nav' | - | Overrides the rendered root element for the fixed rail. |
children | React.ReactNode | undefined | - | - | Sidebar content. |
onClose | () => void | undefined | - | - | Called when the mobile drawer requests to close. |
withBorder | boolean | undefined | - | - | Overrides the root withBorder value for this section. |
withBoxShadow | boolean | undefined | false | - | Applies the rail box shadow on desktop fixed mode. |
zIndex | number | undefined | - | - | Overrides the root z-index value for this section. |
Below are the link(s) to the relevant GitHub type files:
AppShell.Aside Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
asElement | string | undefined | 'aside' | - | Overrides the rendered root element for the fixed rail. |
children | React.ReactNode | undefined | - | - | Aside content. |
onClose | () => void | undefined | - | - | Called when the mobile drawer requests to close. |
withBorder | boolean | undefined | - | - | Overrides the root withBorder value for this section. |
withBoxShadow | boolean | undefined | false | - | Applies the rail box shadow on desktop fixed mode. |
zIndex | number | undefined | - | - | Overrides the root z-index value for this section. |
Below are the link(s) to the relevant GitHub type files:
AppShell.Footer Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
asElement | string | undefined | 'footer' | - | Overrides the rendered root element. Useful when nesting a component that already provides its own semantic landmark (e.g. Footer) to avoid duplicate/nested landmarks. |
children | React.ReactNode | undefined | - | - | Footer content. |
withBorder | boolean | undefined | - | - | Overrides the root withBorder value for this section. |
zIndex | number | undefined | - | - | Overrides the root z-index value for this section. |
Below are the link(s) to the relevant GitHub type files:
AppShell.Main Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
asElement | string | undefined | 'main' | - | Overrides the rendered root element. |
children | React.ReactNode | undefined | - | - | Main content. |
Below are the link(s) to the relevant GitHub type files:
AppShell.Section Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
children | React.ReactNode | undefined | - | - | Section content. |
component | React.ElementType | undefined | - | - | Optional custom component, such as ScrollArea. |
grow | boolean | undefined | false | - | Grows to fill the remaining Sidebar or Aside height. |
Below are the link(s) to the relevant GitHub type files:
AppShell Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-root | AppShell root element |
AppShell.Header Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-header | AppShell header element |
AppShell.Sidebar Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-sidebar | AppShell sidebar element |
| .abyss-app-shell-sidebar-drawer | AppShell sidebar drawer wrapper |
AppShell.Aside Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-aside | AppShell aside element |
| .abyss-app-shell-aside-drawer | AppShell aside drawer wrapper |
AppShell.Footer Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-footer | AppShell footer element |
AppShell.Main Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-main | AppShell main element |
AppShell.Section Classes
| Class Name | Description |
|---|---|
| .abyss-app-shell-section | AppShell section element |
AppShell is a layout utility and does not add its own keyboard model. Interactive children inside each region keep their native keyboard behavior.
Keyboard interactions
| Key | Description |
|---|---|
| Tab | Moves focus through interactive elements rendered inside the shell regions. |
| Shift + Tab | Moves focus to the previous interactive element rendered inside the shell regions. |
| Esc | Closes mobile sidebar or aside drawers when the consumer provides the close handler required by Drawer. |
Semantic HTML
AppShell.Headerrenders aheaderAppShell.Sidebarrenders anavAppShell.Asiderenders anasideAppShell.Footerrenders afooterAppShell.Mainrenders amain
Each region renders its element unconditionally, regardless of what you place inside it. Nesting a component that provides the same landmark produces a duplicate/nested landmark, which most screen readers surface as confusing, separate regions rather than one.
Do not render another main element inside AppShell.Main — only one main is allowed per page. The same applies to the other regions: don't nest another header inside AppShell.Header, nav inside AppShell.Sidebar, aside inside AppShell.Aside, or footer inside AppShell.Footer.
If you're composing a component that already provides its own landmark — for example the standalone Header or Footer components — override the region's element with asElement="div" so only one landmark exists:
<AppShell.Header asElement="div"> {/* Header.Container renders its own <header role="banner">, so AppShell.Header steps aside and renders a plain <div> instead */} <Header.Container>{/* ... */}</Header.Container></AppShell.Header>Component tokens
Click on a token row to copy the token to your clipboard.
| Token Name | Value | |
|---|---|---|
| app-shell.color.border.separator | #E5E5E6 | |
| app-shell.color.surface.content | #FFFFFF | |
| app-shell.color.surface.rail | #FFFFFF | |
| app-shell.border-width.separator | 1px | |
| app-shell.box-shadow.aside | -6px 0px 8px -2px rgba(0,0,0,0.16) | |
| app-shell.box-shadow.sidebar | 6px 0px 8px -2px rgba(0,0,0,0.16) |