iT邦幫忙

2025 iThome 鐵人賽

DAY 25
1

前言

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

Sidebar

Sidebar Demo 1

Sidebar Demo 2

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

Sidebar Demo on Mobile

▲ 在手機上也已經完成響應式設計

打開的方向似乎反了...? 一個月前的版本式正常的啊 🤯

  1. 使用 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
    

    Sidebar Install

  2. 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>
    );
    }
    
  3. 改成你要的功能 😂,這個可以變化的地方太多了

    1. 也可以至 Shadcn UI 中的 Blocks 來看看不同種類的 Sidebar,搞不好看完後會讓你更有想法~

      Sidebar Shadcn

小結

這次介紹了 Animate UI 的 Sidebar 元件,讓你體驗到「側邊欄」其實也能很有戲。
不論是固定、可收合,還是動態出場,都能透過簡單的設定實現!

透過 shadcn CLI 的安裝與調整,你也可以輕鬆打造出既俐落又有互動感的 Sidebar,讓你的介面不只是好看,還 hen~ 好用 💪

Reference


上一篇
Day 24 - Animate UI Star Background
下一篇
Day 26 - Animate UI Theme Toggler
系列文
讓你的 UI 動起來:Animate-UI 初探26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

1
AndyAWD
iT邦新手 2 級 ‧ 2025-10-09 23:03:15

第一次看到從右邊滑出來的 XD

adsfaaron iT邦新手 5 級 ‧ 2025-10-10 11:57:21 檢舉

非常不符合使用者操作XD

0
chiaominchang222
iT邦新手 5 級 ‧ 2025-10-10 12:19:58

喜歡gpt的簡潔(畢竟也看了一年多(?

我要留言

立即登入留言