# App shell 5

> Sidebar workspace with command-style search, custom trigger and theme controls, navbar strip, and latest-change row.

A shadcn/ui block from the [@efferd](https://efferd.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/efferd/app-shell-5).

- Page, with a live preview: https://shadcn.exchange/r/efferd/app-shell-5
- Screenshot: https://shadcn.exchange/shot/efferd/app-shell-5/light.png
- Registry item: https://efferd.com/r/app-shell-5.json
- Categories: app-shell
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/efferd/app-shell-5.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `next-themes`
- Registry items: `@efferd/use-keypress`, `avatar`, `breadcrumb`, `button`, `dropdown-menu`, `input-group`, `kbd`, `separator`, `sidebar`, `tooltip`, `@efferd/logo`

## Files

### `registry/blocks/app-shell/5/app-header.tsx`

```tsx
import {
	Breadcrumb,
	BreadcrumbItem,
	BreadcrumbList,
	BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { navLinks } from "@/components/app-shared";
import { CustomTrigger } from "@/components/custom-trigger";
import { NavUser } from "@/components/nav-user";

export function AppHeader() {
	const activeItem = navLinks.find((item) => item.isActive);

	return (
		<header className="sticky top-0 z-50 flex h-(--app-header-height) w-full shrink-0 items-center justify-between gap-2 border-b bg-background px-4 md:px-6">
			<div className="flex items-center gap-3">
				<CustomTrigger place="navbar" />
			</div>
			<Breadcrumb>
				<BreadcrumbList>
					<BreadcrumbItem>
						<BreadcrumbPage>{activeItem?.title}</BreadcrumbPage>
					</BreadcrumbItem>
				</BreadcrumbList>
			</Breadcrumb>{" "}
			<div className="flex items-center gap-3">
				<Button size="icon-sm" variant="outline">
					<IconPlaceholder
						hugeicons="HelpCircleIcon"
						lucide="HelpCircleIcon"
						phosphor="QuestionIcon"
						remixicon="RiQuestionLine"
						tabler="IconHelpCircle"
					/>
				</Button>
				<Button aria-label="Notifications" size="icon-sm" variant="outline">
					<IconPlaceholder
						hugeicons="Notification03Icon"
						lucide="BellIcon"
						phosphor="BellIcon"
						remixicon="RiNotification3Line"
						tabler="IconBell"
					/>
				</Button>
				<Separator
					className="h-4 data-[orientation=vertical]:self-center"
					orientation="vertical"
				/>
				<NavUser />
			</div>
		</header>
	);
}
```

### `registry/blocks/app-shell/5/app-shared.tsx`

```tsx
import type { ReactNode } from "react";

export type SidebarNavItem = {
	title: string;
	path?: string;
	icon?: ReactNode;
	isActive?: boolean;
	subItems?: SidebarNavItem[];
};

export type SidebarNavGroup = {
	label: string;
	items: SidebarNavItem[];
};

export const navGroups: SidebarNavGroup[] = [
	{
		label: "Explore",
		items: [
			{
				title: "Dashboard",
				path: "#/overview",
				icon: (
					<IconPlaceholder
						hugeicons="DashboardSquare01Icon"
						lucide="LayoutDashboardIcon"
						phosphor="SquaresFourIcon"
						remixicon="RiDashboardLine"
						tabler="IconLayoutDashboard"
					/>
				),
				isActive: true,
			},
			{
				title: "Events",
				path: "#/events",
				icon: (
					<IconPlaceholder
						hugeicons="Cursor02Icon"
						lucide="MousePointerClickIcon"
						phosphor="CursorClickIcon"
						remixicon="RiCursorLine"
						tabler="IconPointer"
					/>
				),
			},
			{
				title: "Funnels",
				path: "#/funnels",
				icon: (
					<IconPlaceholder
						hugeicons="FilterIcon"
						lucide="FunnelIcon"
						phosphor="FunnelIcon"
						remixicon="RiFilterLine"
						tabler="IconChartFunnel"
					/>
				),
			},
			{
				title: "Retention",
				path: "#/retention",
				icon: (
					<IconPlaceholder
						hugeicons="RepeatIcon"
						lucide="RepeatIcon"
						phosphor="RepeatIcon"
						remixicon="RiRepeatLine"
						tabler="IconRepeat"
					/>
				),
			},
			{
				title: "Flows",
				path: "#/flows",
				icon: (
					<IconPlaceholder
						hugeicons="GitBranchIcon"
						lucide="GitBranchIcon"
						phosphor="GitBranchIcon"
						remixicon="RiGitBranchLine"
						tabler="IconGitBranch"
					/>
				),
			},
		],
	},
	{
		label: "Audiences",
		items: [
			{
				title: "Segments",
				path: "#/segments",
				icon: (
					<IconPlaceholder
						hugeicons="UserGroupIcon"
						lucide="UsersIcon"
						phosphor="UsersThreeIcon"
						remixicon="RiGroupLine"
						tabler="IconUsersGroup"
					/>
				),
			},
			{
				title: "Cohorts",
				path: "#/cohorts",
				icon: (
					<IconPlaceholder
						hugeicons="PieChartIcon"
						lucide="ChartPieIcon"
						phosphor="ChartPieIcon"
						remixicon="RiPieChartLine"
						tabler="IconChartPie"
					/>
				),
			},
			{
				title: "Profiles",
				path: "#/profiles",
				icon: (
					<IconPlaceholder
						hugeicons="UserIcon"
						lucide="UserIcon"
						phosphor="UserIcon"
						remixicon="RiUserLine"
						tabler="IconUser"
					/>
				),
			},
		],
	},
	{
		label: "Configure",
		items: [
			{
				title: "Integrations",
				path: "#/integrations",
				icon: (
					<IconPlaceholder
						hugeicons="Plug01Icon"
						lucide="PlugIcon"
						phosphor="PlugIcon"
						remixicon="RiPlugLine"
						tabler="IconPlug"
					/>
				),
			},
		],
	},
];

export const navLinks: SidebarNavItem[] = [
	...navGroups.flatMap((group) =>
		group.items.flatMap((item) =>
			item.subItems?.length ? [item, ...item.subItems] : [item]
		)
	),
];
```

### `registry/blocks/app-shell/5/app-search.tsx`

```tsx
"use client";

import { useRef } from "react";
import { useKeypress } from "@/hooks/use-keypress";
import {
	InputGroup,
	InputGroupAddon,
	InputGroupInput,
} from "@/components/ui/input-group";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import { useSidebar } from "@/components/ui/sidebar";

export function AppSearch() {
	const groupRef = useRef<HTMLDivElement>(null);
	const { setOpen } = useSidebar();

	useKeypress({
		combo: ["meta+k", "ctrl+k"],
		callback: () => {
			const input = groupRef.current?.querySelector<HTMLInputElement>(
				"[data-slot=input-group-control]"
			);
			input?.focus({ preventScroll: true });
			setOpen(true);
		},
	});

	return (
		<InputGroup ref={groupRef}>
			<InputGroupAddon align="inline-start" className="pl-1.75">
				<IconPlaceholder
					hugeicons="SearchIcon"
					lucide="SearchIcon"
					phosphor="MagnifyingGlassIcon"
					remixicon="RiSearchLine"
					tabler="IconSearch"
				/>
			</InputGroupAddon>
			<InputGroupInput
				aria-label="Search"
				name="q"
				placeholder="Search..."
				type="search"
			/>
			<InputGroupAddon align="inline-end">
				<KbdGroup>
					<Kbd>⌘</Kbd>
					<Kbd>K</Kbd>
				</KbdGroup>
			</InputGroupAddon>
		</InputGroup>
	);
}
```

### `registry/blocks/app-shell/5/app-sidebar.tsx`

```tsx
"use client";

import { cn } from "@/lib/utils";
import { LogoIcon } from "@/components/logo";
import { Button } from "@/components/ui/button";
import {
	Sidebar,
	SidebarContent,
	SidebarFooter,
	SidebarGroup,
	SidebarGroupLabel,
	SidebarHeader,
	SidebarMenu,
	SidebarMenuButton,
	SidebarMenuItem,
	SidebarRail,
} from "@/components/ui/sidebar";
import { AppSearch } from "@/components/app-search";
import { navGroups } from "@/components/app-shared";
import { CustomTrigger } from "@/components/custom-trigger";
import { LatestChange } from "@/components/latest-change";
import { ThemeSwitcher } from "@/components/theme-switcher";

export function AppSidebar() {
	return (
		<Sidebar
			className={cn(
				"*:data-[slot=sidebar-inner]:bg-background",
				"transition-[left,right,top,width] group-data-[collapsible=offcanvas]:top-[calc(var(--app-header-height)*0.5)]"
			)}
			collapsible="offcanvas"
			variant="sidebar"
		>
			<SidebarHeader className="h-(--app-header-height,3rem) flex-row items-center justify-between">
				<Button asChild variant="ghost">
					<a href="#link">
						<LogoIcon />
						<span className="font-medium">Efferd</span>
					</a>
				</Button>
				<CustomTrigger place="sidebar" />
			</SidebarHeader>
			<SidebarContent>
				<SidebarGroup>
					<AppSearch />
				</SidebarGroup>
				{navGroups.map((group) => (
					<SidebarGroup key={group.label}>
						<SidebarGroupLabel className="group-data-[collapsible=icon]:pointer-events-none">
							{group.label}
						</SidebarGroupLabel>
						<SidebarMenu>
							{group.items.map((item) => (
								<SidebarMenuItem key={item.title}>
									<SidebarMenuButton
										asChild
										isActive={item.isActive}
										tooltip={item.title}
									>
										<a href={item.path}>
											{item.icon}
											<span>{item.title}</span>
										</a>
									</SidebarMenuButton>
								</SidebarMenuItem>
							))}
						</SidebarMenu>
					</SidebarGroup>
				))}
			</SidebarContent>
			<SidebarFooter className="px-4">
				<LatestChange />
				<div className="flex items-center pt-4 pb-2">
					<ThemeSwitcher />
					<Button
						asChild
						className="text-muted-foreground"
						size="icon-sm"
						variant="ghost"
					>
						<a aria-label="Settings" href="#">
							<IconPlaceholder
								hugeicons="Settings01Icon"
								lucide="SettingsIcon"
								phosphor="GearIcon"
								remixicon="RiSettings3Line"
								tabler="IconSettings"
							/>
						</a>
					</Button>
				</div>
			</SidebarFooter>
			<SidebarRail />
		</Sidebar>
	);
}
```

### `registry/blocks/app-shell/5/custom-trigger.tsx`

```tsx
"use client";
import { cn } from "@/lib/utils";
import { useIsMobile } from "@/hooks/use-mobile";
import { SidebarTrigger, useSidebar } from "@/components/ui/sidebar";

type Place = "sidebar" | "navbar";

export function CustomTrigger({ place }: { place: Place }) {
	const isMobile = useIsMobile();
	const { open, openMobile } = useSidebar();
	const sidebarOpen = isMobile ? openMobile : open;

	return (
		<SidebarTrigger
			className={cn(
				"transition-opacity duration-0 ease-out motion-reduce:transition-none",
				!sidebarOpen &&
					place === "navbar" &&
					"0fill-mode-forwards delay-100 duration-300",
				sidebarOpen && place === "navbar" && "pointer-events-none opacity-0",
				!sidebarOpen && place === "sidebar" && "pointer-events-none opacity-0"
			)}
		/>
	);
}
```

### `registry/blocks/app-shell/5/app-shell.tsx`

```tsx
import { cn } from "@/lib/utils";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { AppHeader } from "@/components/app-header";
import { AppSidebar } from "@/components/app-sidebar";

export function AppShell({ children }: { children: React.ReactNode }) {
	return (
		<SidebarProvider
			className={cn(
				"[--app-wrapper-max-width:80rem]",
				"[--app-header-height:3rem]"
			)}
		>
			<AppSidebar />
			<SidebarInset className="bg-muted dark:bg-background">
				<AppHeader />
				<div
					className={cn(
						"flex flex-1 flex-col p-4 md:p-6",
						"mx-auto w-full max-w-(--app-wrapper-max-width)"
					)}
				>
					{children}
				</div>
			</SidebarInset>
		</SidebarProvider>
	);
}
```

### `registry/blocks/app-shell/5/latest-change.tsx`

```tsx
"use client";

import { cn } from "@/lib/utils";
import { useState } from "react";
import { Button } from "@/components/ui/button";

const latestChange = {
	title: "Real-time funnels",
	description:
		"Build and analyze conversion funnels with live event streaming in real-time.",
	url: "#",
} as const;

export function LatestChange() {
	const [isOpen, setIsOpen] = useState(true);

	if (!isOpen) {
		return null;
	}

	return (
		<div
			className={cn(
				"rounded-lg group/latest-change z-20 size-full justify-center border bg-background"
			)}
		>
			<div className="relative flex size-full flex-col gap-1.5 overflow-hidden px-3 pt-4 pb-1">
				<p className="font-heading font-medium text-xs">{latestChange.title}</p>
				<span className="text-[10px] text-muted-foreground">
					{latestChange.description}
				</span>
				<Button
					asChild
					className="w-max px-0 font-light text-xs"
					size="sm"
					variant="link"
				>
					<a href={latestChange.url}>Read more</a>
				</Button>
				<Button
					className="absolute top-2 right-2 z-10 size-6 rounded-full opacity-0 transition-opacity group-hover/latest-change:opacity-100"
					onClick={() => setIsOpen(false)}
					size="icon-sm"
					variant="secondary"
				>
					<IconPlaceholder
						className="size-3.5 text-muted-foreground"
						hugeicons="Cancel01Icon"
						lucide="XIcon"
						phosphor="XIcon"
						remixicon="RiCloseLine"
						tabler="IconX"
					/>
				</Button>
			</div>
		</div>
	);
}
```

### `registry/blocks/app-shell/5/nav-user.tsx`

```tsx
"use client";

import {
	Avatar,
	AvatarFallback,
	AvatarImage,
} from "@/components/ui/avatar";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuSeparator,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

const user = {
	name: "Shaban Haider",
	email: "shaban@efferd.com",
	avatar: "https://github.com/shabanhr.png",
};

export function NavUser() {
	return (
		<DropdownMenu>
			<DropdownMenuTrigger asChild>
				<Avatar className="size-8">
					<AvatarImage src={user.avatar} />
					<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
				</Avatar>
			</DropdownMenuTrigger>
			<DropdownMenuContent align="end" className="w-60">
				<DropdownMenuGroup>
					<DropdownMenuLabel className="pointer-events-none flex items-center gap-2">
						<Avatar className="isolate size-8">
							<AvatarImage src={user.avatar} />
							<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
						</Avatar>
						<div>
							<span className="font-medium text-foreground">{user.name}</span>{" "}
							<br />
							<div className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground text-xs">
								{user.email}
							</div>
						</div>
					</DropdownMenuLabel>
				</DropdownMenuGroup>
				<DropdownMenuSeparator />
				<DropdownMenuGroup>
					<DropdownMenuItem>
						<IconPlaceholder
							hugeicons="UserMultipleIcon"
							lucide="UserIcon"
							phosphor="UserIcon"
							remixicon="RiUserLine"
							tabler="IconUser"
						/>
						Account
					</DropdownMenuItem>
					<DropdownMenuItem>
						<IconPlaceholder
							hugeicons="Settings01Icon"
							lucide="SettingsIcon"
							phosphor="GearIcon"
							remixicon="RiSettings3Line"
							tabler="IconSettings"
						/>
						Settings
					</DropdownMenuItem>
				</DropdownMenuGroup>
				<DropdownMenuSeparator />
				<DropdownMenuGroup>
					<DropdownMenuItem>
						<IconPlaceholder
							hugeicons="CreditCardIcon"
							lucide="CreditCardIcon"
							phosphor="CreditCardIcon"
							remixicon="RiBankCardLine"
							tabler="IconCreditCard"
						/>
						Plan & Billing
					</DropdownMenuItem>
				</DropdownMenuGroup>
				<DropdownMenuSeparator />
				<DropdownMenuGroup>
					<DropdownMenuItem variant="destructive">
						<IconPlaceholder
							hugeicons="ArrowLeft01Icon"
							lucide="LogOutIcon"
							phosphor="CaretLeftIcon"
							remixicon="RiArrowLeftSLine"
							tabler="IconChevronLeft"
						/>
						Log out
					</DropdownMenuItem>
				</DropdownMenuGroup>
			</DropdownMenuContent>
		</DropdownMenu>
	);
}
```

### `registry/blocks/app-shell/5/theme-switcher.tsx`

```tsx
"use client";

import { useTheme } from "next-themes";
import * as React from "react";
import { Button } from "@/components/ui/button";

export function ThemeSwitcher() {
	const { setTheme, resolvedTheme } = useTheme();

	const toggleTheme = React.useCallback(() => {
		setTheme(resolvedTheme === "dark" ? "light" : "dark");
	}, [resolvedTheme, setTheme]);

	return (
		<Button
			className="text-muted-foreground"
			onClick={toggleTheme}
			size="icon-sm"
			variant="ghost"
		>
			<svg
				className="size-4.5"
				fill="none"
				height="24"
				stroke="currentColor"
				strokeLinecap="round"
				strokeLinejoin="round"
				strokeWidth="2"
				viewBox="0 0 24 24"
				width="24"
				xmlns="http://www.w3.org/2000/svg"
			>
				<path d="M0 0h24v24H0z" fill="none" stroke="none" />
				<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
				<path d="M12 3l0 18" />
				<path d="M12 9l4.65 -4.65" />
				<path d="M12 14.3l7.37 -7.37" />
				<path d="M12 19.6l8.85 -8.85" />
			</svg>
			<span className="sr-only">Toggle theme</span>
		</Button>
	);
}
```
