Agents (llms.txt)

App Shell

A composable application shell with sidebar, header, main, and aside

Dashboard
Main content
import { AppShell } from '@/components/app-shell';

export const meta = {
  layout: 'fullscreen',
} as const;

export default function AppShellPreview() {
  return (
    <div className="h-full w-full overflow-hidden">
      <AppShell>
        <AppShell.Sidebar>
          <div className="p-4 text-foreground-secondary text-sm">Sidebar</div>
        </AppShell.Sidebar>
        <AppShell.Main>
          <AppShell.Header>
            <span className="font-medium">Dashboard</span>
          </AppShell.Header>
          <AppShell.Content>
            <div className="p-6 text-foreground-secondary text-sm">
              Main content
            </div>
          </AppShell.Content>
        </AppShell.Main>
      </AppShell>
    </div>
  );
}

Dependencies

Source Code

'use client';

import { createContext, use, useState } from 'react';

import { Slot } from '@/components/slot';
import { cn } from '@/lib/utils/classnames';

interface AppShellContextValue {
  sidebarCollapsed: boolean;
  setSidebarCollapsed: (collapsed: boolean) => void;
  mobileOpen: boolean;
  setMobileOpen: (open: boolean) => void;
}

const AppShellContext = createContext<AppShellContextValue | null>(null);

const useAppShell = () => {
  const ctx = use(AppShellContext);
  if (!ctx) throw new Error('AppShell components must be inside <AppShell>');
  return ctx;
};

interface AppShellProps extends React.ComponentPropsWithRef<'div'> {
  defaultSidebarCollapsed?: boolean;
  asChild?: boolean;
}

const AppShell = ({
  ref,
  className,
  defaultSidebarCollapsed = false,
  asChild,
  ...rest
}: AppShellProps) => {
  const [sidebarCollapsed, setSidebarCollapsed] = useState(
    defaultSidebarCollapsed
  );
  const [mobileOpen, setMobileOpen] = useState(false);
  const Comp = asChild ? Slot : 'div';

  return (
    <AppShellContext
      value={{
        sidebarCollapsed,
        setSidebarCollapsed,
        mobileOpen,
        setMobileOpen,
      }}
    >
      <Comp
        ref={ref}
        data-app-shell
        data-sidebar-collapsed={sidebarCollapsed ? '' : undefined}
        className={cn(
          'relative flex h-svh w-full overflow-hidden   text-foreground',
          className
        )}
        {...rest}
      />
    </AppShellContext>
  );
};

interface AppShellSidebarProps extends React.ComponentPropsWithRef<'aside'> {
  /** Width when expanded. Default 16rem. */
  width?: string;
  /** Width when collapsed. Default 3.5rem. */
  collapsedWidth?: string;
}

const AppShellSidebar = ({
  ref,
  className,
  width = '16rem',
  collapsedWidth = '3.5rem',
  style,
  ...rest
}: AppShellSidebarProps) => {
  const { sidebarCollapsed, mobileOpen, setMobileOpen } = useAppShell();

  return (
    <>
      {/* Mobile backdrop */}
      {mobileOpen && (
        <button
          type="button"
          aria-label="Close sidebar"
          onClick={() => setMobileOpen(false)}
          className="fixed inset-0 z-40  md:hidden"
        />
      )}
      <aside
        ref={ref}
        data-app-shell-sidebar
        style={
          {
            '--sidebar-w': width,
            '--sidebar-w-collapsed': collapsedWidth,
            ...style,
          } as React.CSSProperties
        }
        className={cn(
          'fixed inset-y-0 left-0 z-50 flex shrink-0 flex-col border-border border-r bg-background',
          'transition-[width,transform] duration-(--duration-hover) ease-(--ease)',
          'w-(--sidebar-w)',
          mobileOpen ? 'translate-x-0' : '-translate-x-full',
          'md:static md:translate-x-0',
          // collapsed width — driven by the data attribute on the AppShell root
          sidebarCollapsed && 'md:w-(--sidebar-w-collapsed)',
          className
        )}
        {...rest}
      />
    </>
  );
};

const AppShellHeader = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'header'>) => (
  <header
    ref={ref}
    data-app-shell-header
    className={cn(
      'flex h-14 shrink-0 items-center gap-3 border-border border-b bg-background px-4 md:h-16 md:px-6',
      className
    )}
    {...rest}
  />
);

interface AppShellMainProps extends React.ComponentPropsWithRef<'main'> {
  /**
   * Renders the main region as a floating, rounded card inset from the page
   * edges (shadcn-style "inset" sidebar layout). The page background shows
   * through around all sides.
   */
  inset?: boolean;
  /** Override className for the inset card wrapper (only used when `inset`). */
  insetClassName?: string;
}

const AppShellMain = ({
  ref,
  className,
  inset,
  insetClassName,
  children,
  ...rest
}: AppShellMainProps) => (
  <main
    ref={ref}
    data-app-shell-main
    data-inset={inset ? '' : undefined}
    className={cn(
      'flex min-w-0 flex-1 flex-col overflow-hidden',
      inset && 'p-2 md:p-3',
      className
    )}
    {...rest}
  >
    {inset ? (
      <div
        className={cn(
          'flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-border/50 bg-surface-default shadow-sm',
          insetClassName
        )}
      >
        {children}
      </div>
    ) : (
      children
    )}
  </main>
);

const AppShellContent = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'div'>) => (
  <div
    ref={ref}
    data-app-shell-content
    className={cn('min-h-0 flex-1 overflow-auto ', className)}
    {...rest}
  />
);

const AppShellAside = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'aside'>) => (
  <aside
    ref={ref}
    data-app-shell-aside
    className={cn(
      'hidden w-80 shrink-0 overflow-auto border-border border-l  lg:block',
      className
    )}
    {...rest}
  />
);

const CompoundAppShell = Object.assign(AppShell, {
  Sidebar: AppShellSidebar,
  Header: AppShellHeader,
  Main: AppShellMain,
  Content: AppShellContent,
  Aside: AppShellAside,
});

export type { AppShellMainProps, AppShellProps, AppShellSidebarProps };
export { CompoundAppShell as AppShell, useAppShell };

AppShell is the top-level layout for productivity apps and dashboards. It composes a collapsible sidebar, a sticky header, the main content region, and an optional right-hand aside. On small screens the sidebar slides in over a backdrop.

Anatomy


          <AppShell>
  <AppShell.Sidebar>{nav}</AppShell.Sidebar>
  <AppShell.Main>
    <AppShell.Header>{topbar}</AppShell.Header>
    <AppShell.Content>{page}</AppShell.Content>
  </AppShell.Main>
  <AppShell.Aside>{panel}</AppShell.Aside>
</AppShell>
        

API Reference

AppShell

Extends the div element.

Prop Default Type Description
defaultSidebarCollapsed false boolean
asChild false boolean Render the shell root as the provided child element instead of a `<div>` — e.g. a framework `<Layout>` wrapper.

asChild

The shell root is a plain <div> so it can host any framework wrapper. Pass asChild to merge the shell’s layout styles and context onto a consumer-supplied element (for example, a Next.js layout wrapper or a motion.div) without forking the component.


          <AppShell asChild>
  <div data-page="dashboard">
    <AppShell.Sidebar>{nav}</AppShell.Sidebar>
    <AppShell.Main>
      <AppShell.Header>{topbar}</AppShell.Header>
      <AppShell.Content>{page}</AppShell.Content>
    </AppShell.Main>
  </div>
</AppShell>
        

AppShell.Sidebar

Extends the aside element.

Prop Default Type
width "16rem" string
collapsedWidth "3.5rem" string

AppShell.Header

Extends the header element.

AppShell.Main

Extends the main element.

Prop Default Type
inset false boolean

When inset is set, the main region renders as a floating, rounded card with an outer margin — the shadcn-style “inset sidebar” layout. Pair with a transparent, borderless <AppShell.Sidebar> so the page tint shows through behind it.

AppShell.Content

Extends the div element. Scrollable region inside Main.

AppShell.Aside

Extends the aside element. Hidden below lg.

Examples

Default

Dashboard
Main content
import { AppShell } from '@/components/app-shell';

export const meta = {
  layout: 'fullscreen',
} as const;

export default function AppShellPreview() {
  return (
    <div className="h-full w-full overflow-hidden">
      <AppShell>
        <AppShell.Sidebar>
          <div className="p-4 text-foreground-secondary text-sm">Sidebar</div>
        </AppShell.Sidebar>
        <AppShell.Main>
          <AppShell.Header>
            <span className="font-medium">Dashboard</span>
          </AppShell.Header>
          <AppShell.Content>
            <div className="p-6 text-foreground-secondary text-sm">
              Main content
            </div>
          </AppShell.Content>
        </AppShell.Main>
      </AppShell>
    </div>
  );
}

Inset (floating dashboard)

Overview

Dashboard

Track activity and performance across your workspace.

Active users
12,480
+8.2%
Revenue
$48.2k
vs $44.1k last month
Churn
1.4%
−0.3%
Recent activity
Maya shipped v1.4.02m ago
Rahul opened 12 issues18m ago
Sofia merged 3 PRs1h ago
import {
  Home01,
  Inbox01,
  LineChartUp01,
  Plus,
  Settings01,
  TrendUp01,
  Users01,
} from '@untitledui-pro/icons/solid';

import { AppShell } from '@/components/app-shell';
import { Badge } from '@/components/badge';
import { Button } from '@/components/button';
import { Container } from '@/components/container';
import { Grid } from '@/components/grid';
import { Nav } from '@/components/nav';
import { PageHeader } from '@/components/page-header';
import { Stat } from '@/components/stat';
import { Surface } from '@/components/surface';

export const meta = {
  layout: 'fullscreen',
} as const;

export default function AppShellInsetPreview() {
  return (
    <div className="h-full w-full overflow-hidden">
      <AppShell>
        <AppShell.Sidebar width="13rem" className="border-r-0 bg-transparent">
          <div className="flex h-14 shrink-0 items-center gap-2 px-4 font-semibold md:h-16">
            Acme
          </div>
          <Nav>
            <Nav.Group label="Workspace">
              <Nav.Item href="#" active>
                <Nav.Icon>
                  <Home01 />
                </Nav.Icon>
                <Nav.Label>Overview</Nav.Label>
              </Nav.Item>
              <Nav.Item href="#">
                <Nav.Icon>
                  <Inbox01 />
                </Nav.Icon>
                <Nav.Label>Inbox</Nav.Label>
                <Nav.Trailing>
                  <Badge size="xs" variant="neutral">
                    12
                  </Badge>
                </Nav.Trailing>
              </Nav.Item>
              <Nav.Item href="#">
                <Nav.Icon>
                  <LineChartUp01 />
                </Nav.Icon>
                <Nav.Label>Analytics</Nav.Label>
              </Nav.Item>
              <Nav.Item href="#">
                <Nav.Icon>
                  <Users01 />
                </Nav.Icon>
                <Nav.Label>Team</Nav.Label>
              </Nav.Item>
            </Nav.Group>
            <Nav.Group label="Settings">
              <Nav.Item href="#">
                <Nav.Icon>
                  <Settings01 />
                </Nav.Icon>
                <Nav.Label>Preferences</Nav.Label>
              </Nav.Item>
            </Nav.Group>
          </Nav>
        </AppShell.Sidebar>

        <AppShell.Main inset>
          <AppShell.Header>
            <span className="font-medium">Overview</span>
            <div className="ml-auto flex items-center gap-2">
              <Button variant="outline" size="sm">
                Filter
              </Button>
              <Button size="sm">
                <Plus />
                New
              </Button>
            </div>
          </AppShell.Header>
          <AppShell.Content className="bg-transparent">
            <Container className="mx-auto max-w-4xl py-8" gutter="md">
              <PageHeader>
                <PageHeader.Content>
                  <PageHeader.Title>Dashboard</PageHeader.Title>
                  <PageHeader.Description>
                    Track activity and performance across your workspace.
                  </PageHeader.Description>
                </PageHeader.Content>
              </PageHeader>

              {/* KPI tiles — elevation="raised" sit on the inset card */}
              <Grid className="mt-8">
                <Grid.Col span={4}>
                  <Surface elevation="raised">
                    <Stat>
                      <Stat.Label>Active users</Stat.Label>
                      <Stat.Value>12,480</Stat.Value>
                      <Stat.Trend direction="up">
                        <TrendUp01 /> +8.2%
                      </Stat.Trend>
                    </Stat>
                  </Surface>
                </Grid.Col>
                <Grid.Col span={4}>
                  <Surface elevation="raised">
                    <Stat>
                      <Stat.Label>Revenue</Stat.Label>
                      <Stat.Value>$48.2k</Stat.Value>
                      <Stat.Description>vs $44.1k last month</Stat.Description>
                    </Stat>
                  </Surface>
                </Grid.Col>
                <Grid.Col span={4}>
                  <Surface elevation="raised">
                    <Stat>
                      <Stat.Label>Churn</Stat.Label>
                      <Stat.Value>1.4%</Stat.Value>
                      <Stat.Trend direction="down">−0.3%</Stat.Trend>
                    </Stat>
                  </Surface>
                </Grid.Col>

                {/* Recent activity panel — raised, with overlay-elevation rows nested inside */}
                <Grid.Col span="full">
                  <Surface elevation="raised" padding="lg">
                    <div className="mb-4 flex items-center justify-between">
                      <div className="font-medium">Recent activity</div>
                      <Button size="xs" variant="ghost">
                        View all
                      </Button>
                    </div>
                    <div className="flex flex-col gap-2">
                      <Surface elevation="overlay" padding="sm">
                        <div className="flex items-center justify-between">
                          <span className="text-sm">
                            <span className="font-medium">Maya</span> shipped
                            v1.4.0
                          </span>
                          <span className="text-foreground-secondary text-xs">
                            2m ago
                          </span>
                        </div>
                      </Surface>
                      <Surface elevation="overlay" padding="sm">
                        <div className="flex items-center justify-between">
                          <span className="text-sm">
                            <span className="font-medium">Rahul</span> opened 12
                            issues
                          </span>
                          <span className="text-foreground-secondary text-xs">
                            18m ago
                          </span>
                        </div>
                      </Surface>
                      <Surface elevation="overlay" padding="sm">
                        <div className="flex items-center justify-between">
                          <span className="text-sm">
                            <span className="font-medium">Sofia</span> merged 3
                            PRs
                          </span>
                          <span className="text-foreground-secondary text-xs">
                            1h ago
                          </span>
                        </div>
                      </Surface>
                    </div>
                  </Surface>
                </Grid.Col>
              </Grid>
            </Container>
          </AppShell.Content>
        </AppShell.Main>
      </AppShell>
    </div>
  );
}

Previous

Setup

Next

Auto Grid