A responsive frame for your application, with a collapsible sidebar, grouped navigation, nested links and an account menu. Supply navigationGroups and breadcrumbs, then render your pages through children beneath the shared header. Routing and account actions stay in your app. Explore desktop collapse and the separate mobile navigation in the demo; the examples below explain how to connect your data and control the sidebar. About this block
Getting started
Add this block
Add a complete navigation layout to an existing Preact app in three steps. Copy the source, connect the Kamod dependencies and bring your own pages. The files live in your project, so you can adapt the sidebar, header and account menu as your application grows.
1. Copy the block
Copy the files from the showcase’s Code tab into
src/components/application-shell-1. Skippreview.tsx,demo-data.tsxandassets/kamod-ui-logo.svgunless you want the demo. To keep the demo branding, copy the SVG into the sameassetssubfolder.Keep the reusable files together:
application-shell-1.tsx,app-sidebar.tsx,nav-main.tsx,nav-user.tsx,menu.tsx,types.tsandindex.ts. Their relative imports work within this folder; the entrypoint exports the component and its public types. The examples assume an importing file atsrc/App.tsx; adjust the relative import if yours lives elsewhere. Keep the repository’s license with your copy.2. Install missing dependencies
Install the packages your app does not already have. Kamod UI supplies the interactive components, Icons supplies the SVG icons, and Themes and Preact Signals support the shared styling and state setup. Use your project's existing package manager.
pnpm add @kamod-ch/ui @kamod-ch/icons @kamod-ch/themes @preact/signalsCompatibility:@kamod-ch/uimust exportuseDropdownand support theportalprop onDropdownContent, and exposecreateRovingFocusfrom@kamod-ch/ui/lib/interactive. The shell's menu adapters use these APIs to manage keyboard navigation and keep menus outside the sidebar's scroll container. Use a UI release that includes these APIs before integrating the block.3. Set up styles and import
Follow the theme and Tailwind setup in your app's global stylesheet, then ensure Tailwind scans the copied files as well as the Kamod components. An app that already uses Kamod can keep its existing theme setup. Import the shell from your new local folder:import { ApplicationShell1 } from "./components/application-shell-1";Check the first render: the sidebar, borders and page background should follow your app's theme. If the layout appears unstyled, check the global CSS import and Tailwind source detection before changing the block's classes.
Integration
Usage
import { ApplicationShell1 } from "./components/application-shell-1";
export const App = () => (
<ApplicationShell1
brand={{ name: "Acme Inc", description: "Enterprise", href: "/" }}
navigationGroups={[{
id: "workspace", label: "Workspace",
items: [{ id: "overview", label: "Overview", href: "/overview" }],
}]}
user={{ name: "Alex Morgan", email: "alex@example.com" }}
breadcrumbs={[{ label: "Overview" }]}
currentPath="/overview"
onUserAction={(action) => console.log(action)}
>
<h1 class="text-2xl font-semibold">Overview</h1>
<p>Your application content goes here.</p>
</ApplicationShell1>
);Keep the shell mounted across routes. Replace its children and route data without changing the shell’s key to preserve local sidebar state. The shell already owns its SidebarProvider; adding a second provider outside it will not control its inner navigation. Use the public desktop state props instead.
- Connect navigation
- Links use their
hrefby default. With a client router, pass its current path tocurrentPathand handle ordinary clicks inonNavigate. Update the breadcrumbs with each page; they are not inferred from the navigation tree. - Connect account actions
- Replace the example's console logging with your account, billing, notification and logout handlers. Place your routed content inside the shell; it already provides the page's
mainlandmark and content padding.
API reference
Props and data
Supply the identity, destinations and page content; the shell supplies the layout and controls. Start with the four required data props, then add callbacks or controlled desktop state as your app needs them. All ten public types are exported by your local application-shell-1 entrypoint.
types.ts, keeping the reference aligned with the block's public API. Explore the Data type reference for complete data shapes, required and optional fields, and practical notes on how each type is used.TypeScriptComponent props
Each row lists a prop accepted by ApplicationShell1, its TypeScript type and how it affects the shell. Follow a linked type to open its full definition, including individual fields and their documentation. Optional props let you supply page content, connect navigation and account actions, control desktop expansion or adjust wrapper styling.
brand, navigationGroups, user and breadcrumbs. Supply all four when using the block; the two array props may be empty. Props without this marker are optional. Hover, focus or tap the icon to see its label.| Prop / type | Description |
|---|---|
brand | Sidebar identity, an optional logo and an optional home or workspace link. |
navigationGroups | Ordered groups of destinations, with at most one level of child links. See Type your navigation data. |
user | Account name and email, with an optional avatar or custom initials. No session is inferred. |
breadcrumbs | An independent, ordered trail. The last entry is never a link; earlier entries hide below 768px. An empty array omits the trail and its separator. |
children | Page content inside the existing main landmark and padded content area. No placeholder content is inserted. |
currentPath | Exact URL used to match item href values. An item's explicit active value takes precedence; no router or path normalization is applied. |
onNavigate | Handles linked brand, breadcrumb and navigation activation, or a leaf action button. Native links still work without it. |
onUserAction | Receives an account menu selection. Your app implements the resulting navigation or account operation. |
open | Controls desktop expansion: true expands, false collapses to icons. Update this value in onOpenChange to respond to the toggle. See Sidebar state. |
defaultOpen | Initial uncontrolled desktop state. Ignored when open is supplied; changing it after mount does not reset the sidebar. |
onOpenChange | Reports requested desktop expansion in either state mode. Mobile visibility does not call this callback. |
class | Additional classes merged onto the outer SidebarProvider wrapper. |
className | Alias for class, merged after it when both are supplied. |
Wrapper styling: class and className target the shell's outer wrapper. Arbitrary HTML attributes and other provider options are not forwarded. The shell already renders main; do not add another one inside it.
ApplicationShell1PropsType your navigation data
navigationGroups and set currentPath="/projects/recent" to mark Recent projects as current. The satisfies operator checks the shape without replacing the inferred type.import { FolderIcon } from "@kamod-ch/icons/lucide";
import type { ApplicationShellNavigationGroup } from "./components/application-shell-1";
export const navigationGroups = [{
id: "workspace",
label: "Workspace",
items: [{
id: "projects",
label: "Projects",
href: "/projects",
icon: FolderIcon,
items: [
{ id: "recent", label: "Recent projects", href: "/projects/recent" },
{ id: "archive", label: "Archive", href: "/projects/archive", disabled: true },
],
}],
}] satisfies readonly ApplicationShellNavigationGroup[];- One tree, three types
- Group → item → child link. A child is a leaf, so it cannot contain another submenu.
- Keep selection explicit
- Matching is exact:
/projectsand/projects/differ. Useactivewhen your app needs its own matching rules.
Data type reference
Open a definition to inspect its exact fields, optional markers and original JSDoc. Multiple definitions can stay open for comparison. Types joined with & inherit the fields of the referenced type. The asterisk identifies each shape's required fields, including inherited ones.
navigationGroups and breadcrumbs may still be empty arrays.ComponentChildren, ComponentType and JSX in these definitions are Preact types. The copied types.ts already imports them.
Brand and logo
ApplicationShellBrandnameAccount identity
ApplicationShellUsername, emailNavigation groups
ApplicationShellNavigationGroupid, itemssrc/components/application-shell-1/types.ts/** Ordered navigation section; its optional label appears above its items. */
export type ApplicationShellNavigationGroup = {
/** Stable key, unique among groups. */
id: string;
/** Section heading; omitted headings do not leave an empty label. */
label?: string;
/** Destinations displayed in the supplied order. */
items: readonly ApplicationShellNavigationItem[];
};Keep group IDs unique and item IDs unique among siblings. Omitting a group label leaves no empty heading. Readonly arrays work directly; the shell preserves your order.
ApplicationShellNavigationItemid, labelApplicationShellNavigationLinkid, labelBreadcrumbs and callback destinations
ApplicationShellDestinationlabelApplicationShellIconBranch selection: an active child makes the branch start expanded and highlights its icon-mode menu or URL-free disclosure trigger. A parent rendered as a separate link keeps its own active state; a child does not mark that parent link as the current page. Later path changes do not reset an already mounted disclosure. Switching to desktop icon mode replaces the disclosure with a dropdown; expanding the sidebar creates a new disclosure from the current active state.
Navigation and callbacks
onNavigate and cancel only the clicks your router handles. Keep currentPath and breadcrumbs in sync with your router; the shell does not infer either.ApplicationShellNavigateThis adapter preserves modified clicks and leaves external, hash-only and other non-root-relative URLs to the browser. Connect it to your router's navigation function and pass the returned handler to onNavigate.
import type { ApplicationShellNavigate } from "./components/application-shell-1";
// Supply your router's navigation function when creating this handler.
export const createNavigateHandler = (
navigate: (href: string) => void,
): ApplicationShellNavigate => (destination, event) => {
if (
event.defaultPrevented || event.button !== 0 ||
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey
) return;
const { href } = destination;
// This example intercepts only app-local, root-relative URLs.
if (!href?.startsWith("/") || href.startsWith("//")) return;
event.preventDefault();
navigate(href);
};Mobile navigation: ordinary brand and navigation selections close the mobile sheet after the callback runs, including when it calls preventDefault(). Modified clicks leave the sheet open. Disabled destinations do not call the handler.
ApplicationShellUserActionSidebar state
- Uncontrolled Default
- Omit
open. The desktop sidebar starts expanded; usedefaultOpen={false}to start with icons.onOpenChangecan observe changes without owning state. - Controlled
- Supply
open. To let the built-in toggles change it, update this value inonOpenChange; otherwise the sidebar keeps the supplied state. Your app owns desktop expansion in this mode.
import { useState } from "preact/hooks";
import {
ApplicationShell1,
type ApplicationShell1Props,
} from "./components/application-shell-1";
type AppFrameProps = Omit<
ApplicationShell1Props,
"open" | "defaultOpen" | "onOpenChange"
>;
export const AppFrame = (props: AppFrameProps) => {
const [open, setOpen] = useState(true);
return (
<ApplicationShell1 {...props} open={open} onOpenChange={setOpen} />
);
};Desktop and mobile are separate. Below 768px the sidebar uses an independently managed sheet that starts closed when the shell mounts. The toggle opens that sheet instead of changing desktop expansion, so mobile toggles do not call onOpenChange. Setting open or defaultOpen does not open the mobile sheet; your desktop preference still applies when returning to a wider screen.
Remember the desktop preference. For optional persistence, install @kamod-ch/hooks with your package manager (for example, pnpm add @kamod-ch/hooks) and use this version of AppFrame. Kamod Hooks' useLocalStorageState handles storage and state updates while keeping the same open/onOpenChange wiring. This package is only needed for this optional example.
import { useLocalStorageState } from "@kamod-ch/hooks";
import {
ApplicationShell1,
type ApplicationShell1Props,
} from "./components/application-shell-1";
type AppFrameProps = Omit<
ApplicationShell1Props,
"open" | "defaultOpen" | "onOpenChange"
>;
export const AppFrame = (props: AppFrameProps) => {
const [open, setOpen] = useLocalStorageState<boolean>("app-sidebar-desktop-open", {
defaultValue: true,
getInitialValueInEffect: true,
deserializer: (raw) => raw !== "false",
onError: () => { /* Storage is optional; the toggle still works in memory. */ },
});
return (
<ApplicationShell1 {...props} open={open} onOpenChange={setOpen} />
);
};- What is remembered
- Only desktop expansion is saved in this browser for this site. A missing or invalid value falls back to expanded; blocked storage leaves the toggle usable without persistence. The mobile sheet and navigation submenu states are not saved.
- First render
getInitialValueInEffectuses the expanded default for both server rendering and the first client render, then restores storage after mounting. The sidebar may briefly appear expanded before a saved collapsed preference is restored. To avoid that shift, your server can read the core sidebar'ssidebar_statecookie and pass its parsed boolean asdefaultOpenon the first render, or initialize controlled state with it. The core writes this cookie but does not restore it automatically.
A closer look
About this block
Application Shell 1 is the frame around your application. It gives people a consistent place to navigate, understand where they are and reach their account controls, while your pages occupy the main content area. It suits dashboards, workspaces and internal tools that share navigation across several screens.
Structure and composition
The layout has three parts: a sidebar, a compact header and a flexible content area. The sidebar keeps context in view with a brand at the top, grouped links in the middle and an account menu at the bottom. The header places the sidebar toggle beside a vertical separator and a breadcrumb trail. A bottom border separates these controls from the page below.
The block composes existing Kamod components. SidebarProvider coordinates the sidebar controls, AppSidebar combines the brand and navigation, and SidebarInset provides the main landmark. Breadcrumb, Collapsible, Dropdown, Avatar and Separator supply the smaller pieces. This keeps the shell consistent with the rest of your Kamod interface and lets you customize a part without replacing the whole layout.
Navigation and routing
Navigation is driven by navigationGroups, displayed in the order you supply. Each group has a stable ID, an optional heading and its items. An item can be a direct link or a branch with one level of child links. Branches expand inline in the full sidebar. If a parent also has an href, its link and disclosure toggle stay separate, so opening a submenu does not unexpectedly navigate away.
Pass currentPath to highlight links by an exact match with their href. An item's explicit active value takes precedence, including false. A branch initially opens when it or a child is active; later path changes update the highlight without overriding the user's disclosure choice. Disabled items cannot be activated, and disabling a branch also disables its child links.
The shell does not choose a router for you. Links follow their URLs normally. To use client-side routing, handle onNavigate(destination, event), call event.preventDefault() for the click you handle and update your route. Preserve Ctrl/Cmd, Shift and Alt clicks so browser shortcuts keep working. A leaf without an href acts as an action button; a branch without one only toggles its submenu. Breadcrumbs are supplied separately: the last entry describes the current page, while earlier entries may link to parent destinations.
Responsive behavior and state
At desktop widths, the sidebar starts expanded and can collapse to an icon rail. Direct links keep accessible names and tooltips; branches open dropdowns that expose their parent destination, when present, and child links. The header toggle and sidebar rail both control this state, so nested navigation remains reachable even when space is limited.
Below the sidebar's 768px breakpoint, the same navigation moves into a modal sheet opened by the header toggle. The sheet starts closed and closes after an ordinary navigation selection, including a selection handled by your router. Modified clicks leave it open. On narrow screens, the breadcrumb trail shows only the current page to leave room for the toggle and page title.
Use defaultOpen for an initial desktop preference, or pass open and onOpenChange to control it from your app. The callback can also observe changes in uncontrolled mode. Desktop collapse and mobile visibility are independent: the mobile sheet manages its own state and does not overwrite the desktop preference.
Account menu and page content
The footer displays the user's name, email and optional avatar. When an image is unavailable, it shows initials from the first two words of the name, or the initials you provide. The account menu offers Account, Billing, Notifications and Log out. Choosing one reports its identifier through onUserAction and dismisses the menu. Connect these callbacks to your own pages, dialogs or authentication service; the block itself does not manage a session.
children appears beneath the header inside the existing main landmark. Replace the demo's muted placeholders with a dashboard, form, table or routed page. The content wrapper provides padding and flexible vertical space, while your page owns its headings, loading states and data. Avoid nesting another main element inside the shell. Accessibility and styling
Navigation controls retain accessible labels in icon mode, current links expose aria-current, and disclosure buttons report whether their content is expanded. Menus focus the first enabled item when opened; Arrow Up/Down move between items and Home/End jump to the first or last. Escape closes a menu and returns focus to its trigger. Inside the mobile sheet, closing the account menu with Escape leaves the sheet open; a subsequent Escape closes the sheet and restores focus to its opener.
Colors come from Kamod's semantic theme tokens, including background, foreground, border, muted and sidebar colors. The same markup supports light and dark themes, and reduced-motion preferences suppress the shell's transitions and animations. Keep labels meaningful, maintain visible focus styles and use your app's theme setup when adapting the block. Long labels and email addresses truncate visually, so concise names remain easier to scan.
Making it your own
Start by replacing the brand, navigation groups, user data and breadcrumbs. Keep group and item IDs stable, supply real destination URLs and connect the callbacks your app needs. Use class or className for wrapper styling, and edit the copied components when you need a different header, account action or navigation arrangement.
The showcase's sample workspace and hash links live in demo-data.tsx. preview.tsx adds local selection messages and placeholder panels so you can explore the interactions without an application backend. These files are optional: the reusable shell has no dependency on the demo's content or routing state. After integration, check your longest labels, nested destinations and account actions on both a narrow screen and a desktop, using the keyboard as well as the pointer.
Design inspiration
Design reference
The original Shadcnblocks layout brings grouped navigation, a breadcrumb header and an account menu into one application frame. Use it to compare the placement of controls and the balance between navigation and page content. When adapting the Kamod version, start with your own navigation hierarchy and route names, then use the typed navigation example and callback reference above to connect destinations and account actions to your app.
This block adapts the original sidebar, breadcrumb header and account menu using Kamod's Preact components and theme tokens. Your application supplies its routing and account actions.
Map the layout to your own pages with the typed navigation example.
Use the reference to compare the layout and interactions. To add this version to your app, use the source in this page's Code tab and follow the Kamod setup instructions above.