用 shadcn/ui 搭建后台骨架:Sidebar + Layout 最佳实践
手把手教你搭建一个可扩展的后台管理系统骨架,从 Sidebar 组件到 Next.js Layout 整合,完整代码可直接上手。
上周接了个后台管理系统的项目,第一时间想到 shadcn/ui。说实话,之前用过 Ant Design 和 MUI,总觉得样式定制起来挺费劲的——要么得覆盖一大堆样式,要么被框架的设计理念绑死。
shadcn/ui 不一样。它是 “Copy-paste” 模式,代码直接放你项目里,想改就改。用了两周下来,感觉确实香。特别是 Sidebar 组件,配合 Next.js 的 App Router,搭建后台骨架清爽多了。
这篇文章就把我的实践过程整理出来。从零开始,带你搭建一个可扩展的后台布局。
一、为什么选择 shadcn/ui Sidebar?
先说说我踩过的坑。
之前用 Ant Design Pro,开箱即用确实爽,但项目做久了就开始头疼:想改侧边栏样式,得翻半天文档;想做点个性化交互,发现框架限制太多了。MUI 也有类似的问题,主题定制虽说灵活,但那是建立在你会写 Material Design 的前提下。
传统方案的痛点
Ant Design:功能全面,但定制成本高。想改个侧边栏的圆角,可能要写三层样式覆盖。
MUI:设计体系完整,但学习曲线陡。Styled Components 的写法,团队新人上手得一周。
自己写:完全可控,但从零写一个支持响应式、可访问性、键盘导航的侧边栏?少说得三天。
shadcn/ui 的解法
shadcn/ui 走的是另一条路:
- Copy-paste 模式:组件代码直接放你项目里,没有黑盒依赖
- Radix UI 基础:可访问性内置,键盘导航、ARIA 属性都帮你处理好
- Tailwind CSS 驱动:样式就是类名,想改就改,没有样式覆盖的烦恼
我见过不少团队从 MUI 迁移到 shadcn/ui,原因很简单:他们想要的是可控,而不是开箱即用的模板。
适用场景
如果你正在做:
- 中小型后台管理系统
- SaaS 产品的控制台
- 内部工具或运营平台
shadcn/ui Sidebar 值得一试。它不会给你一个完整的后台模板,但会给你一个足够灵活的骨架。
二、Sidebar 组件架构解析
在动手之前,先理解 shadcn/ui Sidebar 的组件体系。这块是官方文档里写得比较清楚的部分,我这里快速过一遍。
核心组件清单
Sidebar 由一整套组件组成,各司其职:
SidebarProvider // 状态上下文,包裹整个应用
Sidebar // 侧边栏容器
SidebarHeader // 顶部固定区域,放 Logo
SidebarContent // 可滚动内容区,放菜单
SidebarGroup // 菜单分组
SidebarMenu // 菜单列表
SidebarMenuItem // 菜单项
SidebarMenuButton // 菜单按钮(支持 Link)
SidebarFooter // 底部固定区域,放用户信息
SidebarTrigger // 折叠/展开按钮
SidebarInset // 主内容区包装器
看着组件多,其实关系很清晰:
SidebarProvider
├── Sidebar
│ ├── SidebarHeader
│ ├── SidebarContent
│ │ └── SidebarGroup
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ └── SidebarMenuButton
│ └── SidebarFooter
└── SidebarInset
└── {children}
状态管理机制
Sidebar 的折叠状态由 SidebarProvider 管理。有两种模式:
非受控模式(推荐):
<SidebarProvider defaultOpen={true}>
<Sidebar />
</SidebarProvider>
受控模式:
const [open, setOpen] = useState(true);
<SidebarProvider open={open} onOpenChange={setOpen}>
<Sidebar />
</SidebarProvider>
大多数情况下,非受控模式就够了。除非你需要在其他地方控制 Sidebar 状态(比如用户设置里有个开关),才用受控模式。
响应式设计原理
Sidebar 内置了响应式支持:
- 桌面端:侧边栏固定在左侧,可以通过
SidebarTrigger折叠 - 移动端:自动变成抽屉式(Sheet),点击 Trigger 弹出
这个逻辑是组件内部处理的,你只需要在 SidebarProvider 里配置好,剩下的交给组件。
三、Next.js Layout 整合实战
好,核心概念讲完了。接下来是重头戏——把 Sidebar 整合到 Next.js 的 Layout 系统里。
3.1 项目结构设计
我推荐用 Next.js 的 Route Groups 组织布局。这样可以让不同功能的页面有不同的布局,而不影响 URL 结构。
app/
├── layout.tsx # Root Layout(全局)
├── (marketing)/ # 营销页面组(Landing、About)
│ ├── layout.tsx # 无 Sidebar
│ └── page.tsx # 首页
├── (dashboard)/ # 后台页面组
│ ├── layout.tsx # 带 Sidebar 的布局
│ ├── page.tsx # Dashboard 主页
│ ├── users/
│ │ └── page.tsx # 用户管理
│ └── settings/
│ └── page.tsx # 系统设置
└── (auth)/ # 认证页面组
├── layout.tsx # 居中布局
├── login/
│ └── page.tsx # 登录页
└── register/
└── page.tsx # 注册页
这个结构的好处是:
- 布局隔离:营销页面不需要 Sidebar,后台页面需要,用 Route Groups 自然分开
- URL 简洁:
(dashboard)不出现在 URL 里,/users就是/users - 易于扩展:新增页面组只需要新建文件夹
3.2 Root Layout 配置
Root Layout 是整个应用的入口,我们在这里配置全局的东西:主题、字体、SidebarProvider。
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { SidebarProvider } from "@/components/ui/sidebar";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Admin Dashboard",
description: "Built with shadcn/ui and Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<SidebarProvider>
{children}
</SidebarProvider>
</body>
</html>
);
}
注意这里 SidebarProvider 放在 Root Layout,而不是 Dashboard Layout。这样做的好处是 Sidebar 状态可以跨页面保持(比如从 /users 跳到 /settings,折叠状态不会丢失)。
3.3 Dashboard Layout 实现
Dashboard Layout 是后台的核心布局,Sidebar 在这里引入。
// app/(dashboard)/layout.tsx
import { AppSidebar } from "@/components/app-sidebar";
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="/dashboard">
后台管理
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>概览</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="flex-1 p-4 pt-6">{children}</main>
</SidebarInset>
</>
);
}
这个布局包含:
- AppSidebar:自定义的侧边栏组件(下一节实现)
- SidebarInset:主内容区包装器,自动处理 Sidebar 折叠时的宽度
- Header:顶部导航栏,包含 SidebarTrigger 和面包屑
- Main:主内容区
3.4 AppSidebar 组件实现
现在来实现侧边栏本身。我推荐用配置驱动的方式:导航菜单存在一个配置文件里,组件根据配置渲染。
先定义导航配置:
// lib/navigation.ts
import {
Home,
Users,
Settings,
FileText,
BarChart3,
Shield,
} from "lucide-react";
export interface NavItem {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
badge?: string;
}
export const navConfig: NavItem[] = [
{
title: "概览",
href: "/dashboard",
icon: Home,
},
{
title: "用户管理",
href: "/users",
icon: Users,
badge: "12", // 角标
},
{
title: "数据分析",
href: "/analytics",
icon: BarChart3,
},
{
title: "内容管理",
href: "/content",
icon: FileText,
},
{
title: "系统设置",
href: "/settings",
icon: Settings,
},
{
title: "权限管理",
href: "/permissions",
icon: Shield,
},
];
然后实现 AppSidebar:
// components/app-sidebar.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { navConfig } from "@/lib/navigation";
import { Logo } from "@/components/logo";
import { UserNav } from "@/components/user-nav";
export function AppSidebar() {
const pathname = usePathname();
return (
<Sidebar>
<SidebarHeader className="border-b border-border">
<Logo />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>导航菜单</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navConfig.map((item) => {
const isActive = pathname === item.href;
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={item.title}
>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
{item.badge && (
<span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-2 py-0.5">
{item.badge}
</span>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="border-t border-border">
<UserNav />
</SidebarFooter>
</Sidebar>
);
}
这里有个关键点:路由高亮。我用 usePathname() 获取当前路径,然后跟 item.href 比较,匹配上就给 SidebarMenuButton 传 isActive={true},组件会自动应用激活样式。
3.5 多级菜单实现
如果你的后台有二级菜单,可以用 Collapsible 包裹 SidebarGroup:
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
// 在 SidebarMenu 里
<Collapsible defaultOpen>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<Settings className="h-4 w-4" />
<span>系统设置</span>
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/settings/general">
<span>基础设置</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/settings/security">
<span>安全设置</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
四、进阶功能实现
基础布局搭好了,接下来看点实用的进阶功能。
4.1 权限控制(RBAC)
很多后台需要根据用户角色显示不同的菜单。实现方式很简单:在导航配置里加个 roles 字段,渲染时过滤。
先改造配置:
// lib/navigation.ts
export interface NavItem {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
roles?: string[]; // 允许访问的角色
}
export const navConfig: NavItem[] = [
{
title: "概览",
href: "/dashboard",
icon: Home,
// 不设 roles,所有人都能看
},
{
title: "用户管理",
href: "/users",
icon: Users,
roles: ["admin", "manager"], // 只有 admin 和 manager 能看
},
{
title: "权限管理",
href: "/permissions",
icon: Shield,
roles: ["admin"], // 只有 admin 能看
},
];
然后在 AppSidebar 里根据用户角色过滤:
// components/app-sidebar.tsx
import { useAuth } from "@/hooks/use-auth"; // 假设你有个 auth hook
export function AppSidebar() {
const pathname = usePathname();
const { user } = useAuth(); // 获取当前用户
const filteredNav = navConfig.filter((item) => {
if (!item.roles) return true; // 没有角色限制,所有人都能看
return item.roles.some((role) => user?.roles?.includes(role));
});
return (
<Sidebar>
{/* ... */}
<SidebarMenu>
{filteredNav.map((item) => {
// ...
})}
</SidebarMenu>
{/* ... */}
</Sidebar>
);
}
这样一来,普通用户登录后,就看不到 “权限管理” 这个菜单项了。
4.2 外部链接和分割线
有时候侧边栏需要放外部链接(比如文档、帮助中心),或者用分割线把菜单分组。shadcn/ui Sidebar 也支持:
<SidebarGroup>
<SidebarGroupLabel>主要功能</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{/* 主要菜单项 */}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>帮助支持</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="https://docs.example.com" target="_blank" rel="noopener">
<BookOpen className="h-4 w-4" />
<span>使用文档</span>
<ExternalLink className="ml-auto h-3 w-3" />
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="mailto:[email protected]">
<HelpCircle className="h-4 w-4" />
<span>联系我们</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
4.3 搜索框和快捷操作
很多后台会在侧边栏放一个搜索框,或者全局搜索(Cmd+K)。shadcn/ui 有 Command 组件可以做这个:
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
<SidebarGroup>
<SidebarGroupContent>
<Command className="rounded-lg border shadow-md">
<CommandInput placeholder="搜索菜单..." />
<CommandList>
<CommandEmpty>未找到结果</CommandEmpty>
<CommandGroup heading="建议">
{navConfig.map((item) => (
<CommandItem key={item.href} onSelect={() => router.push(item.href)}>
<item.icon className="mr-2 h-4 w-4" />
{item.title}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</SidebarGroupContent>
</SidebarGroup>
五、性能优化与最佳实践
最后聊几个实战中的优化点。
Server Components 优先
Next.js App Router 默认所有组件都是 Server Component。Sidebar 的静态部分(比如 Logo、固定的菜单项)可以保持为 Server Component,只有需要交互的部分(路由高亮、折叠状态)才用 "use client"。
我的做法是:
AppSidebar标记为"use client"(因为用了usePathname)SidebarHeader、SidebarFooter里的静态部分单独抽成 Server Component- 导航配置在服务端生成,传给客户端组件
这样可以减少客户端 JS 体积。
懒加载大型菜单
如果你的后台有几十个菜单项,可以考虑懒加载。用 React.lazy 或者 Next.js 的 dynamic:
import dynamic from "next/dynamic";
const AdminMenu = dynamic(() => import("./admin-menu"), {
loading: () => <SidebarMenuSkeleton />,
});
不过说实话,大多数后台的菜单项都不会太多,这个优化场景比较少见。
可访问性要点
shadcn/ui 的 Sidebar 基于 Radix UI,可访问性基本内置了。但你还是要注意几点:
- 图标 + 文字:不要只用图标,屏幕阅读器用户看不见
- Focus 可见:不要覆盖默认的 focus 样式
- 键盘导航:Tab 和方向键应该能正常工作
Radix UI 帮你处理了大部分,但如果你自定义了组件,记得测试一下键盘导航。
六、常见问题
Q1: Sidebar 状态刷新后丢失?
如果你把 SidebarProvider 放在 Dashboard Layout 而不是 Root Layout,路由切换时状态会重置。把 Provider 提升到 Root Layout 就行。
Q2: 移动端 Sidebar 怎么自动关闭?
shadcn/ui 的 Sidebar 在移动端会自动变成 Sheet。你需要在菜单项点击后手动关闭:
const { setOpenMobile } = useSidebar();
<SidebarMenuButton
onClick={() => setOpenMobile(false)}
>
Q3: 如何自定义 Sidebar 宽度?
用 CSS 变量:
<Sidebar
style={{
"--sidebar-width": "280px",
"--sidebar-width-mobile": "100%",
}}
>
或者在 sidebar.tsx 里改 SIDEBAR_WIDTH 常量。
总结
shadcn/ui Sidebar 配合 Next.js Layout,搭建后台骨架确实高效。关键点回顾:
- 组件体系:理解 SidebarProvider、Sidebar、SidebarContent 等组件的职责
- Layout 整合:用 Route Groups 隔离不同布局,
SidebarProvider放 Root Layout - 配置驱动:导航菜单存配置文件,组件根据配置渲染,方便维护
- 路由高亮:
usePathname()+isActiveprop,简单直接 - 权限控制:配置里加
roles,渲染时过滤
这套架构我用了好几个项目,扩展性不错。新增页面只需要在 navConfig 里加一条,剩下的交给组件处理。
有问题欢迎在评论区讨论。下期我会写 shadcn/ui DataTable 的实战用法,感兴趣的可以关注一下。
搭建 shadcn/ui Sidebar + Next.js Layout 后台骨架
从零开始搭建一个可扩展的后台管理系统布局,包含侧边栏、路由高亮和权限控制
⏱️ 预计耗时: 45 分钟
- 1
步骤1: 安装 shadcn/ui 并添加 Sidebar 组件
运行 CLI 命令初始化项目并添加组件:
```bash
npx shadcn@latest init
npx shadcn@latest add sidebar
```
安装过程会询问样式配置,选择默认即可。完成后 components/ui 目录下会生成 sidebar.tsx 文件。 - 2
步骤2: 配置 Root Layout
在 app/layout.tsx 中包裹 SidebarProvider:
• 导入 SidebarProvider 组件
• 在 body 标签内包裹 {children}
• 设置 lang="zh-CN" 语言属性
这样可以确保 Sidebar 状态在全局保持持久化。 - 3
步骤3: 创建 Dashboard Layout
在 app/(dashboard)/layout.tsx 创建后台专用布局:
• 使用 Route Groups 语法 (dashboard)
• 引入 AppSidebar 和 SidebarInset
• 添加顶部 Header 和面包屑导航
Route Groups 不影响 URL 结构,/dashboard 路径直接映射为根路径。 - 4
步骤4: 定义导航配置
创建 lib/navigation.ts 配置文件:
• 定义 NavItem 接口(title、href、icon、badge)
• 导出 navConfig 数组
• 可选添加 roles 字段实现权限控制
配置驱动的方式让新增菜单只需修改一处。 - 5
步骤5: 实现 AppSidebar 组件
创建 components/app-sidebar.tsx:
• 使用 "use client" 标记为客户端组件
• 通过 usePathname 获取当前路由
• 遍历 navConfig 渲染菜单项
• 匹配路由时设置 isActive 属性高亮 - 6
步骤6: 添加权限控制(可选)
实现 RBAC 权限过滤:
• 在 NavItem 接口添加 roles 字段
• 在 AppSidebar 中通过 useAuth 获取用户角色
• 使用 filter 方法过滤菜单项
没有 roles 字段的菜单默认对所有用户可见。
常见问题
shadcn/ui Sidebar 和 Ant Design 的侧边栏有什么区别?
SidebarProvider 应该放在 Root Layout 还是 Dashboard Layout?
如何实现移动端 Sidebar 自动关闭?
```tsx
const { setOpenMobile } = useSidebar();
<SidebarMenuButton onClick={() => setOpenMobile(false)}>
```
这样点击菜单项后抽屉会自动收起。
如何自定义 Sidebar 宽度?
1. 使用 CSS 变量(推荐):
```tsx
<Sidebar style={{ "--sidebar-width": "280px" }} />
```
2. 修改 sidebar.tsx 中的 SIDEBAR_WIDTH 常量
CSS 变量方式更灵活,可以针对不同 Sidebar 设置不同宽度。
如何实现多级菜单?
• CollapsibleTrigger 包含一级菜单按钮
• CollapsibleContent 包含 SidebarMenuSub 和二级菜单项
• ChevronDown 图标指示可展开状态
完整代码见文章 3.5 节。
shadcn/ui Sidebar 的可访问性如何?
参考资料
9 分钟阅读 · 发布于: 2026年3月27日 · 修改于: 2026年3月27日

评论
使用 GitHub 账号登录后即可评论