前言 #
今天要來探索 Animate UI 的 Sidebar 元件,你可能會在 Shadcn UI 上看到超級像(甚至說一模一樣)的側邊欄,但配上 Animate UI 一些些小動畫點綴,讓它不僅能優雅地收放自如,還能讓整個介面瞬間有條理又有高級感!想要了解更多,請繼續看下去~
Sidebar #


▲ 每個按鈕對應的過場動畫

▲ 在手機上也已經完成響應式設計
打開的方向似乎反了…? 一個月前的版本式正常的啊 🤯
-
使用 Shadcn CLI 加入 Sidebar
npx shadcn@latest add @animate-ui/components-radix-sidebar npx shadcn@latest add @animate-ui/primitives-radix-collapsible npx shadcn@latest add @animate-ui/components-radix-dropdown-menu npx shadcn@latest add @animate-ui/components-radix-sheet npx shadcn@latest add @animate-ui/components-animate-tooltip npx shadcn@latest add avatar npx shadcn@latest add breadcrumb
-
Import 元件並將元件放在想要的位置上
"use client" import * as React from 'react'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { Separator } from '@/components/ui/separator'; import { SidebarProvider, SidebarInset, SidebarTrigger, Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarRail, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarMenuAction, } from '@/components/animate-ui/components/radix/sidebar'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/animate-ui/primitives/radix/collapsible'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/animate-ui/components/radix/dropdown-menu'; import { AudioWaveform, BadgeCheck, Bell, BookOpen, Bot, ChevronRight, ChevronsUpDown, Command, CreditCard, Folder, Forward, Frame, GalleryVerticalEnd, LogOut, Map, MoreHorizontal, PieChart, Plus, Settings2, Sparkles, SquareTerminal, Trash2, } from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage, } from '@/components/ui/avatar'; import { useIsMobile } from '@/hooks/use-mobile';側邊欄要顯示的資料
const DATA = { user: { name: 'Skyleen', email: 'skyleen@example.com', avatar: 'https://pbs.twimg.com/profile_images/1909615404789506048/MTqvRsjo_400x400.jpg', }, teams: [ { name: 'Acme Inc', logo: GalleryVerticalEnd, plan: 'Enterprise', }, { name: 'Acme Corp.', logo: AudioWaveform, plan: 'Startup', }, { name: 'Evil Corp.', logo: Command, plan: 'Free', }, ], navMain: [ { title: 'Playground', url: '#', icon: SquareTerminal, isActive: true, items: [ { title: 'History', url: '#', }, { title: 'Starred', url: '#', }, { title: 'Settings', url: '#', }, ], }, { title: 'Models', url: '#', icon: Bot, items: [ { title: 'Genesis', url: '#', }, { title: 'Explorer', url: '#', }, { title: 'Quantum', url: '#', }, ], }, { title: 'Documentation', url: '#', icon: BookOpen, items: [ { title: 'Introduction', url: '#', }, { title: 'Get Started', url: '#', }, { title: 'Tutorials', url: '#', }, { title: 'Changelog', url: '#', }, ], }, { title: 'Settings', url: '#', icon: Settings2, items: [ { title: 'General', url: '#', }, { title: 'Team', url: '#', }, { title: 'Billing', url: '#', }, { title: 'Limits', url: '#', }, ], }, ], projects: [ { name: 'Design Engineering', url: '#', icon: Frame, }, { name: 'Sales & Marketing', url: '#', icon: PieChart, }, { name: 'Travel', url: '#', icon: Map, }, ], };export default function Home() { const isMobile = useIsMobile(); const [activeTeam, setActiveTeam] = React.useState(DATA.teams[0]); if (!activeTeam) return null; return ( <SidebarProvider> <Sidebar collapsible="icon"> <SidebarHeader> {/* Team Switcher */} <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger asChild> <SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> <activeTeam.logo className="size-4" /> </div> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-semibold"> {activeTeam.name} </span> <span className="truncate text-xs"> {activeTeam.plan} </span> </div> <ChevronsUpDown className="ml-auto" /> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" align="start" side={isMobile ? 'bottom' : 'right'} sideOffset={4} > <DropdownMenuLabel className="text-xs text-muted-foreground"> Teams </DropdownMenuLabel> {DATA.teams.map((team, index) => ( <DropdownMenuItem key={team.name} onClick={() => setActiveTeam(team)} className="gap-2 p-2" > <div className="flex size-6 items-center justify-center rounded-sm border"> <team.logo className="size-4 shrink-0" /> </div> {team.name} <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut> </DropdownMenuItem> ))} <DropdownMenuSeparator /> <DropdownMenuItem className="gap-2 p-2"> <div className="flex size-6 items-center justify-center rounded-md border bg-background"> <Plus className="size-4" /> </div> <div className="font-medium text-muted-foreground"> Add team </div> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> {/* Team Switcher */} </SidebarHeader> <SidebarContent> {/* Nav Main */} <SidebarGroup> <SidebarGroupLabel>Platform</SidebarGroupLabel> <SidebarMenu> {DATA.navMain.map((item) => ( <Collapsible key={item.title} asChild defaultOpen={item.isActive} className="group/collapsible" > <SidebarMenuItem> <CollapsibleTrigger asChild> <SidebarMenuButton tooltip={item.title}> {item.icon && <item.icon />} <span>{item.title}</span> <ChevronRight className="ml-auto transition-transform duration-300 group-data-[state=open]/collapsible:rotate-90" /> </SidebarMenuButton> </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> {item.items?.map((subItem) => ( <SidebarMenuSubItem key={subItem.title}> <SidebarMenuSubButton asChild> <a href={subItem.url}> <span>{subItem.title}</span> </a> </SidebarMenuSubButton> </SidebarMenuSubItem> ))} </SidebarMenuSub> </CollapsibleContent> </SidebarMenuItem> </Collapsible> ))} </SidebarMenu> </SidebarGroup> {/* Nav Main */} {/* Nav Project */} <SidebarGroup className="group-data-[collapsible=icon]:hidden"> <SidebarGroupLabel>Projects</SidebarGroupLabel> <SidebarMenu> {DATA.projects.map((item) => ( <SidebarMenuItem key={item.name}> <SidebarMenuButton asChild> <a href={item.url}> <item.icon /> <span>{item.name}</span> </a> </SidebarMenuButton> <DropdownMenu> <DropdownMenuTrigger asChild> <SidebarMenuAction showOnHover> <MoreHorizontal /> <span className="sr-only">More</span> </SidebarMenuAction> </DropdownMenuTrigger> <DropdownMenuContent className="w-48 rounded-lg" side={isMobile ? 'bottom' : 'right'} align={isMobile ? 'end' : 'start'} > <DropdownMenuItem> <Folder className="text-muted-foreground" /> <span>View Project</span> </DropdownMenuItem> <DropdownMenuItem> <Forward className="text-muted-foreground" /> <span>Share Project</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem> <Trash2 className="text-muted-foreground" /> <span>Delete Project</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> ))} <SidebarMenuItem> <SidebarMenuButton className="text-sidebar-foreground/70"> <MoreHorizontal className="text-sidebar-foreground/70" /> <span>More</span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> {/* Nav Project */} </SidebarContent> <SidebarFooter> {/* Nav User */} <SidebarMenu> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger asChild> <SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > <Avatar className="h-8 w-8 rounded-lg"> <AvatarImage src={DATA.user.avatar} alt={DATA.user.name} /> <AvatarFallback className="rounded-lg">CN</AvatarFallback> </Avatar> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-semibold"> {DATA.user.name} </span> <span className="truncate text-xs"> {DATA.user.email} </span> </div> <ChevronsUpDown className="ml-auto size-4" /> </SidebarMenuButton> </DropdownMenuTrigger> <DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side={isMobile ? 'bottom' : 'right'} align="end" sideOffset={4} > <DropdownMenuLabel className="p-0 font-normal"> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <Avatar className="h-8 w-8 rounded-lg"> <AvatarImage src={DATA.user.avatar} alt={DATA.user.name} /> <AvatarFallback className="rounded-lg"> CN </AvatarFallback> </Avatar> <div className="grid flex-1 text-left text-sm leading-tight"> <span className="truncate font-semibold"> {DATA.user.name} </span> <span className="truncate text-xs"> {DATA.user.email} </span> </div> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> <Sparkles /> Upgrade to Pro </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem> <BadgeCheck /> Account </DropdownMenuItem> <DropdownMenuItem> <CreditCard /> Billing </DropdownMenuItem> <DropdownMenuItem> <Bell /> Notifications </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem> <LogOut /> Log out </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </SidebarMenuItem> </SidebarMenu> {/* Nav User */} </SidebarFooter> <SidebarRail /> </Sidebar> <SidebarInset> <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12"> <div className="flex items-center gap-2 px-4"> <SidebarTrigger className="-ml-1" /> <Separator orientation="vertical" className="mr-2 h-4" /> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem className="hidden md:block"> <BreadcrumbLink href="#"> Building Your Application </BreadcrumbLink> </BreadcrumbItem> <BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbItem> <BreadcrumbPage>Data Fetching</BreadcrumbPage> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> </div> </header> <div className="flex flex-1 flex-col gap-4 p-4 pt-0"> <div className="grid auto-rows-min gap-4 md:grid-cols-3"> <div className="aspect-video rounded-xl bg-muted/50" /> <div className="aspect-video rounded-xl bg-muted/50" /> <div className="aspect-video rounded-xl bg-muted/50" /> </div> <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" /> </div> </SidebarInset> </SidebarProvider> ); } -
改成你要的功能 😂,這個可以變化的地方太多了
-
也可以至 Shadcn UI 中的 Blocks 來看看不同種類的 Sidebar,搞不好看完後會讓你更有想法~

-
小結 #
這次介紹了 Animate UI 的 Sidebar 元件,讓你體驗到「側邊欄」其實也能很有戲。 不論是固定、可收合,還是動態出場,都能透過簡單的設定實現!
透過 shadcn CLI 的安裝與調整,你也可以輕鬆打造出既俐落又有互動感的 Sidebar,讓你的介面不只是好看,還 hen~ 好用 💪