Layout
App Shell
The frame of an application, with a top bar, a side rail and one region that scrolls.
Layout
The frame of an application, with a top bar, a side rail and one region that scrolls.
import * as React from 'react'
import {
AppShell,
CommandMenu,
DropdownMenuItem,
IconRail,
MobileNav,
PageContainer,
PageHeader,
PageSection,
ThemeMenu,
TopBar,
TopBarLogo,
TopBarSearch,
TopBarSegment,
TopBarSeparator,
TopBarUserMenu,
type NavGroup,
} from 'ferry-ui'
import { CreditCard, FolderKanban, LayoutDashboard, Settings, Users } from 'lucide-react'
const PAGES = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard /> },
{ id: 'projects', label: 'Projects', icon: <FolderKanban /> },
{ id: 'members', label: 'Members', icon: <Users /> },
{ id: 'billing', label: 'Billing', icon: <CreditCard /> },
{ id: 'settings', label: 'Settings', icon: <Settings /> },
]
export default function AppShellBasic() {
// A real app reads the current page from its router. This demo keeps it in state.
const [page, setPage] = React.useState('overview')
const [navOpen, setNavOpen] = React.useState(false)
const [commandOpen, setCommandOpen] = React.useState(false)
const current = PAGES.find((entry) => entry.id === page) ?? PAGES[0]
// One definition of the navigation for the rail, the phone drawer and the command menu.
const groups: NavGroup[] = [
{
id: 'main',
items: PAGES.map((entry) => ({ ...entry, active: entry.id === page, onSelect: () => setPage(entry.id) })),
},
]
return (
<>
<AppShell
scrollKey={page}
mobileNavOpen={navOpen}
onMobileNavOpenChange={setNavOpen}
onCommandShortcut={() => setCommandOpen((open) => !open)}
topBar={
<TopBar
onOpenMobileNav={() => setNavOpen(true)}
logo={
<TopBarLogo label="Acme home">
<svg viewBox="0 0 20 20" fill="currentColor" className="text-brand">
<circle cx="10" cy="10" r="8" />
</svg>
</TopBarLogo>
}
actions={
<>
<TopBarSearch onClick={() => setCommandOpen(true)} />
<ThemeMenu />
<TopBarUserMenu name="Maya Chen" description="maya@example.com">
<DropdownMenuItem>Sign out</DropdownMenuItem>
</TopBarUserMenu>
</>
}
>
<TopBarSeparator />
<TopBarSegment chevron={false}>Acme</TopBarSegment>
</TopBar>
}
rail={<IconRail groups={groups} />}
mobileNav={<MobileNav groups={groups} title="Acme" showThemeToggle />}
>
<PageContainer>
<PageHeader title={current?.label} description="The page scrolls. The top bar and the rail stay in place." />
<PageSection title="Content">
<div className="h-[40rem] rounded-lg border border-dashed border-border-stronger bg-surface-75" />
</PageSection>
</PageContainer>
</AppShell>
<CommandMenu open={commandOpen} onOpenChange={setCommandOpen} groups={groups} />
</>
)
}Use the buttons above the preview to see the frame on a tablet and on a phone.
AppShell at the root of the signed-in part of the app. Do not nest shells.NavGroup[] to the rail, the phone drawer and the command menu.AppShell has four slots. Each slot takes a component.
import { AppShell, IconRail, MobileNav, TopBar } from 'ferry-ui'
<AppShell topBar={<TopBar />} rail={<IconRail />} mobileNav={<MobileNav />}>
{/* the page */}
</AppShell>| Slot | Content | Component |
|---|---|---|
topBar | The bar above the rail and the page. | Top Bar |
rail | The side navigation. It shows from 768px. | Icon Rail |
mobileNav | The navigation drawer for phones. | Mobile Nav |
children | The page. It is the only region that scrolls. | Page |
AppShell holds the open state of the phone drawer. A MobileNav in the shell follows this state with no prop.
To open the drawer from the top bar, control the state. Pass mobileNavOpen and onMobileNavOpenChange.
const [navOpen, setNavOpen] = React.useState(false)
<AppShell
mobileNavOpen={navOpen}
onMobileNavOpenChange={setNavOpen}
topBar={<TopBar onOpenMobileNav={() => setNavOpen(true)} />}
mobileNav={<MobileNav groups={groups} />}
/>onCommandShortcut runs when the user presses ⌘ K or Ctrl K. Use it to open a Command Menu.
const [commandOpen, setCommandOpen] = React.useState(false)
<AppShell onCommandShortcut={() => setCommandOpen((open) => !open)} />
<CommandMenu open={commandOpen} onOpenChange={setCommandOpen} groups={groups} />Pass the current path to scrollKey. When the value changes, the page region scrolls back to the top.
<AppShell scrollKey={pathname} />skipLinkLabel sets the text of this link. Pass a translation in an app that is not in English.<main> element. mainId sets its id.AppShell also accepts each attribute of the <div> element.
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
The page content, rendered in the scrollable | ||
rail | ReactNode | - |
Desktop side navigation, usually an | ||
topBar | ReactNode | - |
Full-width bar above the rail and the main region, usually a 48px | ||
mobileNav | ReactNode | - |
Phone navigation drawer, usually a | ||
mobileNavOpen | boolean | - |
Controlled open state of the mobile navigation drawer. Pair with | ||
defaultMobileNavOpen | boolean | false |
Initial open state of the mobile navigation drawer when uncontrolled. Defaults to | ||
onMobileNavOpenChange | ((open: boolean) => void) | - |
Called when the mobile navigation drawer opens or closes. | ||
onCommandShortcut | (() => void) | - |
Called on ⌘K / Ctrl+K anywhere in the app, typically to toggle a command palette. Omit it to leave the shortcut unbound. See | ||
scrollKey | string | number | - |
When this value changes, the main region scrolls back to the top. Pass the current pathname so each new page starts at the top, as full page loads do. | ||
mainId | string | - |
Id of the | ||
mainClassName | string | - |
Extra classes for the | ||
skipLinkLabel | string | Skip to content |
Text of the keyboard-only "skip to content" link. Defaults to | ||
useAppShell() reads the state of the nearest shell. Outside a shell, it returns null.
| Value | Type | Role |
|---|---|---|
mobileNavOpen | boolean | Tells if the phone drawer is open. |
setMobileNavOpen | (open: boolean) => void | Opens or closes the phone drawer. |
mainId | string | The id of the page region. |