apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
AppShell
Variant↳ other

Layout

AppShell

Canonical product layout primitive. Composes four slots (header / sidebar / aside / footer) plus main content into a CSS-Grid-driven shell with two layout variants, responsive sidebar collapse to a Drawer on mobile, optional desktop rail-collapse, RTL-aware logical positioning, a skip-to-content link, and a

AppShell

<AppShell /> is the canonical product-layout primitive — the outermost frame for any logged-in dashboard, admin console, docs site, or mail-style app. It composes four named slots (header, sidebar, aside, footer) plus main content into a CSS-Grid-driven shell with two layout variants, responsive sidebar collapse to a <Drawer> on mobile, optional desktop rail-collapse, RTL-aware logical positioning, a skip-to-content link, and a useAppShell() context hook for header components.

  • Slot props, not subparts: header / sidebar / aside / footer are passed as props, so any component (yours, the DS's, a third party's) drops in.
  • Two layout variants — default (Notion / VS Code: sidebar full height) and inset (GitHub / Linear: header full width).
  • Responsive by default — below sidebarBreakpoint (md), the sidebar moves into a <Drawer>; the header's hamburger button toggles it via the context hook.
  • Rail-collapse — sidebarCollapsed swaps the sidebar between full and rail widths with a CSS transition.
  • Skip-to-content link, focusable on Tab, lands focus directly inside <main>.

Overview — header, sidebar, footer dashboard

Loading preview…
Overview.tsx

Anatomy

tsx
import { AppShell, useAppShell } from 'apx-ds';

function Hamburger() {
  const { toggleSidebar } = useAppShell();
  return <Button onClick={toggleSidebar} aria-label="Toggle menu">☰</Button>;
}

<AppShell
  header={<TopBar />}
  sidebar={<SidebarNav />}
  footer={<Footer />}
>
  <PageContent />
</AppShell>
import { AppShell, useAppShell } from 'apx-ds';

function Hamburger() {
  const { toggleSidebar } = useAppShell();
  return <Button onClick={toggleSidebar} aria-label="Toggle menu">☰</Button>;
}

<AppShell
  header={<TopBar />}
  sidebar={<SidebarNav />}
  footer={<Footer />}
>
  <PageContent />
</AppShell>

API

PropValuesDefault
layoutdefault · insetdefault
headerReactNode—
sidebarReactNode—
asideReactNode—
footerReactNode—
sidebarPositionstart · end (logical, RTL-aware)start
sidebarWidthnumber (px)260
sidebarCollapsedWidthnumber (px)64
sidebarCollapsedboolean (controlled)—
defaultSidebarCollapsedbooleanfalse
sidebarBreakpointsm · md · lg · xlmd
sidebarMobileOpenboolean (controlled)—
defaultSidebarMobileOpenbooleanfalse
sidebarLabelstring"Primary navigation"
headerHeightnumber (px)56
headerStickybooleantrue
headerVariantdefault · bordered · floatingdefault
headerOffsetnumber (px reserved above the grid)0
asidePositionstart · endend
asideWidthnumber (px)320
asideOpenboolean (controlled)—
defaultAsideOpenbooleantrue
asideLabelstring"Details"
main{ padding, maxWidth, centered }{ padding: 6, maxWidth: 'full', centered: true }
skipToContentbooleantrue
skipToContentLabelstring"Skip to content"

main.padding accepts 0 \| 2 \| 4 \| 6 \| 8 \| 10 \| 12. main.maxWidth accepts full \| sm \| md \| lg \| xl \| 2xl \| 7xl. Both are Tailwind-spacing-scale literal keys so the JIT scanner picks every variant up at build time.

useAppShell() hook

Header / sidebar children call this hook to inspect or mutate shell state:

FieldTypeNotes
layoutAppShellLayoutCurrent layout variant
sidebarPositionstart | endResolved sidebar logical position
isMobilebooleanWhether the viewport is below sidebarBreakpoint
isSidebarCollapsedbooleanDesktop rail-collapse state
isSidebarOpenbooleanMobile drawer open state
isAsideOpenbooleanAside open state
toggleSidebar()() => voidSmart: flips drawer on mobile, rail on desktop
collapseSidebar()() => voidForce desktop rail-collapse
expandSidebar()() => voidForce desktop rail-expand
openSidebar()() => voidOpen the mobile drawer
closeSidebar()() => voidClose the mobile drawer
toggleAside()() => voidFlip aside open state
openAside()() => voidOpen the aside
closeAside()() => voidClose the aside

toggleSidebar() is the function header hamburger buttons should call — it picks the right behavior automatically based on viewport size.

Layout variants

VariantHeader positionUsed by
defaultHeader occupies the main column only; sidebar extends full heightNotion, Mantine, VS Code
insetHeader spans every column; sidebar starts belowGitHub, Linear, Vercel

Responsive behavior

  • The sidebarBreakpoint prop (default md = 768px) determines when the inline sidebar collapses into a <Drawer>. Above the breakpoint, the sidebar sits in the grid; below, it's removed from the grid and rendered into a drawer that the header hamburger opens.
  • useAppShell().isMobile reflects this state — header components can show / hide a hamburger button accordingly.
  • useAppShell().toggleSidebar() does the right thing on either side of the breakpoint: on mobile it toggles the drawer, on desktop it toggles the rail.

Examples

Basic — header + sidebar + footer dashboard

Loading preview…
BasicLayout.tsx

Inset — header spans every column

Loading preview…
InsetLayout.tsx

Collapsible — rail collapse via useAppShell()

Loading preview…
CollapsibleSidebar.tsx

With aside — controlled detail panel

Loading preview…
WithAside.tsx

Sidebar end — logical-end side panel

Loading preview…
SidebarEnd.tsx

Floating header — detached chrome with shadow

Loading preview…
FloatingHeader.tsx

Centered main — docs-site readability

Loading preview…
CenteredMain.tsx

Minimal — main only

Loading preview…
MinimalNoChrome.tsx

Accessibility

  • The <main> element is the only true landmark; it carries tabIndex={-1} so the skip-to-content link can focus it directly without making it focusable to keyboard navigation in normal use.
  • The header is wrapped in <header role="banner">.
  • The sidebar is wrapped in <aside aria-label={sidebarLabel}> ("Primary navigation" by default — override if the sidebar is something other than navigation).
  • The aside slot is wrapped in <aside aria-label={asideLabel}> ("Details" by default).
  • The footer is wrapped in <footer role="contentinfo">.
  • The skip-to-content link is sr-only until focused, then becomes a visible pill at the top-left of the viewport. Pressing Enter focuses and scrolls to <main>.
  • axe-core: zero violations across default / inset / sidebar-end / collapsed configurations.

RTL

  • sidebarPosition and asidePosition are logical (start / end), not physical (left / right). Under dir="rtl", start becomes the right side.
  • The sidebar / aside inner borders use border-inline-start / border-inline-end so the visible separation always lands between the panel and main content, regardless of writing direction.

Theming

AppShell reads from theme.components.AppShell.styleOverrides.{root, header, sidebar, aside, main, footer, skipLink}. Every slot shares the same component key so a single theme block targets the whole family. Consumer className always wins via tailwind-merge.

Do / Don't

  • Do pass slot props (header={...}) — they're easier to reason about than nested children and the grid template is computed from "which slots are populated".
  • Do use useAppShell() inside your header / sidebar to read isMobile and call toggleSidebar() — that's the canonical hamburger button pattern.
  • Do set main.maxWidth on docs / marketing surfaces to keep prose readable.
  • Don't nest one AppShell inside another. Use a <Stack> / <Grid> for sub-layouts.
  • Don't override <main>'s padding via className — use the main.padding prop so the recipe variant stays consistent and theme-overridable.
  • Don't add your own skip-link — AppShell ships one. Set skipToContent={false} if you truly need to suppress it.

Props

PropTypeDefaultDescription
asideReactNode—Detail / inspector slot, opposite of sidebar by default.
asideLabelstring'Details'Accessible label for the aside landmark.
asideOpenboolean—Controlled aside open state.
asidePositionenum'end'Aside logical side.
asideWidthnumber320Aside width in px when open.
childrenReactNode—Main content. Wrapped in a `<main>` landmark.
classNamestring——
defaultAsideOpenbooleantrueUncontrolled initial aside open state.
defaultSidebarCollapsedbooleanfalseUncontrolled initial rail-collapse state.
defaultSidebarMobileOpenbooleanfalseUncontrolled initial mobile drawer state.
footerReactNode—Bottom footer slot.
headerReactNode—Top header slot. Pure layout — AppShell doesn't introspect the node.
headerHeightnumber56Header height in px.
headerOffsetnumber0Additional top offset for OS toolbars (Electron / Tauri) in px.
headerStickybooleantrueWhether the header is `position: sticky`.
headerVariantenum'default'—
layoutenum'default'—
mainAppShellMainConfig—Inner padding / max-width / centering for the main content.
onAsideOpenChange(open: boolean) => void—Notified whenever the aside open state changes.
onSidebarCollapsedChange(collapsed: boolean) => void—Notified whenever the rail-collapse state changes.
onSidebarMobileOpenChange(open: boolean) => void—Notified whenever the mobile drawer open state changes.
refRef<HTMLDivElement>—Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
sidebarReactNode—Primary navigation slot. Falls into a `<Drawer>` below `sidebarBreakpoint`.
sidebarBreakpointenum'md'Breakpoint below which the sidebar becomes a drawer.
sidebarCollapsedboolean—Controlled rail-collapse on desktop.
sidebarCollapsedWidthnumber64Sidebar width in px when rail-collapsed.
sidebarLabelstring'Primary navigation'Accessible label for the sidebar landmark.
sidebarMobileOpenboolean—Controlled mobile drawer open state.
sidebarPositionenum'start'Sidebar logical side.
sidebarWidthnumber260Sidebar width in px when expanded.
skipToContentbooleantrueRender the skip-to-content link at the top of the shell. When focused, it scrolls to and focuses the `<main>` landmark.
skipToContentLabelstring'Skip to content'Visible text for the skip-to-content link.
styleCSSProperties——
sxSx——