Shells
A shell is the frame your application lives in: the sidebar, the header, the content region. VUI ships two of them, and this page is how you decide which one you want.
Which shell should I use?
Pick by how deep the navigation goes, which is the one question that actually separates them.
- Console shell— a flat or shallow menu, and a layout the buyer can switch between six presets. This is what the free editions wear and what the demos show. Import it from
@viliha/vui-react/voilet-shell. - Workspace shell— a deep, grouped sidebar that remembers which group is open, collapses to a rail, and can open a route in a background tab. This is the Pro shell. Import it from
@viliha/vui-react/workspace-shell.
pnpm check:shell-tier. The tier each shell carries is a row in the shells table, so an operator changes it in the back office rather than by editing code.How a shell is put together
Every shell is two files, and the split is what lets one shell serve React, Vue and Angular without three implementations drifting apart.
<name>-shell-core.tsdecides. Which group is open, which item is current, whether the rail is showing, how wide the sidebar is. Framework free, no imports from React or Vue, and every renderer calls it.<name>-shell.tsxdraws. One renderer per framework, holding markup and nothing else.
Three editions used to answer those four questions themselves, in 876 lines of code, and only one of the three had a test for it. The core is why the answers now agree by construction.
Wiring one in
The shell holds no router and no branding. It takes them as props, which is what lets the same file serve a Next app, a Vite app and an operator console. Your app passes its navigation, its link component and the current path.
"use client";
import { AppSidebar as Sidebar } from "@viliha/vui-react/workspace-shell";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { NAV } from "./nav";
export function AppSidebar() {
return (
<Sidebar
nav={NAV}
pathname={usePathname()}
Link={Link}
brand={(collapsed) => <OrgSwitcher collapsed={collapsed} />}
quickActions={(collapsed) => <QuickActionsLauncher collapsed={collapsed} />}
/>
);
}The slots
nav,pathname,Link— required. The navigation tree, the current route and your router’s link component.brandandquickActionsboth accept a function of the rail stateas well as a plain element. Take the function form if your control looks different when collapsed, which a tenant switcher and a ⌘K launcher both do; a plain element is frozen in its expanded form and overflows a 4rem rail.brandis a slot rather than an import because an operator console has no organizations to switch between, and half the consumers would be wrong either way.quickActions— your ⌘K launcher, if you have one.groupMode— how a group opens once the sidebar is collapsed to a rail:flyout-hover(the default),flyout-click, orinline.onBackgroundOpen— optional, and its absence is the tab strip’s absence. Pass a handler and a command-click opens a background tab; pass nothing and the gesture falls through to the browser.
Using a shell outside React
Import the core, write the markup in your framework. The core is plain TypeScript published at @viliha/vui-core/voilet-shell and @viliha/vui-core/workspace-shell, so a Vue or Angular renderer inherits every decision and its whole test suite without porting any of it.