Layout
Inner Menu
A side menu for the sections of one area, such as the settings of an app.
Layout
A side menu for the sections of one area, such as the settings of an app.
import * as React from 'react'
import { InnerMenu, PageContainer, PageHeader, type NavGroup } from 'ferry-ui'
import { BellRing, CreditCard, KeyRound, ShieldCheck, User, Users } from 'lucide-react'
const GROUPS: NavGroup[] = [
{
id: 'account',
label: 'Account',
items: [
{ id: 'profile', label: 'Profile', icon: <User /> },
{ id: 'notifications', label: 'Notifications', icon: <BellRing /> },
{ id: 'security', label: 'Security', icon: <ShieldCheck /> },
],
},
{
id: 'workspace',
label: 'Workspace',
items: [
{ id: 'members', label: 'Members', icon: <Users /> },
{ id: 'billing', label: 'Billing', icon: <CreditCard /> },
{ id: 'api-keys', label: 'API keys', icon: <KeyRound /> },
],
},
]
export default function InnerMenuHero() {
// The menu does not render the sections. Your state (or your router) holds the current one.
const [section, setSection] = React.useState('profile')
const current = GROUPS.flatMap((group) => group.items).find((item) => item.id === section)
return (
<div className="flex h-dvh flex-col bg-background md:flex-row">
<InnerMenu title="Settings" groups={GROUPS} value={section} onValueChange={setSection} />
<div className="min-w-0 flex-1 overflow-y-auto">
<PageContainer size="narrow">
<PageHeader title={current?.label} description="The menu selects the section that shows here." />
</PageContainer>
</div>
</div>
)
}Use the buttons above the preview to see the menu on a phone. Below 768px, the menu becomes a row of tabs that scrolls.
Put the menu first in a container with the classes flex flex-col md:flex-row. Put the content that scrolls after the menu.
import { InnerMenu } from 'ferry-ui'
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
<InnerMenu title="Settings" groups={groups} />
<div className="min-w-0 flex-1 overflow-y-auto">{/* the section */}</div>
</div>groups is a list of NavGroup. The id of each item must be unique in all the groups.
Pass value and onValueChange. The item with the same id as value is the current item. The demo at the top of the page uses this.
const [section, setSection] = React.useState('profile')
<InnerMenu title="Settings" groups={groups} value={section} onValueChange={setSection} />Give an href to each item. Set active on the item of the current page.
const groups: NavGroup[] = [
{
id: 'project',
items: [
{ id: 'overview', label: 'Overview', href: '/project', active: pathname === '/project' },
{ id: 'members', label: 'Members', href: '/project/members', active: pathname === '/project/members' },
{ id: 'docs', label: 'Documentation', href: 'https://example.com/docs', external: true },
],
},
]A badge shows at the end of the row. The user cannot select a disabled item. A long label stays on one line.
import * as React from 'react'
import { Badge, InnerMenu, type NavGroup } from 'ferry-ui'
const GROUPS: NavGroup[] = [
{
id: 'reports',
label: 'Reports',
items: [
{ id: 'revenue', label: 'Weekly revenue by region and sales channel' },
{ id: 'invoices', label: 'Invoices', badge: <Badge variant="info">12</Badge> },
{ id: 'forecast', label: 'Forecast', badge: <Badge variant="warning">Beta</Badge> },
{ id: 'exports', label: 'Scheduled exports', disabled: true },
],
},
]
export default function InnerMenuStates() {
const [section, setSection] = React.useState('revenue')
return (
<div className="flex h-dvh flex-col bg-background md:flex-row">
<InnerMenu title="Analytics" groups={GROUPS} value={section} onValueChange={setSection} />
<div className="min-w-0 flex-1 bg-dot-grid" />
</div>
)
}header shows between the title and the groups. footer shows below the groups. The row of tabs does not show these two slots.
import * as React from 'react'
import { Badge, Button, InnerMenu, toast, type NavGroup } from 'ferry-ui'
import { Plug, Settings2, Users } from 'lucide-react'
const GROUPS: NavGroup[] = [
{
id: 'project',
items: [
{ id: 'general', label: 'General', icon: <Settings2 /> },
{ id: 'members', label: 'Members', icon: <Users /> },
{ id: 'integrations', label: 'Integrations', icon: <Plug /> },
],
},
]
export default function InnerMenuHeaderFooter() {
const [section, setSection] = React.useState('general')
return (
<div className="flex h-dvh flex-col bg-background md:flex-row">
<InnerMenu
title="Project settings"
groups={GROUPS}
value={section}
onValueChange={setSection}
header={
<div className="flex items-center justify-between gap-2 px-3">
<span className="truncate text-sm text-foreground">Billing portal</span>
<Badge font="mono">Pro</Badge>
</div>
}
footer={
<div className="flex flex-col items-start gap-2 rounded-lg border border-dashed p-3">
<p className="text-[13px] text-foreground-light">Do you have a question about a setting?</p>
<Button size="tiny" onClick={() => toast('The support form opens here')}>
Contact support
</Button>
</div>
}
/>
<div className="min-w-0 flex-1 bg-dot-grid" />
</div>
)
}Set mobileTabs to false. The side menu then shows at each width.
import * as React from 'react'
import { InnerMenu, type NavGroup } from 'ferry-ui'
const GROUPS: NavGroup[] = [
{
id: 'account',
items: [
{ id: 'profile', label: 'Profile' },
{ id: 'notifications', label: 'Notifications' },
{ id: 'security', label: 'Security' },
],
},
]
export default function InnerMenuNoTabs() {
const [section, setSection] = React.useState('profile')
return (
// The container is a row at each width, because the menu stays on the side.
<div className="flex h-dvh bg-background">
<InnerMenu
label="Account settings"
groups={GROUPS}
value={section}
onValueChange={setSection}
mobileTabs={false}
/>
<div className="min-w-0 flex-1 bg-dot-grid" />
</div>
)
}<nav> element. label sets its name. The default is title when it is text, or "Section".aria-current="page".external item opens a new tab. externalLabel sets the text that screen readers get after the label. The default is "(opens in a new tab)".InnerMenu accepts only the props of this table.
| Prop | Type | Default |
|---|---|---|
groupsRequired | NavGroup[] | - |
Sections of the menu, separated by hairline borders. A group | ||
title | ReactNode | - |
Heading of the menu, shown in a 48px bar at the top (e.g. the name of the entity being viewed). | ||
header | ReactNode | - |
Custom content rendered between the title bar and the groups (a switcher, a search field, a back link). Omit | ||
footer | ReactNode | - |
Content pinned under the groups (a help card, a danger-zone link). Desktop menu only. | ||
label | string | - |
Accessible name of the navigation landmark (the | ||
value | string | - |
Id of the current item, for menus that switch sections without a router (tabs-like). An item is active when its own | ||
onValueChange | ((id: string) => void) | - |
Called with the item id when an item becomes the current section: a button, or a plain click on an internal link. Not called for | ||
mobileTabs | boolean | true |
Below the | ||
linkComponent | LinkComponent | - |
Router-aware link used for internal items. Defaults to the nearest | ||
externalLabel | string | (opens in a new tab) |
Screen-reader text appended to the name of | ||
className | string | - |
Extra classes for the side menu ( | ||