Layout
Icon Rail
The side navigation of an application, with icons and labels that the user can hide.
Layout
The side navigation of an application, with icons and labels that the user can hide.
import * as React from 'react'
import { IconRail, type NavGroup, type NavItem } from 'ferry-ui'
import { CreditCard, FolderKanban, LayoutDashboard, Settings, Users } from 'lucide-react'
const MAIN: NavItem[] = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard /> },
{ id: 'projects', label: 'Projects', icon: <FolderKanban /> },
{ id: 'members', label: 'Members', icon: <Users /> },
]
const WORKSPACE: NavItem[] = [
{ id: 'billing', label: 'Billing', icon: <CreditCard /> },
{ id: 'settings', label: 'Settings', icon: <Settings /> },
]
export default function IconRailHero() {
// A real app reads the current page from its router. This demo keeps it in state.
const [page, setPage] = React.useState('overview')
const withState = (item: NavItem): NavItem => ({
...item,
active: item.id === page,
onSelect: () => setPage(item.id),
})
const groups: NavGroup[] = [
{ id: 'main', items: MAIN.map(withState) },
{ id: 'workspace', items: WORKSPACE.map(withState) },
]
const current = [...MAIN, ...WORKSPACE].find((item) => item.id === page)
return (
<div className="flex h-dvh bg-background">
<IconRail groups={groups} />
<div className="flex-1 p-6">
<h1 className="text-lg font-medium text-foreground">{current?.label}</h1>
<p className="mt-1 text-[13px] text-foreground-light">Select an item of the rail to change the page.</p>
</div>
</div>
)
}The button at the bottom of the rail shows or hides the labels.
NavGroup[] to the rail, the phone drawer and the Command Menu.Pass groups for lists with a divider between them, or items for one list. Each item is a NavItem.
import { IconRail, IconRailItem } from 'ferry-ui'
<IconRail
groups={groups}
logo={<Logo />}
footer={<IconRailItem item={item} />}
/>Set active on the item of the current page. An item with href is a link. An item with no href is a button that calls onSelect.
const groups: NavGroup[] = [
{
id: 'main',
items: [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard />, href: '/', active: pathname === '/' },
{ id: 'projects', label: 'Projects', icon: <FolderKanban />, href: '/projects', active: pathname.startsWith('/projects') },
{ id: 'search', label: 'Search', icon: <Search />, onSelect: openSearch },
],
},
]A collapsed rail is 56px wide. An expanded rail is 200px wide. defaultExpanded expands the rail at the start. The label of a group then shows as a heading.
import * as React from 'react'
import { IconRail, type NavGroup, type NavItem } from 'ferry-ui'
import { CreditCard, FolderKanban, LayoutDashboard, Settings, Users } from 'lucide-react'
const MAIN: NavItem[] = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard /> },
{ id: 'projects', label: 'Projects', icon: <FolderKanban /> },
{ id: 'members', label: 'Members', icon: <Users /> },
]
const WORKSPACE: NavItem[] = [
{ id: 'billing', label: 'Billing', icon: <CreditCard /> },
{ id: 'settings', label: 'Settings', icon: <Settings /> },
]
export default function IconRailExpanded() {
const [page, setPage] = React.useState('projects')
const withState = (item: NavItem): NavItem => ({
...item,
active: item.id === page,
onSelect: () => setPage(item.id),
})
const groups: NavGroup[] = [
{ id: 'main', items: MAIN.map(withState) },
// The label of a group shows as a heading while the rail is expanded.
{ id: 'workspace', label: 'Workspace', items: WORKSPACE.map(withState) },
]
return (
<div className="flex h-dvh bg-background">
<IconRail groups={groups} defaultExpanded />
<div className="flex-1 bg-dot-grid" />
</div>
)
}To control the state, pass expanded and onExpandedChange. The rail does not save the state between visits.
import * as React from 'react'
import { Button, IconRail, type NavItem } from 'ferry-ui'
import { FolderKanban, LayoutDashboard, Users } from 'lucide-react'
const ITEMS: NavItem[] = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard />, active: true },
{ id: 'projects', label: 'Projects', icon: <FolderKanban /> },
{ id: 'members', label: 'Members', icon: <Users /> },
]
export default function IconRailControlled() {
// Your code holds the state. Save it in the storage of your app to keep it between visits.
const [expanded, setExpanded] = React.useState(true)
return (
<div className="flex h-dvh bg-background">
<IconRail items={ITEMS} expanded={expanded} onExpandedChange={setExpanded} />
<div className="flex flex-1 flex-col items-start gap-3 p-6">
<p className="text-[13px] text-foreground-light">The rail is {expanded ? 'expanded' : 'collapsed'}.</p>
<Button onClick={() => setExpanded((value) => !value)}>{expanded ? 'Collapse the rail' : 'Expand the rail'}</Button>
</div>
</div>
)
}logo is the slot above the items. footer is the slot below the items. Use IconRailItem in the footer for rows with the look of the items. In a slot, useIconRail() gives the state of the rail.
import * as React from 'react'
import { IconRail, IconRailItem, useIconRail, type NavItem } from 'ferry-ui'
import { FolderKanban, Keyboard, LayoutDashboard, LifeBuoy, Users } from 'lucide-react'
const ITEMS: NavItem[] = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard />, active: true },
{ id: 'projects', label: 'Projects', icon: <FolderKanban /> },
{ id: 'members', label: 'Members', icon: <Users /> },
]
/** A square mark. It shows the name of the product when the rail is expanded. */
function Wordmark() {
const rail = useIconRail()
return (
<span className="flex h-9 items-center gap-2 px-[9px] text-sm font-medium text-foreground">
<span className="size-[18px] shrink-0 rounded-sm bg-brand" aria-hidden="true" />
{rail?.expanded && 'Acme'}
</span>
)
}
export default function IconRailSlots() {
const [message, setMessage] = React.useState('Select an item of the footer.')
return (
<div className="flex h-dvh bg-background">
<IconRail
items={ITEMS}
defaultExpanded
logo={<Wordmark />}
footer={
<>
<IconRailItem
item={{ id: 'help', label: 'Help center', icon: <LifeBuoy />, onSelect: () => setMessage('Help center') }}
/>
<IconRailItem
item={{ id: 'shortcuts', label: 'Shortcuts', icon: <Keyboard />, onSelect: () => setMessage('Shortcuts') }}
/>
</>
}
/>
<p className="flex-1 p-6 text-[13px] text-foreground-light">{message}</p>
</div>
)
}A badge shows after the label. On a collapsed rail, the badge becomes a dot. The user cannot select a disabled item. collapsible={false} hides the button at the bottom of the two rails.
import { Badge, IconRail, type NavItem } from 'ferry-ui'
import { CreditCard, FolderKanban, Inbox, LayoutDashboard } from 'lucide-react'
const ITEMS: NavItem[] = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard />, active: true },
{
id: 'inbox',
label: 'Inbox',
icon: <Inbox />,
badge: (
<Badge variant="primary" font="mono">
12
</Badge>
),
},
{ id: 'projects', label: 'Projects', icon: <FolderKanban /> },
{ id: 'billing', label: 'Billing', icon: <CreditCard />, disabled: true },
]
export default function IconRailBadges() {
return (
<div className="flex h-dvh bg-background">
{/* Two rails on one page: each one needs its own name. */}
<IconRail items={ITEMS} collapsible={false} aria-label="Collapsed rail" />
<IconRail items={ITEMS} collapsible={false} defaultExpanded aria-label="Expanded rail" />
<div className="flex-1 bg-dot-grid" />
</div>
)
}<nav> element. aria-label sets its name. The default is "Main".TooltipProvider above the rail.external item opens a new tab. Screen readers get the external text after the label.labels sets the texts of the rail. Use it to translate them.Key of labels | Default text |
|---|---|
expand | "Expand menu" |
collapse | "Collapse menu" |
collapseText | "Collapse" |
external | "(opens in a new tab)" |
IconRail also accepts each attribute of the <nav> element.
| Prop | Type | Default |
|---|---|---|
groups | NavGroup[] | - |
Navigation groups, separated by thin dividers. A group | ||
items | NavItem[] | - |
Shorthand for a single group of items (no dividers). Ignored when | ||
expanded | boolean | - |
Controlled expanded state (labels visible, 200px wide). Pair with | ||
defaultExpanded | boolean | false |
Initial expanded state when uncontrolled. Defaults to | ||
onExpandedChange | ((expanded: boolean) => void) | - |
Called when the toggle expands or collapses the rail. Persist the value yourself if needed. | ||
collapsible | boolean | true |
Shows the expand / collapse toggle at the bottom. Defaults to | ||
logo | ReactNode | - |
Top slot, above the items (brand mark, workspace switcher). It is 40px wide while collapsed: render a square mark, and use | ||
footer | ReactNode | - |
Bottom slot, pinned above the toggle: secondary rows (help, docs, settings) as | ||
linkComponent | LinkComponent | - |
Router-aware link used by every row. Defaults to the nearest | ||
labels | IconRailLabels | - |
Built-in texts: the expand / collapse toggle and the screen-reader suffix of external items. | ||
aria-label | string | - |
Accessible name of the | ||
Use IconRailItem only in an IconRail.
| Prop | Type | Default |
|---|---|---|
itemRequired | NavItem | - |
The destination or action to render (label, icon, href / onSelect, active, badge…). | ||
linkComponent | LinkComponent | - |
Router-aware link for this item. Defaults to the rail's | ||
className | string | - |
Extra classes for the row. | ||
useIconRail() reads the state of the nearest rail. Outside a rail, it returns null.
| Value | Type | Role |
|---|---|---|
expanded | boolean | Tells if the rail shows the labels. |
setExpanded | (expanded: boolean) => void | Expands or collapses the rail. |
One destination or one action. Icon Rail, Mobile Nav, Inner Menu and Command Menu use this type.
| Field | Type | Role |
|---|---|---|
id | string | A stable key. Required. |
label | string | The visible text and the accessible name. Required. |
icon | ReactNode | A line icon. |
href | string | The destination. The link component of the app renders it. |
onSelect | () => void | Runs when the user selects the item. |
active | boolean | Marks the current page. |
external | boolean | Opens href in a new tab. The link component does not render it. |
badge | ReactNode | A count or a tag after the label. |
disabled | boolean | Dims the item. The user cannot select it. |
A group of items with an optional heading.
| Field | Type | Role |
|---|---|---|
id | string | A stable key. Required. |
label | string | The heading of the group. |
items | NavItem[] | The items of the group, in order. Required. |