# Dashboard 2

> Dashboard 2 variant for block.

A shadcn/ui block from the [@uiable](https://uiable.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiable/block-dashboard-2).

- Page, with a live preview: https://shadcn.exchange/r/uiable/block-dashboard-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiable/block-dashboard-2-light-a09d78d4f024e90d.png
- Registry item: https://uiable.com/r/block-dashboard-2.json
- Categories: dashboard-layout
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uiable.com/r/block-dashboard-2.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: `lucide-react`
- Registry items: `@uiable/button`, `@uiable/collapsible`, `@uiable/dropdown-menu`, `@uiable/input`, `@uiable/logo`, `@uiable/notification-dropdown`, `@uiable/search-bar`, `@uiable/sheet`, `@uiable/user-profile`

## Files

### `src/components/uiable/blocks/layout/dashboard-layout/dashboard-2.tsx`

```tsx
"use client"

// react
import { type ReactNode, useState } from "react"

// next
import Link from "next/link"
import { usePathname } from "next/navigation"

// shadcn
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

// project-imports
import {
  hasActiveDescendant,
  isNavItemActive,
  menuGroups,
  type NavItem,
} from "@/components/uiable/blocks/layout/dashboard-layout/data/dashboard-2.data"
import NotificationDropdown from "@/components/uiable/layout/notification-dropdown"
import SearchBar from "@/components/uiable/layout/search-bar"
import Logo from "@/components/uiable/layout/shared/logo"
import UserProfile from "@/components/uiable/layout/user-profile"

// assets
import {
  ChevronDown,
  ChevronRight,
  Menu,
  Search,
  Settings,
  X,
} from "lucide-react"

const navItemClass = "gap-2 rounded-lg px-3.5 py-2 font-medium"
const navItemActiveClass =
  "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
const navItemInactiveClass = "text-muted-foreground hover:text-foreground"

// The top-nav layout renders each menu group as a top-level entry, so its label
// becomes the trigger and its items become the dropdown children.
const topNavItems: NavItem[] = menuGroups.map((group) => ({
  id: group.id,
  title: group.label,
  icon: group.icon,
  children: group.items,
}))

//  ------------------------------ | TOP NAV ITEM | ------------------------------  //

function TopNavItem({
  item,
  className,
  onNavigate,
}: {
  item: NavItem
  className?: string
  onNavigate?: () => void
}) {
  const pathname = usePathname()
  const Icon = item.icon
  const isActive = isNavItemActive(pathname, item.href)
  return (
    <Button
      variant="ghost"
      nativeButton={false}
      className={`${navItemClass} ${
        isActive ? navItemActiveClass : navItemInactiveClass
      } ${className ?? ""}`}
      render={
        <Link
          href={item.href ?? "#"}
          target={item.target}
          rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
          onClick={onNavigate}
        />
      }
    >
      {Icon ? <Icon className="size-4.5" /> : null}
      <span>{item.title}</span>
    </Button>
  )
}

//  ------------------------------ | TOP NAV DROPDOWN NODE | ------------------------------  //

// Renders a single entry inside a dropdown: a leaf becomes a link item, a parent
// nests another submenu recursively so menus can go arbitrarily deep.
function TopNavMenuNode({ node }: { node: NavItem }) {
  const pathname = usePathname()

  if (!node.children?.length) {
    return (
      <DropdownMenuItem
        data-active={isNavItemActive(pathname, node.href)}
        className="data-[active=true]:bg-primary/10 data-[active=true]:text-primary"
        render={
          <Link
            href={node.href ?? "#"}
            target={node.target}
            rel={node.target === "_blank" ? "noopener noreferrer" : undefined}
          />
        }
      >
        <span>{node.title}</span>
      </DropdownMenuItem>
    )
  }

  return (
    <DropdownMenuSub>
      <DropdownMenuSubTrigger
        data-active={hasActiveDescendant(node, pathname)}
        className="data-[active=true]:text-primary"
      >
        <span>{node.title}</span>
      </DropdownMenuSubTrigger>
      <DropdownMenuSubContent>
        {node.children.map((child) => (
          <TopNavMenuNode key={child.id} node={child} />
        ))}
      </DropdownMenuSubContent>
    </DropdownMenuSub>
  )
}

//  ------------------------------ | TOP NAV DROPDOWN | ------------------------------  //

function TopNavDropdown({ item }: { item: NavItem }) {
  const pathname = usePathname()
  const Icon = item.icon
  const isActive = hasActiveDescendant(item, pathname)
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={
          <Button
            variant="ghost"
            className={`${navItemClass} ${
              isActive ? navItemActiveClass : navItemInactiveClass
            }`}
          />
        }
      >
        {Icon ? <Icon className="size-4.5" /> : null}
        <span>{item.title}</span>
        <ChevronDown className="size-4" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="min-w-44 p-2">
        {item.children?.map((child) => (
          <TopNavMenuNode key={child.id} node={child} />
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

//  ------------------------------ | TOP NAV | ------------------------------  //

function TopNav() {
  return (
    <nav className="ml-4 hidden items-center gap-1 xl:flex">
      {topNavItems.map((item) =>
        item.children?.length ? (
          <TopNavDropdown key={item.id} item={item} />
        ) : (
          <TopNavItem key={item.id} item={item} />
        )
      )}
    </nav>
  )
}

//  ------------------------------ | MOBILE NAV NODE | ------------------------------  //

// Mirrors the desktop dropdown as a nested collapsible tree so touch users get
// the same multi-level menu. Parents expand when a descendant route is active.
function MobileNavNode({
  node,
  onNavigate,
}: {
  node: NavItem
  onNavigate: () => void
}) {
  const pathname = usePathname()

  if (!node.children?.length) {
    return (
      <TopNavItem
        item={node}
        className="w-full justify-start"
        onNavigate={onNavigate}
      />
    )
  }

  const Icon = node.icon
  return (
    <Collapsible
      className="group/collapsible"
      defaultOpen={hasActiveDescendant(node, pathname)}
    >
      <CollapsibleTrigger
        nativeButton={false}
        render={
          <Button
            variant="ghost"
            className={`w-full justify-start ${navItemClass} ${
              hasActiveDescendant(node, pathname)
                ? navItemActiveClass
                : navItemInactiveClass
            }`}
          />
        }
      >
        {Icon ? <Icon className="size-4.5" /> : null}
        <span>{node.title}</span>
        <ChevronRight className="ml-auto size-4 transition-transform group-data-open/collapsible:rotate-90" />
      </CollapsibleTrigger>
      <CollapsibleContent className="h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-300 ease-out data-ending-style:h-0 data-starting-style:h-0">
        <div className="mt-1 ml-4 flex flex-col gap-1 border-l border-border/50 pl-2">
          {node.children.map((child) => (
            <MobileNavNode
              key={child.id}
              node={child}
              onNavigate={onNavigate}
            />
          ))}
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}

//  ------------------------------ | MOBILE NAV | ------------------------------  //

function MobileNav() {
  const [open, setOpen] = useState(false)
  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-primary xl:hidden">
        <Menu className="size-5" />
        <span className="sr-only">Toggle navigation</span>
      </SheetTrigger>
      <SheetContent side="left" className="w-72 gap-0 overflow-y-auto p-0">
        <SheetHeader className="border-b border-border/50 px-4 py-3">
          <SheetTitle className="sr-only">Navigation</SheetTitle>
          <Logo link={false} />
        </SheetHeader>
        <nav className="flex flex-col gap-1 p-3">
          {topNavItems.map((item) => (
            <MobileNavNode
              key={item.id}
              node={item}
              onNavigate={() => setOpen(false)}
            />
          ))}
        </nav>
      </SheetContent>
    </Sheet>
  )
}

//  ------------------------------ | MOBILE SEARCH | ------------------------------  //

function MobileSearch() {
  return (
    <Sheet>
      <SheetTrigger className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-primary md:hidden">
        <Search className="size-5" />
        <span className="sr-only">Open search</span>
      </SheetTrigger>
      <SheetContent side="top" showCloseButton={false} className="gap-0 p-0">
        <SheetHeader className="px-4 py-3">
          <SheetTitle className="sr-only">Search</SheetTitle>
          <div className="flex items-center gap-2">
            <div className="relative flex-1">
              <Search className="absolute top-3.5 left-3.5 h-4 w-4 text-muted-foreground" />
              <Input
                type="search"
                autoFocus
                placeholder="Search..."
                className="block h-auto w-full rounded-xl border border-border bg-input py-[.66rem] pr-3 pl-10 text-base placeholder:text-[#bec8d0] focus:border-primary focus:outline-none dark:focus:border-primary"
              />
            </div>
            <SheetClose className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
              <X className="size-5" />
              <span className="sr-only">Close search</span>
            </SheetClose>
          </div>
        </SheetHeader>
      </SheetContent>
    </Sheet>
  )
}

//  ------------------------------ | DASHBOARD HEADER | ------------------------------  //

function DashboardHeader() {
  return (
    <header className="flex h-16 shrink-0 items-center gap-2 border-b border-border/50 bg-background/80 px-4 backdrop-blur-md sm:gap-4 sm:px-6">
      <MobileNav />

      <Logo link={false} />

      <TopNav />

      <div className="flex-1" />

      <div className="-mr-1 flex items-center gap-2 sm:gap-4">
        <SearchBar />
        <MobileSearch />
        <Button
          variant="ghost"
          className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-muted-foreground transition-colors hover:bg-muted hover:text-primary"
        >
          <Settings className="size-5" />
        </Button>
        <div className="mx-1 hidden h-4 w-px bg-border sm:block" />
        <NotificationDropdown />
        <div className="mx-1 hidden h-4 w-px bg-border sm:block" />
        <UserProfile />
      </div>
    </header>
  )
}

//  ------------------------------ | DASHBOARD CONTENT | ------------------------------  //

function DashboardContent() {
  const pathname = usePathname()
  const activeItem =
    topNavItems.find((item) => hasActiveDescendant(item, pathname)) ??
    topNavItems[0]

  return (
    <div className="flex-1 overflow-y-auto">
      <div className="flex h-full min-h-0 flex-col gap-6 p-4 sm:p-6">
        <div className="flex flex-col gap-1">
          <h4 className="mb-0 capitalize">{activeItem.title}</h4>
          <p className="text-sm text-muted-foreground">
            Welcome back, here is what is happening today.
          </p>
        </div>

        {/* Stat cards */}
        <div className="grid auto-rows-min gap-4 sm:grid-cols-2 lg:grid-cols-4">
          <div className="h-24 rounded-xl bg-card" />
          <div className="h-24 rounded-xl bg-card" />
          <div className="h-24 rounded-xl bg-card" />
          <div className="h-24 rounded-xl bg-card" />
        </div>

        {/* Main + side panels */}
        <div className="grid flex-1 gap-4 lg:grid-cols-3">
          <div className="min-h-64 rounded-xl bg-card lg:col-span-2" />
          <div className="min-h-64 rounded-xl bg-card" />
        </div>
      </div>
    </div>
  )
}

//  ------------------------------ | BLOCK - DASHBOARD LAYOUT 2 (TOP NAV) | ------------------------------  //

export default function DashboardTwo({ children }: { children?: ReactNode }) {
  return (
    <div className="relative flex h-screen w-full flex-col overflow-hidden bg-background [.in-iframe-preview_&]:h-[600px]">
      <DashboardHeader />
      {children ? (
        <main className="flex-1 overflow-y-auto p-4 sm:p-6">{children}</main>
      ) : (
        <DashboardContent />
      )}
    </div>
  )
}
```

### `src/components/uiable/blocks/layout/dashboard-layout/data/dashboard-2.data.ts`

```ts
// assets
import {
  Bell,
  BookOpen,
  ChartColumn,
  CircleX,
  Clock,
  FileText,
  Key,
  LayoutDashboard,
  Lock,
  type LucideIcon,
  Mail,
  Network,
  LayoutGrid,
  PanelLeft,
  PanelTop,
  Shield,
  ShieldCheck,
  TriangleAlert,
  UserPlus,
  Users,
  Wrench,
} from "lucide-react"

//  ------------------------------ | TYPES | ------------------------------  //

// A nav node is either a leaf (has `href`) or a parent (has `children`). Icons
// and targets are optional so nested levels can render as plain entries.
export type NavItem = {
  id: string
  title: string
  icon?: LucideIcon
  href?: string
  target?: "_blank" | "_self" | "_parent" | "_top"
  children?: NavItem[]
}

// A labelled section of the menu. The sidebar layout renders the label as a
// group heading; the top-nav layout renders it as a top-level dropdown trigger.
export type NavGroup = {
  id: string
  label: string
  icon?: LucideIcon
  items: NavItem[]
}

//  ------------------------------ | ACTIVE MENU-ITEM LOGIC | ------------------------------  //

// A link is active when the current pathname is the link itself or a nested
// route below it (e.g. `/menu-level/2-2/3-1` keeps "Level 2.2" active).
export function isNavItemActive(pathname: string, href?: string): boolean {
  if (!href) return false
  return pathname === href || pathname.startsWith(`${href}/`)
}

// A parent node is active/open when any descendant leaf matches the current
// route, so dropdown triggers and collapsibles can highlight and expand.
export function hasActiveDescendant(item: NavItem, pathname: string): boolean {
  if (isNavItemActive(pathname, item.href)) return true
  return (item.children ?? []).some((child) =>
    hasActiveDescendant(child, pathname)
  )
}

//  ------------------------------ | MENU GROUPS | ------------------------------  //

// Single source of truth for the dashboard navigation, shared by both the
// sidebar layout (dashboard-1) and the top-nav layout (dashboard-2).
export const menuGroups: NavGroup[] = [
  {
    id: "dashboard",
    label: "Dashboard",
    icon: LayoutDashboard,
    items: [
      {
        id: "dashboard",
        title: "Default",
        icon: LayoutDashboard,
        href: "/dashboard/default",
      },
      {
        id: "finance",
        title: "Finance",
        icon: ChartColumn,
        href: "/dashboard/finance",
      },
      {
        id: "lms",
        title: "LMS",
        icon: BookOpen,
        href: "/dashboard/lms",
      },
    ],
  },
  {
    id: "authentication",
    label: "Authentication",
    icon: Shield,
    items: [
      { id: "login", title: "Login", icon: Shield, href: "/login" },
      { id: "register", title: "Register", icon: UserPlus, href: "/register" },
      {
        id: "forgot-password",
        title: "Forgot Password",
        icon: Key,
        href: "/forgot-password",
      },
      {
        id: "reset-password",
        title: "Reset Password",
        icon: Lock,
        href: "/reset-password",
      },
      {
        id: "check-mail",
        title: "Check Mail",
        icon: Mail,
        href: "/check-mail",
      },
      {
        id: "code-verification",
        title: "Code Verification",
        icon: ShieldCheck,
        href: "/code-verification",
      },
    ].map((item) => ({ ...item, target: "_blank" as const })),
  },
  {
    id: "layout",
    label: "Layout",
    icon: PanelTop,
    items: [
      {
        id: "vertical-layout",
        title: "Vertical",
        icon: PanelLeft,
        href: "/layout/vertical",
      },
      {
        id: "horizontal-layout",
        title: "Horizontal",
        icon: PanelTop,
        href: "/layout/horizontal",
      },
      {
        id: "icon-rail-layout",
        title: "Icon Rail Split",
        icon: LayoutGrid,
        href: "/layout/icon-rail",
      },
    ],
  },
  {
    id: "maintenance",
    label: "Maintenance",
    icon: Wrench,
    items: [
      {
        id: "error-404",
        title: "Error 404",
        icon: CircleX,
        href: "/error-404",
      },
      {
        id: "error-500",
        title: "Error 500",
        icon: TriangleAlert,
        href: "/error-500",
      },
      {
        id: "coming-soon",
        title: "Coming Soon",
        icon: Clock,
        href: "/coming-soon",
      },
      {
        id: "under-construction",
        title: "Under Construction",
        icon: Wrench,
        href: "/under-construction",
      },
      {
        id: "join-waitlist",
        title: "Join Waitlist",
        icon: Users,
        href: "/join-waitlist",
      },
    ].map((item) => ({ ...item, target: "_blank" as const })),
  },
  {
    id: "extra",
    label: "Extra",
    icon: Bell,
    items: [
      {
        id: "docs",
        title: "Documentation",
        icon: FileText,
        href: "https://uiable.com/doc/admin-dashboard",
        target: "_blank",
      },
      // Leaf nodes carry a unique `href`; parent nodes derive their open state
      // from whether any descendant leaf matches the current route.
      {
        id: "menu-levels",
        title: "Menu levels",
        icon: Network,
        children: [
          {
            id: "level-2-1",
            title: "Level 2.1",
            href: "#",
          },
          {
            id: "level-2-2",
            title: "Level 2.2",
            children: [
              {
                id: "level-2-2-3-1",
                title: "Level 3.1",
                href: "#",
              },
              {
                id: "level-2-2-3-2",
                title: "Level 3.2",
                href: "#",
              },
              {
                id: "level-2-2-3-3",
                title: "Level 3.3",
                children: [
                  {
                    id: "level-2-2-3-3-4-1",
                    title: "Level 4.1",
                    href: "#",
                  },
                  {
                    id: "level-2-2-3-3-4-2",
                    title: "Level 4.2",
                    href: "#",
                  },
                ],
              },
            ],
          },
          {
            id: "level-2-3",
            title: "Level 2.3",
            children: [
              {
                id: "level-2-3-3-1",
                title: "Level 3.1",
                href: "#",
              },
              {
                id: "level-2-3-3-2",
                title: "Level 3.2",
                href: "#",
              },
              {
                id: "level-2-3-3-3",
                title: "Level 3.3",
                children: [
                  {
                    id: "level-2-3-3-3-4-1",
                    title: "Level 4.1",
                    href: "#",
                  },
                  {
                    id: "level-2-3-3-3-4-2",
                    title: "Level 4.2",
                    href: "#",
                  },
                ],
              },
            ],
          },
        ],
      },
      { id: "sample", title: "Sample Page", icon: Bell, href: "/sample-page" },
    ],
  },
]
```
