Tinyrack

AppShell

A responsive application frame with a static desktop sidebar and modal mobile navigation.

Contract

Main owns the page's single main landmark. Sidebar is sticky and independently scrollable above the selected breakpoint. sidebarMode switches it between the expanded width and an icon rail, while SidebarLabel keeps every navigation name accessible in both states. Below the breakpoint, mobileSidebar="drawer" preserves the Base UI TRDrawer behavior and mobileSidebar="rail" opts into an inline fixed rail.

Install

pnpm add @tinyrack/ui

Scroll inside the code area to read long lines.

Styles

@import '@tinyrack/ui/components/app-shell.css';

Scroll inside the code area to read long lines.

Imports

import { TRAppShell } from '@tinyrack/ui/components/app-shell';

Scroll inside the code area to read long lines.

Playground

Usage

Responsive shell permalink

Switch layout, breakpoint, drawer state, sidebar mode, mobile behavior, and control appearance. Reset restores the complete controlled preview.

Examples

Responsive navigation patterns permalink

Three production-minded patterns compare the 48rem and 64rem drawer breakpoints with a persistent mobile rail. Resize the page to see each shell adapt in place.

Control appearances permalink

Compare solid, outline, and ghost shell controls without mixing in layout decisions.

API

Root accepts breakpoint (sm or lg), layout (header-first or sidebar-first), and mobileSidebar (drawer or rail). Drawer state supports open, defaultOpen, and onOpenChange; sidebar width supports sidebarMode, defaultSidebarMode, and onSidebarModeChange. Use portalContainer for a contained preview or embedded application and drawerPopupClassName for popup styling.

Header renders the header landmark. Sidebar renders an independently scrollable aside and supplies its accessible name to the mobile dialog. It owns its own inline size, so a width change — expanding, collapsing, or switching to the rail — is animated; pass collapsed to animate it away, which also makes it inert immediately and hides it once the transition ends. Do not wrap it in a fixed-width box. Trigger opens the mobile drawer, Close dismisses it, and SidebarToggle switches expanded and rail modes while retaining focus and exposing aria-expanded. SidebarLabel is visually hidden rather than removed in rail mode. Main renders the page's main landmark and supports Base UI's render contract.

The stable styling surface includes --tr-app-shell-background, --tr-app-shell-sidebar-background, --tr-app-shell-sidebar-width, --tr-app-shell-sidebar-rail-width, --tr-app-shell-header-block-size, and --tr-app-shell-drawer-block-size. Root state is reflected by data-breakpoint, data-layout, data-mobile-sidebar, data-page-scroll, and data-sidebar-mode; the sidebar carries data-collapsed and data-sidebar-mode. Motion follows prefers-reduced-motion.

Documentation chrome is a built-in mode of the same shell. Set chrome to docs, splash, or standalone (the default is app) to opt into route progress and scroll restoration, then compose the Brand, Actions, and Outline parts. In this mode Root also accepts currentPath, hash, locationKey, navigationKind, and pendingPath; when pendingPath differs from currentPath the shell shows a top progress bar and marks content aria-busy. Main accepts scroll to wrap its content in the page content region wired to shell scroll restoration, with viewportLabel and contentClassName. Root state adds data-chrome.

Page scroll

Root accepts pageScroll to choose which element scrolls the page.

container, the default, keeps the shell inside a viewport-height frame, and Main scroll renders a scroll panel that owns the page scroll position. Use it when the shell is embedded in a fixed-height box, such as a preview or a screenshot mock.

document hands scrolling to the document itself, leaving only the sidebar and the outline as independent scrollers. This is the right choice for a full-page site: a page that clips its own scrolling cannot be panned once it is pinch-zoomed on a phone, so document is what keeps zoom usable.

<TRAppShell.Root chrome="docs" pageScroll="document">

Two consequences are worth planning for. Under pageScroll="document" the host page's own stylesheet must not set overflow: hidden or height: 100% on html or body, because the shell cannot undo a clip it does not own, and --tr-app-shell-block-size no longer applies. Main also has no nested scroll region in this posture, so viewportLabel is ignored while contentClassName and the pending aria-busy signal still apply.

If the header is taller than the default control height, set --tr-app-shell-header-block-size. It is the single source of truth for the header row, the sticky offset of the sidebar, and the sticky offset of the outline, so all three stay aligned.

For SSR, the server renders the desktop/static structure and hydrates without reading browser globals during module evaluation. The client then applies the selected media query; avoid deriving different child markup from window in application render code. pageScroll is a static prop rather than a media query result, so the scroll posture is already correct in the prerendered HTML and does not change during hydration. The modal drawer traps focus, closes on Escape or backdrop activation, restores focus to Trigger, and prevents page scrolling while open.