App shell
BetaBeta — the props contract is close to final, but small breaking changes can still land.The application container: persistent navigation and header around changing page content, with one shared scroll policy.
The outlined area is the AppShell container. Collapse the sidebar or toggle loading: the frame stays in place while page content changes. This embedded preview uses layout="contained"; a full application uses the default viewport layout.
Composition
Persistent application frame
AppShell includes SidebarProvider. Set scroll once: page uses the document, inset lets PageLayout scroll, and none lets workspace panes own scrolling. Keep the shell mounted while route content loads.
- <AppShell>
- <Sidebar>
- <SidebarContent />
- <SidebarRail />
- <AppShellContent>
- <AppShellHeader>
- <SidebarTrigger />
- <PageLayout>
- <PageHeader>
- <PageTitle />
- <PageBody>
- route content
- <PageHeader>
- <AppShellHeader>
- <Sidebar>
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/app-shell.tsx.
npx shadcn@latest add https://control-ui.dev/r/app-shell.jsonDependencies
Raw code
Primary installed source
"use client";import type { ComponentProps, CSSProperties } from "react";import type { AppShellHeaderKnobStyle } from "@/components/control-ui/knob-contracts/app-shell-header-knobs";import { cn } from "@/components/control-ui/lib/cn";import { type PageScroll, PageScrollContext, usePageScroll } from "@/components/control-ui/ui/page-layout";import { SidebarInset, type SidebarInsetProps, SidebarProvider, type SidebarProviderProps } from "@/components/control-ui/ui/sidebar";export type AppShellProps = SidebarProviderProps & { scroll?: PageScroll };export function AppShell({ scroll = "auto", layout = "viewport", className, ...props }: AppShellProps) { const resolvedScroll = usePageScroll(scroll); return ( <PageScrollContext.Provider value={resolvedScroll}> <SidebarProvider data-app-shell="" data-scroll={resolvedScroll} layout={layout} className={cn(layout === "viewport" && (resolvedScroll === "page" ? "min-h-dvh" : "h-dvh min-h-0 overflow-hidden"), className)} {...props} /> </PageScrollContext.Provider> );}export function AppShellContent({ className, ...props }: SidebarInsetProps) { const scroll = usePageScroll(); return <SidebarInset data-app-shell-content="" className={cn("min-h-0", scroll !== "page" && "overflow-hidden", className)} {...props} />;}export type AppShellHeaderProps = Omit<ComponentProps<"header">, "style"> & { style?: CSSProperties & AppShellHeaderKnobStyle;};export function AppShellHeader({ className, ...props }: AppShellHeaderProps) { return ( <header data-control-ui="app-shell" data-control-family="app-shell-header" data-slot="root" className={cn("sticky top-0 z-30 flex shrink-0 items-center gap-2", className)} {...props} /> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-app-shell-header-* · 5 knobsHow the cascade resolves--cui-app-shell-header-height<length-percentage>calc(var(--spacing) * 14)--cui-app-shell-header-padding-inline<length-percentage>calc(var(--spacing) * 4)--cui-app-shell-header-background<color>var(--background)--cui-app-shell-header-border-color<color>var(--border)--cui-app-shell-header-border-width<length>1px