Navigation, sections & tabs
One config drives the whole shell: the sidebar, the breadcrumb trail, and the open-tabs strip all read from nav-config.ts. This page covers the two ways to group sidebar items and the browser-style keep-alive tabs, two upgrades that are easy to miss because they live in the app shell, not the component package.
These are reference-app patterns
@viliha/vui-ui exports. Copy nav-config.ts, route-meta.ts, app-sidebar.tsx, and open-tabs.tsx from the backoffice demo and adapt. When you install the package fresh, you must wire these in; they won't appear automatically.One nav config
nav-config.ts is the single source of truth. The sidebar renders it, the breadcrumbs derive their trail from it, and the tab strip pulls labels, icons, and colors from it. Add or move a page in one place and everything follows.
Two ways to group sidebar items
There are exactly two grouping shapes. Pick the one the structure calls for; don't hand-roll a third.
1 · Section (a titled band)
A NavSection is a top-level band with an optional title heading. Its items are always visible; there is no collapse. Use it to cluster related pages under a label (e.g. Records, System). The first section usually has no title (Home, Charts, …).
2 · Collapsible group (hide / unhide)
A NavGroup is an entry inside a section that has children. It renders as a parent row with a chevron that hides and unhides its nested links, and it opens automatically when one of those children is the active route. Reach for it to tuck a cluster of sub-pages under one parent (e.g. Auth, CRM, System) and keep the sidebar short.
export const NAV: NavSection[] = [
// Section with NO title: always-visible top-level links + one collapsible group
{
items: [
{ label: "Home", href: "/dashboard", icon: Home, color: "text-blue-500" },
{ label: "Charts", href: "/charts", icon: BarChart3, color: "text-fuchsia-500" },
// A collapsible group: a parent with children (hide / unhide)
{
label: "Auth",
icon: Lock,
color: "text-rose-500",
children: [
{ label: "Sign in", href: "/auth/signin", icon: LogIn },
{ label: "Sign up", href: "/auth/signup", icon: Users },
],
},
],
},
// Section WITH a title heading: a static labeled band of links
{
title: "Records",
items: [
{ label: "Organizations", href: "/organizations", icon: Building2 },
{ label: "Branches", href: "/branches", icon: Network },
],
},
];Open tabs (keep-alive)
Admin users tend to keep several pages open at once, so the shell ships a browser-style tab strip under the top bar. It's keep-alive: every opened page stays mounted (inactive ones hidden), so switching tabs is instant (no remount, no flash) and each page keeps its live state, from scroll position to form input to active filters, including data already fetched from a server, which is not re-fetchedwhen you return to the tab. New routes mount on first visit; a route's element is cached once and reused, so returning never remounts it (within NEXT_PUBLIC_MAX_TABS).
Wiring (mount once in the app layout)
<OpenTabsProvider>
<AppSidebar />
<TopBar />
<TabStrip /> {/* the strip, under the top bar */}
<div className="…overflow-hidden">
{/* Keep-alive: open pages stay mounted so switching is instant */}
<KeepAliveTabs>{children}</KeepAliveTabs>
</div>
</OpenTabsProvider>What you get
Keep-alive needs a static export
Env config for the shell (NEXT_PUBLIC_MAX_TABS, your NEXT_PUBLIC_LOGO_URL, and footer branding) lives in Configuration.