npm

Customization

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

The sidebar, nav config, and open-tabs system are app-shell patterns, not @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.

nav-config.ts
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 },
    ],
  },
];
  • Section vs group: a section is a visual band with a heading (flat, always shown); a group is a collapsible parent that nests links and toggles them open/closed.
  • A group parent has no page of its own; its breadcrumb links to its first child (see breadcrumbs).
  • Every link takes an icon and an optional color (a Tailwind text-* class); mirror it in route-meta.ts so the page title, breadcrumb, and tab share the accent.

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)

app/(app)/layout.tsx
<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

  • Labels, icons, and colors derive from nav-config.ts + route-meta.ts, with no per-tab wiring.
  • The open list persists in sessionStorage, capped by NEXT_PUBLIC_MAX_TABS (default 5; oldest is FIFO-evicted with a warning).
  • Tabs are drag-reorderable and right-click-taggable with one of seven colors.
  • ⌘/Ctrl-clicka sidebar item opens a background tab. For a custom “open in new tab” button, call useOpenTabs().openTab(href, { background: true }).
  • Tab identity is normalized via tabKey (trailing-slash safe), so /foo and /foo/ are the same tab.

Keep-alive needs a static export

Keep-alive works because the reference app is a static export (all client at runtime). If your app uses server components per route, either adopt the static-export shell from the demo or fall back to a lighter navigation-tab model (a plain router push per tab), but the strip, persistence, and nav-config wiring stay the same.

Env config for the shell (NEXT_PUBLIC_MAX_TABS, your NEXT_PUBLIC_LOGO_URL, and footer branding) lives in Configuration.