Layout
Top Bar
The header of an application, with a logo, a trail of segments and a group of actions.
Layout
The header of an application, with a logo, a trail of segments and a group of actions.
import * as React from 'react'
import {
DropdownMenuItem,
ResourceSwitcher,
ThemeMenu,
TopBar,
TopBarLogo,
TopBarSearch,
TopBarSegment,
TopBarSeparator,
TopBarUserMenu,
toast,
type ResourceSwitcherItem,
} from 'ferry-ui'
import { Building2, FolderKanban } from 'lucide-react'
const PROJECTS: ResourceSwitcherItem[] = [
{ id: 'billing-portal', label: 'Billing portal', icon: <FolderKanban /> },
{ id: 'customer-app', label: 'Customer app', icon: <FolderKanban /> },
{ id: 'status-page', label: 'Status page', icon: <FolderKanban /> },
]
export default function TopBarHero() {
const [project, setProject] = React.useState('billing-portal')
// The bar holds no state. A real app opens its phone drawer and its command menu here.
return (
<TopBar
onOpenMobileNav={() => toast('The phone drawer opens here')}
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={() => toast('The command menu opens here')} />
<ThemeMenu />
<TopBarUserMenu name="Maya Chen" description="maya@example.com">
<DropdownMenuItem>Sign out</DropdownMenuItem>
</TopBarUserMenu>
</>
}
>
{/* On a phone, this segment hides together with its separator. A real app gives it an `href`. */}
<span className="flex min-w-0 items-center gap-0.5 max-sm:hidden">
<TopBarSeparator />
<TopBarSegment icon={<Building2 />} chevron={false}>
Acme
</TopBarSegment>
</span>
<TopBarSeparator />
<ResourceSwitcher label="switch project" items={PROJECTS} value={project} onValueChange={setProject} />
</TopBar>
)
}Use the buttons above the preview to see the bar on a tablet and on a phone.
TopBar in the topBar slot of App Shell.The trail is the children of the bar. The logo and the actions are slots.
import {
TopBar,
TopBarIconButton,
TopBarLogo,
TopBarSearch,
TopBarSegment,
TopBarSeparator,
TopBarUserMenu,
} from 'ferry-ui'
<TopBar
logo={<TopBarLogo />}
actions={
<>
<TopBarSearch />
<TopBarIconButton />
<TopBarUserMenu />
</>
}
>
<TopBarSeparator />
<TopBarSegment />
</TopBar>| Part | Role |
|---|---|
TopBar | The header. It is 48px high. |
TopBarLogo | The brand mark at the start of the trail. |
TopBarSeparator | The slash before each segment. |
TopBarSegment | One item of the trail: a button or a link. |
TopBarSearch | A button with the look of a search field. |
TopBarIconButton | A round button with one icon. |
TopBarUserMenu | The account menu. It comes last in the actions. |
Put a TopBarSeparator before each segment. A segment with href is a link. Set current on the segment of the current page.
<TopBar logo={<TopBarLogo href="/" label="Acme home">{logo}</TopBarLogo>}>
<TopBarSeparator />
<TopBarSegment href="/projects">Projects</TopBarSegment>
<TopBarSeparator />
<TopBarSegment href="/projects/billing-portal" current>
Billing portal
</TopBarSegment>
</TopBar>A segment with no href is a button with a chevron. To open a Dropdown Menu, put the segment in a DropdownMenuTrigger with asChild. chevron={false} hides the chevron.
For a long list with a search field, use Resource Switcher.
import * as React from 'react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
TopBar,
TopBarLogo,
TopBarSegment,
TopBarSeparator,
} from 'ferry-ui'
import { Building2 } from 'lucide-react'
const WORKSPACES = [
{ id: 'acme', name: 'Acme' },
{ id: 'acme-labs', name: 'Acme Labs' },
{ id: 'acme-europe', name: 'Acme Europe' },
]
export default function TopBarSegmentMenu() {
const [workspace, setWorkspace] = React.useState('acme')
const current = WORKSPACES.find((entry) => entry.id === workspace)
return (
<TopBar
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>
}
>
<TopBarSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TopBarSegment icon={<Building2 />}>{current?.name}</TopBarSegment>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-52">
<DropdownMenuLabel>Workspace</DropdownMenuLabel>
<DropdownMenuRadioGroup value={workspace} onValueChange={setWorkspace}>
{WORKSPACES.map((entry) => (
<DropdownMenuRadioItem key={entry.id} value={entry.id}>
{entry.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<TopBarSeparator />
<TopBarSegment chevron={false}>Settings</TopBarSegment>
</TopBar>
)
}loading shows a skeleton in place of the text. badge adds content after the text, but not on a phone. The segment cuts long text at 180px. disabled blocks the segment.
import { Badge, TopBar, TopBarLogo, TopBarSegment, TopBarSeparator } from 'ferry-ui'
import { Building2, FolderKanban } from 'lucide-react'
export default function TopBarSegmentStates() {
return (
<TopBar
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>
}
>
<TopBarSeparator />
<TopBarSegment icon={<Building2 />} loading aria-label="Workspace" />
<TopBarSeparator />
<TopBarSegment
icon={<Building2 />}
badge={
<Badge font="mono" case="normal">
Pro
</Badge>
}
>
Acme
</TopBarSegment>
<TopBarSeparator />
<TopBarSegment icon={<FolderKanban />}>Revenue reports for the finance team of Acme Europe</TopBarSegment>
<TopBarSeparator />
<TopBarSegment icon={<FolderKanban />} disabled>
Archived project
</TopBarSegment>
</TopBar>
)
}TopBarSearch is a button, not a field. Open a Command Menu from its onClick. shortcut sets the key hint, and false hides it. The component does not bind the key.
On a phone, the button shows only its icon. compactOnMobile={false} keeps the full button.
Clicks: 0
import * as React from 'react'
import { TopBarSearch } from 'ferry-ui'
export default function TopBarSearchDemo() {
const [clicks, setClicks] = React.useState(0)
const open = () => setClicks((count) => count + 1)
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-wrap items-center justify-center gap-3">
<TopBarSearch onClick={open} />
<TopBarSearch placeholder="Search invoices…" shortcut="/" keyShortcuts="/" onClick={open} />
<TopBarSearch placeholder="Go to…" shortcut={false} onClick={open} />
<TopBarSearch compactOnMobile={false} onClick={open} />
</div>
<p className="text-[13px] text-foreground-light">Clicks: {clicks}</p>
</div>
)
}TopBarIconButton needs icon and label. The label is the accessible name and the text of the tooltip. With href, the button is a link.
import { TopBarIconButton, toast } from 'ferry-ui'
import { Bell, CircleHelp, LogOut, RefreshCw } from 'lucide-react'
export default function TopBarIconButtons() {
return (
<>
<TopBarIconButton icon={<Bell />} label="Notifications" onClick={() => toast('No new notifications')} />
<TopBarIconButton icon={<CircleHelp />} label="Help" onClick={() => toast('Help opens here')} />
<TopBarIconButton icon={<RefreshCw />} label="Refresh" loading />
<TopBarIconButton icon={<LogOut />} label="Sign out" tooltip={false} disabled />
</>
)
}The trigger shows the initials of name. The children are the items of the menu. Pass header to replace the name and the description. Pass avatarSrc to show a picture.
import { Badge, DropdownMenuItem, DropdownMenuSeparator, TopBarUserMenu } from 'ferry-ui'
import { Building2, CreditCard, LogOut, Settings, UserRound, Users } from 'lucide-react'
export default function TopBarUserMenuDemo() {
return (
<>
<TopBarUserMenu name="Maya Chen" description="maya@example.com">
<DropdownMenuItem>
<UserRound /> Profile
</DropdownMenuItem>
<DropdownMenuItem>
<Settings /> Account settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut /> Sign out
</DropdownMenuItem>
</TopBarUserMenu>
<TopBarUserMenu
label="Workspace account"
fallback={<Building2 className="size-3.5" />}
align="start"
header={
<div className="flex items-center justify-between gap-2 px-2 py-1.5 text-[13px] text-foreground">
<span className="truncate font-medium">Acme</span>
<Badge font="mono" case="normal">
Pro
</Badge>
</div>
}
>
<DropdownMenuItem>
<Users /> Members
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard /> Billing
</DropdownMenuItem>
</TopBarUserMenu>
</>
)
}Pass onOpenMobileNav. The bar then shows a menu button below 768px. Open a Mobile Nav from this function.
<TopBar onOpenMobileNav={() => setNavOpen(true)} />TopBarLogo needs a label. The label is the accessible name of the logo.<nav> element. navLabel sets its name. The default is "Breadcrumb".mobileNavLabel sets the accessible name of the menu button. The default is "Open navigation".TopBarIconButton needs a TooltipProvider above the bar. See Tooltip.Each part also accepts the attributes of its element. TopBarUserMenu does not.
| Prop | Type | Default |
|---|---|---|
logo | ReactNode | - |
Brand mark rendered first in the trail, usually a | ||
children | ReactNode | - |
The breadcrumb-like trail after the logo: | ||
actions | ReactNode | - |
Right-aligned cluster (8px gap): | ||
onOpenMobileNav | (() => void) | - |
Shows a hamburger button below the | ||
mobileNavLabel | string | Open navigation |
Accessible label of the hamburger button (default "Open navigation"). | ||
navLabel | string | Breadcrumb |
Accessible label of the trail | ||
| Prop | Type | Default |
|---|---|---|
labelRequired | string | - |
Accessible name, e.g. "Acme home". Required: the logo itself is decorative. | ||
childrenRequired | ReactNode | - |
The brand mark (an inline SVG or | ||
href | string | - |
Destination (usually the app home). Without it the logo renders as a static image. | ||
linkComponent | LinkComponent | - |
Link component override (defaults to the nearest | ||
TopBarSeparator has no props of its own. It accepts the attributes of the element it renders.
| Prop | Type | Default |
|---|---|---|
icon | ReactNode | - |
Leading 16px icon (muted), e.g. the entity's type icon. | ||
children | ReactNode | - |
Segment text, truncated past 180px. Keep it to text (or inline content): it is wrapped in a truncating block, so put an icon or avatar in | ||
badge | ReactNode | - |
Trailing adornment after the text (version or plan badge…), inside the hit area. Hidden below the | ||
chevron | boolean | - |
Shows the ⇕ chevron that signals a switcher. Defaults to | ||
loading | boolean | false |
Replaces the text with a skeleton while the segment's data loads (sets | ||
disabled | boolean | - |
Dims the segment and blocks interaction. On a link segment it sets | ||
href | string | - |
Renders the segment as a link (through the link component) instead of a button. | ||
current | boolean | false |
Marks a link segment as the current page ( | ||
linkComponent | LinkComponent | - |
Link component override for | ||
| Prop | Type | Default |
|---|---|---|
label | string | Search |
Accessible name (default "Search"); the shortcut is appended to it. | ||
placeholder | string | Search… |
Visible text of the wide trigger (default "Search…"). | ||
shortcut | string | false | - |
Keyboard hint shown on the right. Defaults to "⌘K" on Apple platforms and "Ctrl K" elsewhere ("Ctrl K" during server rendering and hydration, then updated, so SSR markup always matches); | ||
keyShortcuts | string | - |
| ||
compactOnMobile | boolean | true |
Collapse into a round icon-only button below the | ||
| Prop | Type | Default |
|---|---|---|
iconRequired | ReactNode | - |
The 16px icon. | ||
labelRequired | string | - |
Accessible name, also shown as a tooltip. | ||
loading | boolean | - |
Shows a spinner in place of | ||
tooltip | boolean | true |
Show | ||
href | string | - |
Renders a link instead of a button (help, docs, changelog…). | ||
external | boolean | false |
Opens | ||
linkComponent | LinkComponent | - |
Link component override for internal | ||
| Prop | Type | Default |
|---|---|---|
name | string | - |
Display name, shown in the menu header and used for the initials fallback. | ||
description | ReactNode | - |
Secondary header line, typically the email address or the current role. | ||
avatarSrc | string | - |
Avatar picture URL. Without it (or while it loads) the fallback shows. | ||
fallback | ReactNode | - |
Fallback inside the round trigger; defaults to the initials of | ||
header | ReactNode | - |
Replaces the default name / description header. | ||
label | string | Account |
Accessible name of the trigger (default "Account"). | ||
children | ReactNode | - |
Menu items ( | ||
open | boolean | - |
Controlled open state (pair with | ||
defaultOpen | boolean | - |
Initial open state when uncontrolled. | ||
onOpenChange | ((open: boolean) => void) | - |
Called with the next open state. | ||
modal | boolean | - |
Radix modal mode (default | ||
align | "center" | "start" | "end" | end |
Alignment of the menu against the trigger (default "end"). | ||
className | string | - |
Classes for the round trigger. | ||
contentClassName | string | - |
Classes for the menu panel (default width 240px). | ||