# Dashboard 3

> Dashboard 3 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-3).

- Page, with a live preview: https://shadcn.exchange/r/uiable/block-dashboard-3
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiable/block-dashboard-3-light-358422dc68f938c3.png
- Registry item: https://uiable.com/r/block-dashboard-3.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-3.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`, `next-themes`
- Registry items: `@uiable/button`, `@uiable/collapsible`, `@uiable/logo`, `@uiable/notification-dropdown`, `@uiable/search-bar`, `@uiable/sheet`, `@uiable/tooltip`, `@uiable/user-profile`

## Files

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

```tsx
"use client"

// react
import { type ReactNode, useState, useSyncExternalStore } 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 {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"

// third-party
import { useTheme } from "next-themes"

// project-imports
import branding from "@/branding.json"
import {
  hasActiveDescendant,
  isNavItemActive,
  menuGroups,
  type NavItem,
} from "@/components/uiable/blocks/layout/dashboard-layout/data/dashboard-3.data"
// import { LayoutSwitcher } from "@/components/uiable/layout/layout-preference"
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 {
  ChevronRight,
  LifeBuoy,
  Moon,
  PanelLeft,
  Pin,
  PinOff,
  Settings,
  Sun,
} from "lucide-react"

// assets
const brandFavLogo = "https://cdn.uiable.com/brand/favicon.svg"

// Map each menu group to a rail item: the group icon appears in the icon rail,
// and the group's items populate the secondary panel when selected.
const railItems: NavItem[] = menuGroups.map((group) => ({
  id: group.id,
  title: group.label,
  icon: group.icon,
  children: group.items,
}))

const railItemClass = "size-11 rounded-lg"
const railItemActiveClass =
  "bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
const railItemInactiveClass = "text-muted-foreground hover:text-foreground"

const panelLinkClass =
  "justify-start rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors"
const panelLinkActiveClass = "bg-primary/10 text-primary"
const panelLinkInactiveClass =
  "text-muted-foreground hover:bg-muted hover:text-foreground"

//  ------------------------------ | ICON RAIL | ------------------------------  //

function IconRail({
  activeId,
  onSelect,
  panelOpen,
  onTogglePanel,
}: {
  activeId: string
  onSelect: (item: NavItem) => void
  panelOpen: boolean
  onTogglePanel: () => void
}) {
  return (
    <aside className="flex w-16 shrink-0 flex-col items-center border-r border-border/50 bg-background/80">
      {/* Below xl the full logo lives in the secondary panel, so show the
          compact mark here. At xl+ that space becomes the panel toggle. */}
      <div className="flex h-16 w-full shrink-0 items-center justify-center border-b border-border/50">
        <img
          src={brandFavLogo}
          alt={`${branding.brandName} Logo`}
          className="size-8 xl:hidden"
        />
        <Tooltip>
          <TooltipTrigger
            render={
              <Button
                variant="ghost"
                size="icon"
                onClick={onTogglePanel}
                aria-pressed={panelOpen}
                className={`hidden cursor-pointer ${railItemClass} ${railItemInactiveClass} xl:inline-flex`}
              />
            }
          >
            {panelOpen ? (
              <Pin className="size-5.5" />
            ) : (
              <PinOff className="size-5.5" />
            )}
            <span className="sr-only">
              {panelOpen ? "Collapse menu" : "Expand menu"}
            </span>
          </TooltipTrigger>
          <TooltipContent side="right">
            <p>{panelOpen ? "Collapse menu" : "Expand menu"}</p>
          </TooltipContent>
        </Tooltip>
      </div>

      <nav className="flex flex-1 flex-col items-center gap-1 py-4">
        {railItems.map((item) => {
          const Icon = item.icon
          const isActive = item.id === activeId
          return (
            <Tooltip key={item.id}>
              <TooltipTrigger
                render={
                  <Button
                    variant="ghost"
                    size="icon"
                    onClick={() => onSelect(item)}
                    className={`${railItemClass} ${
                      isActive ? railItemActiveClass : railItemInactiveClass
                    }`}
                  />
                }
              >
                {Icon ? <Icon className="size-5.5" /> : null}
              </TooltipTrigger>
              <TooltipContent side="right">
                <p>{item.title}</p>
              </TooltipContent>
            </Tooltip>
          )
        })}
      </nav>

      <div className="flex flex-col items-center gap-2 pb-4">
        <Tooltip>
          <TooltipTrigger
            render={
              <Button
                variant="ghost"
                size="icon"
                className={`${railItemClass} ${railItemInactiveClass}`}
              />
            }
          >
            <LifeBuoy className="size-5.5" />
          </TooltipTrigger>
          <TooltipContent side="right">
            <p>Support</p>
          </TooltipContent>
        </Tooltip>
        <Button
          variant="ghost"
          size="icon"
          className={`${railItemClass} ${railItemInactiveClass}`}
        >
          <Settings className="size-5.5" />
        </Button>
      </div>
    </aside>
  )
}

//  ------------------------------ | PANEL NAV NODE | ------------------------------  //

// Renders a single nav entry in the secondary panel. Leaf items become links;
// parent items (e.g. "Menu levels") become an inline collapsible section.
function PanelNavNode({
  node,
  activeLinkId,
  onSelect,
}: {
  node: NavItem
  activeLinkId?: string
  onSelect: (link: NavItem) => void
}) {
  const pathname = usePathname()

  if (!node.children?.length) {
    const isActive =
      node.id === activeLinkId || isNavItemActive(pathname, node.href)
    return (
      <Button
        variant="ghost"
        nativeButton={false}
        onClick={() => onSelect(node)}
        className={`${panelLinkClass} ${
          isActive ? panelLinkActiveClass : panelLinkInactiveClass
        }`}
        render={
          <Link
            href={node.href ?? "#"}
            target={node.target}
            rel={node.target === "_blank" ? "noopener noreferrer" : undefined}
          />
        }
      >
        {node.title}
      </Button>
    )
  }

  return (
    <Collapsible
      className="group/collapsible"
      defaultOpen={hasActiveDescendant(node, pathname)}
    >
      <CollapsibleTrigger
        render={
          <Button
            variant="ghost"
            className={`w-full ${panelLinkClass} ${
              hasActiveDescendant(node, pathname)
                ? panelLinkActiveClass
                : panelLinkInactiveClass
            }`}
          />
        }
      >
        <span className="flex-1">{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="ml-3 flex flex-col gap-0.5 border-l border-border/50 pt-0.5 pl-2">
          {node.children.map((child) => (
            <PanelNavNode
              key={child.id}
              node={child}
              activeLinkId={activeLinkId}
              onSelect={onSelect}
            />
          ))}
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}

//  ------------------------------ | PANEL NAV | ------------------------------  //

function PanelNav({
  item,
  activeLinkId,
  onSelect,
}: {
  item: NavItem
  activeLinkId?: string
  onSelect: (link: NavItem) => void
}) {
  return (
    <nav className="flex flex-col gap-1">
      {item.children?.map((node) => (
        <PanelNavNode
          key={node.id}
          node={node}
          activeLinkId={activeLinkId}
          onSelect={onSelect}
        />
      ))}
    </nav>
  )
}

//  ------------------------------ | SECONDARY PANEL | ------------------------------  //

function SecondaryPanel({
  item,
  activeLinkId,
  onSelect,
}: {
  item: NavItem
  activeLinkId?: string
  onSelect: (link: NavItem) => void
}) {
  return (
    <aside className="hidden w-56 shrink-0 flex-col border-r border-border/50 bg-background/40 xl:flex">
      <div className="flex h-16 shrink-0 items-center border-b border-border/50 px-4">
        <Logo link={false} />
      </div>
      <div className="flex flex-1 flex-col overflow-y-auto p-3">
        <h6 className="mb-3 px-2 pt-2 capitalize">{item.title}</h6>
        <PanelNav item={item} activeLinkId={activeLinkId} onSelect={onSelect} />
      </div>
    </aside>
  )
}

//  ------------------------------ | MOBILE PANEL | ------------------------------  //

// The secondary panel is hidden on small screens, so expose its links through a
// slide-in sheet triggered from the header.
function MobilePanel({
  item,
  activeLinkId,
  onSelect,
}: {
  item: NavItem
  activeLinkId?: string
  onSelect: (link: NavItem) => void
}) {
  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">
        <PanelLeft className="size-5" />
        <span className="sr-only">Toggle section menu</span>
      </SheetTrigger>
      {/* Mirror the desktop secondary panel: brand logo header, then the
          active section's heading and links. */}
      <SheetContent
        side="left"
        showCloseButton={false}
        className="gap-0 overflow-y-auto p-0 data-[side=left]:w-64"
      >
        <SheetHeader className="flex h-16 shrink-0 flex-row items-center border-b border-border/50 px-4 py-0">
          <SheetTitle className="sr-only">{item.title}</SheetTitle>
          <Logo link={false} />
        </SheetHeader>
        <div className="flex flex-col p-3">
          <h6 className="mb-3 px-2 pt-2 capitalize">{item.title}</h6>
          <PanelNav
            item={item}
            activeLinkId={activeLinkId}
            onSelect={(link) => {
              onSelect(link)
              setOpen(false)
            }}
          />
        </div>
      </SheetContent>
    </Sheet>
  )
}

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

function DashboardHeader({
  item,
  activeLinkId,
  onSelect,
}: {
  item: NavItem
  activeLinkId?: string
  onSelect: (link: NavItem) => void
}) {
  const { resolvedTheme, setTheme } = useTheme()
  const mounted = useSyncExternalStore(
    () => () => {},
    () => true,
    () => false
  )

  return (
    <header className="flex h-16 shrink-0 items-center gap-1 border-b border-border/50 bg-background/80 px-4 backdrop-blur-md sm:px-6">
      <MobilePanel
        item={item}
        activeLinkId={activeLinkId}
        onSelect={onSelect}
      />
      <SearchBar />
      <div className="flex-1" />
      <div className="flex items-center gap-1 sm:gap-2">
        <Button
          variant="ghost"
          onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
          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"
        >
          {mounted && resolvedTheme === "dark" ? (
            <Sun className="size-5" />
          ) : (
            <Moon className="size-5" />
          )}
        </Button>
        {/* <LayoutSwitcher /> */}
        <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 h-4 w-px bg-border" />
        <NotificationDropdown />
        <div className="mx-1 h-4 w-px bg-border" />
        <UserProfile />
      </div>
    </header>
  )
}

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

function DashboardContent({ activeLink }: { activeLink?: NavItem }) {
  return (
    <div className="flex-1 overflow-y-auto">
      <div className="flex h-full min-h-0 flex-col gap-4 p-4 sm:p-6">
        <h5 className="mb-0">{activeLink?.title}</h5>
        <div className="grid auto-rows-min gap-4 md:grid-cols-3">
          <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>
        <div className="min-h-64 flex-1 rounded-xl bg-card" />
      </div>
    </div>
  )
}

//  ------------------------------ | BLOCK - DASHBOARD LAYOUT 3 (ICON RAIL) | ------------------------------  //

export default function DashboardThree({ children }: { children?: ReactNode }) {
  const pathname = usePathname()
  const [activeRailId, setActiveRailId] = useState(() => {
    for (const item of railItems) {
      if (hasActiveDescendant(item, pathname)) return item.id
    }
    return railItems[0].id
  })
  const [activeLinkId, setActiveLinkId] = useState<string | undefined>(() => {
    const railItem =
      railItems.find((item) => hasActiveDescendant(item, pathname)) ??
      railItems[0]
    return (
      railItem.children?.find((child) => hasActiveDescendant(child, pathname))
        ?.id ?? railItem.children?.[0]?.id
    )
  })
  const [panelOpen, setPanelOpen] = useState(true)

  const activeItem =
    railItems.find((item) => item.id === activeRailId) ?? railItems[0]
  const activeLink =
    activeItem.children?.find((link) => link.id === activeLinkId) ??
    activeItem.children?.[0]

  const selectRail = (item: NavItem) => {
    setActiveRailId(item.id)
    setActiveLinkId(item.children?.[0]?.id)
  }

  return (
    <TooltipProvider>
      <div className="relative flex h-screen w-full overflow-hidden bg-background [.in-iframe-preview_&]:h-[600px]">
        <IconRail
          activeId={activeRailId}
          onSelect={selectRail}
          panelOpen={panelOpen}
          onTogglePanel={() => setPanelOpen((open) => !open)}
        />
        {panelOpen ? (
          <SecondaryPanel
            item={activeItem}
            activeLinkId={activeLink?.id}
            onSelect={(link) => setActiveLinkId(link.id)}
          />
        ) : null}
        <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
          <DashboardHeader
            item={activeItem}
            activeLinkId={activeLink?.id}
            onSelect={(link) => setActiveLinkId(link.id)}
          />
          {children ? (
            <main className="flex-1 overflow-y-auto p-4 sm:p-6">
              {children}
            </main>
          ) : (
            <DashboardContent activeLink={activeLink} />
          )}
        </div>
      </div>
    </TooltipProvider>
  )
}
```

### `src/branding.json`

```json
{
  "brandName": "UIAble",
  "title": "Components to Complete Interfaces",
  "company": {
    "name": "CodedThemes ",
    "url": "https://codedthemes.com/",
    "socialLink": {
      "linkedin": "https://in.linkedin.com/company/codedthemes",
      "x": "https://x.com/uiable",
      "instagram": "#",
      "github": "https://github.com/codedthemes/uiable",
      "discord": "https://discord.com/invite/txPuEn5atw",
      "support": "https://uiable.com/account/support"
    }
  },
  "logo": {
    "main": "",
    "logoIcon": "",
    "logoFab": "",
    "logoWatermark": ""
  }
}
```

### `src/components/uiable/blocks/layout/dashboard-layout/data/dashboard-3.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" },
    ],
  },
]
```
