Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Layout

App Shell

Layout

App Shell React component for Tailwind CSS v4

AccordionCard
Live props
Appearance
variant
State
defaultCollapsed
defaultMobileOpen
Behavior
collapsible
persistKey
resizable
edgeSwipe
shortcut
Layout
defaultSidebarWidth256px
mobileBreakpoint
mobileDrawerSize
Content
skipLinkLabel
mainId
mainAs
Code
import { AppShell } from "@/components/ui/app-shell"

<AppShell />
  1. Home
  2. Components
  3. Layout
  4. App Shell
Pro

App Shell React component for Tailwind CSS v4

v1.1.0·Updated in 1.11.0

A React app shell for admin layouts: a collapsible, resizable sidebar, a top bar and the page, with a drawer and a tab bar on phones.

On desktop the sidebar sits beside the page: press Mod+B (Cmd on a Mac, Ctrl elsewhere), the top bar's panel button or the sidebar's collapse button to fold it into a 64px icon rail (or, with collapsible="offcanvas", to slide it out completely); the width animates and the state is remembered in a cookie. With resizable, drag the sidebar's edge between 200 and 400px (the width follows the pointer and is saved on release), drag it below 120px to snap to the rail, double-click it to reset, or focus it with Tab and use Left / Right (16px), Home (rail), End (widest) and Enter (fold / unfold). Below the breakpoint the sidebar becomes a drawer: the top bar's menu button or Mod+B opens it, it closes on navigation, on Escape, on a tap on the dimmed page or by swiping it back, and focus returns to the menu button; with edgeSwipe a swipe from the start edge opens it too. The bottom tab bar shows on phones only. Tab once from the top of the page to reveal "Skip to content", which jumps straight to the page.

App Shell is a Pro React component in the React layout components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add app-shell, together with Sidebar, Topbar, Bottom Tab Bar, Sheet, Navigation and 7 more.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add app-shell

App Shell is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import AppShell from @/components/ui/app-shell.

import type { ReactNode } from "react"
import Link from "next/link"
import { cookies } from "next/headers"
import { usePathname, useRouter } from "next/navigation"
import { FileText, House, Inbox, LayoutDashboard, Settings, Users } from "lucide-react"
import { AppShell } from "@/components/ui/app-shell"
import { Sidebar, type SidebarNavGroup } from "@/components/ui/sidebar"
import { Topbar } from "@/components/ui/topbar"
import { persistedCookieName } from "@/hooks/use-persisted-state"
import { LinkProvider, type NavItem } from "@/lib/navigation"

// app/providers.tsx ("use client"): every NavLink inside renders through next/link
export function Providers({ children }: { children: ReactNode }) {
  const router = useRouter()
  return (
    <LinkProvider component={Link} currentPath={usePathname()} navigate={router.push}>
      {children}
    </LinkProvider>
  )
}

const nav: SidebarNavGroup[] = [
  {
    items: [
      { label: "Home", href: "/", icon: <LayoutDashboard /> },
      { label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
    ],
  },
  {
    label: "Sales",
    items: [
      { label: "Clients", href: "/clients", icon: <Users /> },
      { label: "Invoices", href: "/invoices", icon: <FileText /> },
    ],
  },
  { label: "Account", items: [{ label: "Settings", href: "/settings", icon: <Settings /> }] },
]

const tabs: NavItem[] = [
  { label: "Home", href: "/", icon: <House /> },
  { label: "Clients", href: "/clients", icon: <Users /> },
  { label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
  { label: "Settings", href: "/settings", icon: <Settings /> },
]

// app/(app)/layout.tsx, a server component: the cookie renders the right sidebar on the first paint
export default async function AppLayout({ children }: { children: ReactNode }) {
  const stored = (await cookies()).get(persistedCookieName("app-shell-collapsed"))?.value
  return (
    <AppShell
      defaultCollapsed={stored === "true"}
      sidebar={<Sidebar groups={nav} filter />}
      topbar={<Topbar title="Summit CRM" />}
      tabs={tabs}
      labels={{ openMenu: "Open menu", closeMenu: "Close menu", collapse: "Collapse", expand: "Expand", navigation: "Main" }}
      skipLinkLabel="Skip to content"
    >
      {children}
    </AppShell>
  )
}

Props

  • sidebar
    ReactNode

    A Sidebar. The shell renders it in the desktop column (it drives its collapsed state, width and variant) and, on phones, a second copy with mode="sheet" inside the drawer, mounted only while the drawer is open. A wrapper component must forward its props (mode, variant, collapsible, width, className, style) to the Sidebar.

  • nav
    SidebarNavGroup[]

    A shortcut for small apps: renders <Sidebar groups={nav} /> when sidebar is not given.

  • topbar
    ReactNode

    A Topbar, sticky at the top of the content column. It renders the menu button (sidebar toggle on desktop, hamburger on phones) and gets aria-controls and the shortcut hint from the shell unless you set menuControls / menuShortcut (pass the Topbar element itself, not a wrapper component, for that). For your own header use AppShellTrigger (see Parts).

  • tabs
    ReactNode | NavItem[]

    Phones only: a BottomTabBar, or just its items. Hidden from mobileBreakpoint up. Fixed to the viewport with window scroll (the page is padded by --bottom-bar-offset), a static row under the page with content scroll.

  • banner
    ReactNode

    A full-width strip above everything, e.g. an Alert with radius="none" for a maintenance notice.

  • children
    ReactNode

    The page, rendered in <main id={mainId}>. The shell never animates it on navigation. Components inside it read the shell state with useAppShell (see Parts).

  • variantdefault "sidebar"
    "sidebar" | "inset" | "floating"

    sidebar: flush columns with a hairline. inset: the page sits in a raised rounded panel on a tinted shell (Linear, Vercel). floating: the sidebar is a raised panel 8px off the edges. Sets the sidebar's variant unless it has its own.

  • defaultCollapseddefault false
    boolean

    Starts with the rail (or the sidebar hidden, with offcanvas). A stored cookie wins after hydration; read it on the server (persistedCookieName(`${persistKey}-collapsed`)) and pass it here so the first paint is right.

  • collapsed
    boolean

    Controlled collapsed state of the desktop sidebar. Pair it with onCollapsedChange.

  • onCollapsedChange
    (collapsed: boolean) => void

    Every change: the shortcut, the top bar button, the sidebar's collapse button, a drag to the rail.

  • collapsibledefault "icon"
    "icon" | "offcanvas" | "none"

    icon: the sidebar folds into a 64px rail with tooltips. offcanvas: it slides out completely and the page takes the width. none: always expanded (the desktop toggle disappears; phones keep the drawer).

  • persistKeydefault "app-shell"
    string

    Remembers collapsed and the width in the cookies `${persistKey}-collapsed` and `${persistKey}-width` (a server layout can read them). "" keeps both in memory.

  • defaultSidebarWidthdefault 256
    number

    The expanded sidebar width in px, and what a double-click on the resize handle resets to.

  • sidebarWidth
    number

    Controlled width in px. Pair it with onSidebarWidthChange.

  • onSidebarWidthChange
    (width: number) => void

    Fires once per change (pointer up, a key press, a reset), never per frame.

  • resizabledefault false
    boolean

    A handle on the sidebar's end edge: drag between 200 and 400px, below 120px it snaps to the rail, double-click resets. It is a keyboard-operable separator (arrows ±16px, Home, End, Enter).

  • mobileBreakpointdefault "md"
    "md" | "lg"

    Below it the sidebar becomes a drawer and the tabs show: md (768px) or lg (1024px, tablets get the phone layout). Layout switches with CSS, so the server HTML is right.

  • defaultMobileOpendefault false
    boolean

    Starts with the phone drawer open (it only shows below the breakpoint).

  • mobileOpen
    boolean

    Controlled drawer state. Pair it with onMobileOpenChange.

  • onMobileOpenChange
    (open: boolean) => void

    The drawer opened or closed (menu button, shortcut, edge swipe, navigation, Escape, a tap outside, a swipe back).

  • mobileDrawerSizedefault "sm"
    "sm" | "md"

    The drawer's width: 320 or 400px, always leaving a 48px sliver of the page to tap.

  • edgeSwipedefault false
    boolean

    Phones: a 16px strip on the start edge opens the drawer after a 40px drag or a flick. Turn it on in installed PWAs; in Safari that edge is the browser's back gesture.

  • scrolldefault "window"
    "window" | "content"

    window: the document scrolls (router scroll restoration, the iOS toolbar minimizing; the right default for apps). content: the content column scrolls on its own and is provided to the bars as their scroll container (embedded apps, iframes, this demo). Docs-only: the playground always runs in content mode inside its frame.

  • heightdefault "screen"
    "screen" | "full"

    screen: the viewport height (dvh). full: fills the parent, which needs a height. Docs-only: the playground frame always uses full.

  • shortcutdefault "mod+b"
    string

    Toggles the sidebar: the rail on desktop, the drawer on phones. Shown in the top bar button's tooltip. "" binds nothing.

  • skipLinkLabeldefault "Skip to content"
    string

    The skip link, the first focusable element: hidden until tabbed to, then it focuses the page.

  • mainIddefault "main"
    string

    The id of <main>, the skip link's target.

  • mainAsdefault "main"
    "main" | "div"

    The content column's element. Keep main for the app's own shell; use div when the shell renders inside a page that already has its <main> (a docs preview, an embedded demo).

  • container
    HTMLElement | null

    Where the drawer is portaled (default document.body).

  • labels
    { openMenu?: string; closeMenu?: string; collapse?: string; expand?: string; resize?: string; collapsed?: string; navigation?: string }

    English defaults ("Open menu", "Close menu", "Collapse sidebar", "Expand sidebar", "Resize sidebar", "Collapsed", "Navigation"). The first four are shared with the top bar's menu button through the shell context; resize names the resize handle, collapsed is its value text while the sidebar is a rail, navigation is the drawer's hidden title.

  • classNames
    Partial<Record<"sidebar" | "body" | "main" | "topbar" | "tabs" | "drawer" | "resize", string>>

    Extra classes for the parts: the desktop sidebar column, the content column (the raised panel in inset), <main>, the top bar, the tabs row, the drawer body and the resize handle.

  • className
    string

    Extra classes for the root.

Parts

  • useAppShell

    hook

    The shell state for anything inside it: isMobile, collapsed / setCollapsed, mobileOpen / setMobileOpen, toggleSidebar, hasSidebar, scroll, labels, mainRef, sidebarWidth / setSidebarWidth, shortcut, and sidebarId / drawerId for aria-controls. Throws outside an AppShell.

  • AppShellTrigger

    component

    The menu button for your own header: the sidebar toggle with a tooltip on desktop, a hamburger that turns into an X on phones, nothing when the shell has no sidebar. Takes the Button props plus kind, shortcut, tooltip and labels; aria-controls and the shortcut come from the shell.

Examples

Inset

variant="inset": the page sits in a raised rounded panel on a tinted shell, and the top bar glass matches the panel.

Floating, off-canvas

variant="floating" + collapsible="offcanvas": the panel button in the top bar slides the whole sidebar out and the page takes the width.

Resizable, remembered

Drag the sidebar's edge (or focus it and use the arrows); below 120px it snaps to the rail, double-click resets. The width survives a reload.

No sidebar: top bar and tabs

A mobile-first app: links in the top bar on wide screens, a bottom tab bar on phones. No menu button, no drawer, no shortcut.

Maintenance banner

banner: a full-width strip above the shell (an Alert with banner), here with the sidebar starting as a rail.

On a phone: drawer and tabs

The same app at 390px with the drawer open: tap the page or swipe the drawer back, and it closes on navigation.

Dependencies

sidebartopbarbottom-tab-barsheetnavigationuse-controllable-stateuse-media-queryuse-persisted-stateuse-scroll-stateuse-shortcutmotionui-config

Files

  • components/ui/app-shell.tsx

FAQ

Is the React App Shell component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install App Shell?

Run npx wingo-ui@latest add app-shell. It also adds the Wingo UI items it builds on (Sidebar, Topbar, Bottom Tab Bar, Sheet, Navigation and 7 more). With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/app-shell.

How does App Shell behave on phones?

The bottom tab bar shows on phones only.

See also

SidebarTopbarBottom Tab BarSettings LayoutNavigationlib

Used in

Admin Shell

More React layout components

AccordionCollapsibleDetail LayoutEntity HeaderPage HeaderScroll Area
All 12 React layout components

Articles using this component

  • Nextjs PWA Without next-pwa: Offline Page and App Shell

    Mobile UI in React · Nov 5, 2026 · 12 min read

  • Shadcn Sidebar Mobile: Fix the Sheet and Add a Tab Bar

    Mobile UI in React · Nov 5, 2026 · 9 min read

  • Best React Component Library for Dashboard Apps in 2026

    Comparisons and alternatives · Oct 16, 2026 · 13 min read

  • 100vh Mobile Fix: When to Use dvh, svh or lvh in Tailwind

    Mobile UI in React · Oct 9, 2026 · 11 min read

PreviousAccordionNext Card
Skip to content
  • Home
  • Inbox4
Sales
  • Clients
  • Quotes12
  • Invoices
Operations
  • Products
  • Orders
  • Reports
Account
  • Settings
  • Help
HomeHome
SS

Good morning, Anna

Here is how September looks for North Studio LLC.

New invoice

Revenue in September

$184,230.00

+12% vs August

Overdue invoices

3 · $24,180.00

2 due in 7 days

New clients

18

+5 vs August

Open quotes

12

worth $96,400.00

Recent activity

  • Michael Hayes issued invoice #2026-0412 to Northwind Tech Inc.

    today, 10:24 AM
  • Jessica Dunn sent quote #Q-2026-118 to Summit Logistics

    today, 9:10 AM
  • Ellen Stone added the client Green House Realty

    yesterday, 5:42 PM
  • Ryan Martin scheduled a demo with Keystone Construction

    yesterday, 11:05 AM
  • Maria Thomas marked #2026-0409 as paid

    Sep 22, 2026

This preview uses height="full" scroll="content" so the app scrolls inside the frame; on a site keep the defaults (the window scrolls). Mod+J opens the palette here, the showcase keeps Mod+K.

collapsed
false
drawer
closed
width
256px
toggle
CtrlBControl B
breakpoint
md
drawer size
320px
edge swipe
off
cookies
app-shell-collapsed, app-shell-width
  • Home
  • Clients
  • 4Inbox
  • Profile