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
  • Marquee
  • Parallax
  • Reveal
  • Section
  • SectionHeading
  • Sidebar
  • Stack
  • Surface
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
66 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

Header scroll state

condense — the header becomes legible once content is behind it

Loading preview…
HeaderScroll.tsx
tsx
<AppShell headerScroll="condense" header={<Brand />}>…</AppShell>
<AppShell headerScroll="condense" header={<Brand />}>…</AppShell>

A pinned header that never changes on scroll reads as unfinished on a marketing page: it starts transparent over a hero, and once real content is behind it, it has to become a surface. condense gives it a border, a translucent background and a blur past the fold.

The listener lives here rather than in each template. Three templates writing it means three thresholds, three requestAnimationFrame loops, and three chances to forget the reduced-motion path.

valuebehaviour
'none'Inert. The default — an app shell's header is chrome, and changing it on scroll is a landing-page idiom.
'condense'Border, background and blur once past the fold. The house style for templates.
'reveal'As condense, plus hides on scroll down and returns on scroll up.

reveal is available, and is not the house style

It removes the nav at the exact moment a fast-scrolling user is reaching for it, and it takes away the fixed orientation anchor some users rely on to know where they are. Fine as an option someone opts into; wrong as a default.

Its thresholds are asymmetric on purpose — 96px of committed downward scroll to hide, 8px upward to return. Hiding should need commitment; showing should feel instant, because scrolling up usually is the request for the nav. Travel is measured from where the current run of scrolling began rather than from the previous frame, so a trackpad's jitter or a momentum tail can't flip it.

Reduced motion

condense still runs. It's a state change that happens to be animated, and suppressing it would leave the header transparent over content it no longer sits on — a legibility bug, not a courtesy. What a reduced-motion user loses is the transition's smoothness, not the state.

reveal does not run, and degrades to condense. Chrome that moves itself off-screen is precisely the unrequested motion the preference is about, and a header that stays put is strictly more usable.

Styling your own content off the state

The header carries data-scrolled and data-hidden, so a logo that shrinks or a CTA that appears is your CSS hanging off the DS's listener — not a second listener next to it:

tsx
<Typography className="text-lg group-data-[scrolled]:text-base">Northwind</Typography>
<Typography className="text-lg group-data-[scrolled]:text-base">Northwind</Typography>

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.
headerReduceMotionboolean—Force the reduced-motion behaviour on (`true`) or off (`false`), bypassing the media query. For tests and side-by-side documentation.
headerScrollenum'none'How the header reacts to page scroll. - `'none'` — inert. The default, because an app shell's header is chrome, not a marketing surface; changing it on scroll is a landing-page idiom. - `'condense'` — gains a border, a solid background and a blur once the page is past the fold. This is the house style for templates: a header pinned transparently over a hero has to become legible once real content is behind it. - `'reveal'` — additionally hides on scroll down and returns on scroll up. `'reveal'` is offered but is deliberately **not** the house style. It removes the nav at the exact moment a fast-scrolling user is reaching for it, and it takes away the fixed orientation anchor that some users rely on to know where they are. Fine as an option, wrong as a default. It is also suppressed entirely under `prefers-reduced-motion`, where it degrades to `'condense'`. The header carries `data-scrolled` / `data-hidden` attributes in either mode, so a consumer can hang their own styles off the state without re-implementing the listener.
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——