Interactive previewTry it before you copy it

Variant 1 of 1

Live preview and source

Open

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. Skip preview.tsx, demo-data.tsx and assets/kamod-ui-logo.svg unless you want the demo. To keep the demo branding, copy the SVG into the same assets subfolder.

    Keep the reusable files together: application-shell-1.tsx, app-sidebar.tsx, nav-main.tsx, nav-user.tsx, menu.tsx, types.ts and index.ts. Their relative imports work within this folder; the entrypoint exports the component and its public types. The examples assume an importing file at src/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/signals
    Compatibility:
    @kamod-ch/ui must export useDropdown and support the portal prop on DropdownContent, and expose createRovingFocus from @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

Pass your brand, navigation, user and breadcrumbs
, then place your page content inside the shell. Mount it in your app's shared layout so pages can reuse the same navigation. The example below starts with one destination and lets the shell manage its own sidebar state.
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 href by default. With a client router, pass its current path to currentPath and handle ordinary clicks in onNavigate. 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 main landmark 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.

Definitions and field comments below come directly from 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.TypeScript

Component 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.

The red asterisk
marks the four required props: 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.
ApplicationShell1 props, types and descriptions; required props are marked
Prop / typeDescription
brand
ApplicationShellBrand
Sidebar identity, an optional logo and an optional home or workspace link.
navigationGroups
readonly ApplicationShellNavigationGroup[]
Ordered groups of destinations, with at most one level of child links. See Type your navigation data.
user
ApplicationShellUser
Account name and email, with an optional avatar or custom initials. No session is inferred.
breadcrumbs
readonly ApplicationShellDestination[]
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
ComponentChildren
Page content inside the existing main landmark and padded content area. No placeholder content is inserted.
currentPath
string
Exact URL used to match item href values. An item's explicit active value takes precedence; no router or path normalization is applied.
onNavigate
ApplicationShellNavigate
Handles linked brand, breadcrumb and navigation activation, or a leaf action button. Native links still work without it.
onUserAction
(action: ApplicationShellUserAction) => void
Receives an account menu selection. Your app implements the resulting navigation or account operation.
open
boolean
Controls desktop expansion: true expands, false collapses to icons. Update this value in onOpenChange to respond to the toggle. See Sidebar state.
defaultOpen
boolean
Initial uncontrolled desktop state. Ignored when open is supplied; changing it after mount does not reset the sidebar.
onOpenChange
(open: boolean) => void
Reports requested desktop expansion in either state mode. Mobile visibility does not call this callback.
class
string
Additional classes merged onto the outer SidebarProvider wrapper.
className
string
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.

ApplicationShell1Props
All required and optional inputs in one copyable declaration.

Type your navigation data

Use stable IDs
and keep child destinations to one level. Group IDs must be unique among groups, and item IDs among siblings. This example gives Projects its own page and an expandable submenu. Pass the resulting array to 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: /projects and /projects/ differ. Use active when 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.

Required type
marks a type used directly by a required shell prop. The prop must be supplied; 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

Required type
ApplicationShellBrand
The identity shown at the top of the sidebar, including in icon mode.
Required fields
name

Account identity

Required type
ApplicationShellUser
Display data for the sidebar footer and account menu.
Required fields
name, email

Navigation groups

Required type
ApplicationShellNavigationGroup
The outer level of your navigation: an ID, optional heading and ordered items.
Required fields
id, items
src/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.

ApplicationShellNavigationItem
A navigation link that can also contain one level of child links.
Required fields
id, label
ApplicationShellNavigationLink
A destination plus its stable ID, optional icon and interaction state.
Required fields
id, label
ApplicationShellDestination
The shared label and optional URL used by breadcrumbs and navigation callbacks.
Required fields
label
ApplicationShellIcon
A Preact component that accepts the shell's SVG styling and accessibility props.

Branch 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

Native links need no callback. For client routing
, supply onNavigate and cancel only the clicks your router handles. Keep currentPath and breadcrumbs in sync with your router; the shell does not infer either.
ApplicationShellNavigate
Receives a destination and the Preact click event from its link or action button.

This 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.

ApplicationShellUserAction
The exact action identifiers passed to onUserAction, without a click event.

Sidebar state

Uncontrolled Default
Omit open. The desktop sidebar starts expanded; use defaultOpen={false} to start with icons. onOpenChange can observe changes without owning state.
Controlled
Supply open. To let the built-in toggles change it, update this value in onOpenChange; 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
getInitialValueInEffect uses 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's sidebar_state cookie and pass its parsed boolean as defaultOpen on 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.

Everything passed as 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.