Primitives
App shell
BetaBeta — the props contract is close to final, but small breaking changes can still land.Persistent sidebar and content frame with one scroll policy for loading, empty, and loaded pages.
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>calc(var(--spacing) * 14)--cui-app-shell-header-padding-inline<length>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