Przejdź do treści

Sidebar

Stała nawigacja aplikacji — zwija się do paska ikon, na telefonach wysuwa się spoza ekranu, a jej wygląd dopasujesz do motywu co do piksela.

Kod źródłowyRejestr
  • 12
Ulubione
import {
  CalendarIcon,
  ChevronsUpDownIcon,
  FolderKanbanIcon,
  HouseIcon,
  InboxIcon,
  SearchIcon,
} from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
} from "@/components/ui/sidebar"

const navigation = [
  { title: "Strona główna", icon: HouseIcon, active: true },
  { title: "Skrzynka odbiorcza", icon: InboxIcon, badge: 12 },
  { title: "Projekty", icon: FolderKanbanIcon },
  { title: "Kalendarz", icon: CalendarIcon },
  { title: "Szukaj", icon: SearchIcon },
]

const favorites = [
  { title: "Design system", color: "bg-brand-9" },
  { title: "Premiera w Q4", color: "bg-amber-9" },
  { title: "Badania klientów", color: "bg-teal-9" },
]

export default function SidebarDemo() {
  return (
    <SidebarProvider className="min-h-0 w-fit">
      <Sidebar
        collapsible="none"
        className="h-104 rounded-xl border shadow-raised"
      >
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg">
                <span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background">
                  A
                </span>
                <span className="flex min-w-0 flex-col gap-0.5 leading-none">
                  <span className="truncate font-semibold text-foreground">
                    Acme Inc
                  </span>
                  <span className="truncate text-xs text-muted-foreground">
                    Plan Pro
                  </span>
                </span>
                <ChevronsUpDownIcon className="ml-auto" />
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent>
              <SidebarMenu>
                {navigation.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton isActive={item.active}>
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                    {item.badge !== undefined && (
                      <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                    )}
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup>
            <SidebarGroupLabel>Ulubione</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                {favorites.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton>
                      <span
                        className={`ml-1 size-2 shrink-0 rounded-full ${item.color}`}
                      />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg">
                <Avatar size="sm">
                  <AvatarFallback>AK</AvatarFallback>
                </Avatar>
                <span className="flex min-w-0 flex-col gap-0.5 leading-none">
                  <span className="truncate font-medium text-foreground">
                    Ana Kowalska
                  </span>
                  <span className="truncate text-xs text-muted-foreground">
                    ana@acme.com
                  </span>
                </span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
      </Sidebar>
    </SidebarProvider>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/sidebar.json

Użycie

Pasek boczny należy do układu aplikacji, a nie do strony. Owiń układ w SidebarProvider, wyrenderuj Sidebar obok SidebarInset, który zawiera stronę, i umieść SidebarTrigger wszędzie tam, skąd ludzie mają móc zwinąć i rozwinąć pasek.

app/(app)/layout.tsx
import { cookies } from "next/headers"

import { AppSidebar } from "@/components/app-sidebar"
import {
  SidebarInset,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"

export default async function AppLayout({ children }: { children: React.ReactNode }) {
  // The provider saves its state in a cookie; read it to render the same state on the server.
  const cookieStore = await cookies()
  const defaultOpen = cookieStore.get("sidebar_state")?.value !== "false"

  return (
    <SidebarProvider defaultOpen={defaultOpen}>
      <AppSidebar />
      <SidebarInset>
        <header className="flex h-12 items-center gap-2 border-b px-3">
          <SidebarTrigger />
        </header>
        {children}
      </SidebarInset>
    </SidebarProvider>
  )
}
components/app-sidebar.tsx
"use client"

import { HouseIcon, InboxIcon, SettingsIcon } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarRail,
} from "@/components/ui/sidebar"

const items = [
  { title: "Home", href: "/", icon: HouseIcon },
  { title: "Inbox", href: "/inbox", icon: InboxIcon },
  { title: "Settings", href: "/settings", icon: SettingsIcon },
]

export function AppSidebar() {
  const pathname = usePathname()

  return (
    <Sidebar collapsible="icon">
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupContent>
            <nav aria-label="Main">
              <SidebarMenu>
                {items.map((item) => {
                  const active = pathname === item.href
                  return (
                    <SidebarMenuItem key={item.href}>
                      <SidebarMenuButton
                        isActive={active}
                        tooltip={item.title}
                        render={
                          <Link href={item.href} aria-current={active ? "page" : undefined} />
                        }
                      >
                        <item.icon />
                        <span>{item.title}</span>
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  )
                })}
              </SidebarMenu>
            </nav>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
      <SidebarRail />
    </Sidebar>
  )
}

Anatomia

SidebarProvider                 state, keyboard shortcut, width variables
├─ Sidebar                      the panel (desktop) or a sheet (mobile)
│  ├─ SidebarHeader             workspace switcher, search
│  ├─ SidebarContent            scrolls
│  │  └─ SidebarGroup
│  │     ├─ SidebarGroupLabel   · SidebarGroupAction
│  │     └─ SidebarGroupContent
│  │        └─ SidebarMenu
│  │           └─ SidebarMenuItem
│  │              ├─ SidebarMenuButton   · SidebarMenuAction · SidebarMenuBadge
│  │              └─ SidebarMenuSub → SidebarMenuSubItem → SidebarMenuSubButton
│  ├─ SidebarFooter             account, help
│  └─ SidebarRail               a clickable edge that toggles the sidebar
└─ SidebarInset                 the page, with SidebarTrigger in its header

Przykłady

Poniższe podglądy to prawdziwe paski boczne zamknięte w ramce. Na desktopie pasek boczny ma position: fixed; podgląd nadaje swojemu kontenerowi transform: translateZ(0), przez co kontener staje się blokiem zawierającym dla paska, i renderuje SidebarInset jako <div>, bo strona dokumentacji ma już <main>. W twojej aplikacji nie potrzebujesz ani jednego, ani drugiego.

Powłoka aplikacji

collapsible="icon" z SidebarRail i SidebarTrigger. Kliknij wyzwalacz, kliknij szynę wzdłuż prawej krawędzi paska albo naciśnij ⌘ B, aby zwinąć go do paska ikon.

import {
  BookOpenIcon,
  ChartBarIcon,
  FolderKanbanIcon,
  HouseIcon,
  InboxIcon,
  LifeBuoyIcon,
  SettingsIcon,
} from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from "@/components/ui/sidebar"

const platform = [
  { title: "Strona główna", icon: HouseIcon },
  { title: "Skrzynka odbiorcza", icon: InboxIcon },
  { title: "Projekty", icon: FolderKanbanIcon, active: true },
  { title: "Analityka", icon: ChartBarIcon },
]

const support = [
  { title: "Dokumentacja", icon: BookOpenIcon },
  { title: "Centrum pomocy", icon: LifeBuoyIcon },
  { title: "Ustawienia", icon: SettingsIcon },
]

export default function SidebarAppShell() {
  return (
    // `transform` makes this box the containing block for the sidebar's fixed
    // positioning, so the whole app shell fits inside the preview.
    <SidebarProvider className="h-120 min-h-0 [transform:translateZ(0)] overflow-hidden rounded-xl border">
      <Sidebar collapsible="icon" className="h-auto">
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg" tooltip="Acme Inc">
                <span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background">
                  A
                </span>
                <span className="flex min-w-0 flex-col gap-0.5 leading-none">
                  <span className="truncate font-semibold text-foreground">
                    Acme Inc
                  </span>
                  <span className="truncate text-xs text-muted-foreground">
                    Plan Pro
                  </span>
                </span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Platforma</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                {platform.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton
                      isActive={item.active}
                      tooltip={item.title}
                    >
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup className="mt-auto">
            <SidebarGroupLabel>Pomoc</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                {support.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton size="sm" tooltip={item.title}>
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg" tooltip="Ana Kowalska">
                <Avatar size="sm">
                  <AvatarFallback>AK</AvatarFallback>
                </Avatar>
                <span className="flex min-w-0 flex-col gap-0.5 leading-none">
                  <span className="truncate font-medium text-foreground">
                    Ana Kowalska
                  </span>
                  <span className="truncate text-xs text-muted-foreground">
                    ana@acme.com
                  </span>
                </span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
        <SidebarRail />
      </Sidebar>
      {/* The docs page already has a <main>, so the preview renders a <div>. */}
      <SidebarInset render={<div />}>
        <header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
          <SidebarTrigger />
          <Separator
            orientation="vertical"
            className="mr-1 h-4 data-vertical:self-center"
          />
          <Breadcrumb>
            <BreadcrumbList>
              <BreadcrumbItem>
                <BreadcrumbLink href="#">Projekty</BreadcrumbLink>
              </BreadcrumbItem>
              <BreadcrumbSeparator />
              <BreadcrumbItem>
                <BreadcrumbPage>Design system</BreadcrumbPage>
              </BreadcrumbItem>
            </BreadcrumbList>
          </Breadcrumb>
        </header>
        <div className="grid flex-1 auto-rows-min gap-3 p-4 sm:grid-cols-3">
          <div className="aspect-video rounded-lg bg-muted" />
          <div className="aspect-video rounded-lg bg-muted" />
          <div className="aspect-video rounded-lg bg-muted" />
          <div className="h-40 rounded-lg bg-muted sm:col-span-3" />
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}

Zwinięty do ikon

Po zwinięciu etykiety są przycinane, a każdy przycisk pokazuje swój tooltip po najechaniu i przy fokusie. Jeśli korzystasz z tego trybu, daj każdemu elementowi najwyższego poziomu ikonę i podpowiedź.

Strona główna
import {
  ChartBarIcon,
  FolderKanbanIcon,
  HouseIcon,
  InboxIcon,
  SettingsIcon,
} from "lucide-react"

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"

const items = [
  { title: "Strona główna", icon: HouseIcon, active: true },
  { title: "Skrzynka odbiorcza", icon: InboxIcon },
  { title: "Projekty", icon: FolderKanbanIcon },
  { title: "Analityka", icon: ChartBarIcon },
]

export default function SidebarIconCollapsed() {
  return (
    <SidebarProvider
      defaultOpen={false}
      className="h-80 min-h-0 [transform:translateZ(0)] overflow-hidden rounded-xl border"
    >
      <Sidebar collapsible="icon" className="h-auto">
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg" tooltip="Acme Inc">
                <span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background">
                  A
                </span>
                <span className="truncate font-semibold text-foreground">
                  Acme Inc
                </span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent>
              <SidebarMenu>
                {items.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton
                      isActive={item.active}
                      tooltip={item.title}
                    >
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton tooltip="Ustawienia">
                <SettingsIcon />
                <span>Ustawienia</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
      </Sidebar>
      <SidebarInset render={<div />}>
        <header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
          <SidebarTrigger />
          <span className="text-sm font-medium text-foreground">Strona główna</span>
        </header>
        <div className="flex flex-1 flex-col gap-3 p-4">
          <div className="h-24 rounded-lg bg-muted" />
          <div className="flex-1 rounded-lg bg-muted" />
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}

Warianty

sidebar przylega do strony i oddziela się od niej obramowaniem. floating zamienia się w wyniesioną kartę odsuniętą od krawędzi. inset wtapia się w tło strony, a treść leży na wyniesionym panelu — to najspokojniejsza opcja dla gęstych aplikacji.

inset
"use client"

import * as React from "react"
import { FolderKanbanIcon, HouseIcon, InboxIcon } from "lucide-react"

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

type Variant = "sidebar" | "floating" | "inset"

const items = [
  { title: "Strona główna", icon: HouseIcon, active: true },
  { title: "Skrzynka odbiorcza", icon: InboxIcon },
  { title: "Projekty", icon: FolderKanbanIcon },
]

export default function SidebarVariants() {
  const [variant, setVariant] = React.useState<Variant>("inset")

  return (
    <div className="flex w-full flex-col items-center gap-4">
      <ToggleGroup
        variant="outline"
        size="sm"
        spacing={0}
        aria-label="Wariant paska bocznego"
        value={[variant]}
        onValueChange={(value) => value[0] && setVariant(value[0] as Variant)}
      >
        <ToggleGroupItem value="sidebar">Sidebar</ToggleGroupItem>
        <ToggleGroupItem value="floating">Floating</ToggleGroupItem>
        <ToggleGroupItem value="inset">Inset</ToggleGroupItem>
      </ToggleGroup>
      <SidebarProvider className="h-80 min-h-0 w-full [transform:translateZ(0)] overflow-hidden rounded-xl border">
        <Sidebar variant={variant} collapsible="icon" className="h-auto">
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Przestrzeń robocza</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  {items.map((item) => (
                    <SidebarMenuItem key={item.title}>
                      <SidebarMenuButton
                        isActive={item.active}
                        tooltip={item.title}
                      >
                        <item.icon />
                        <span>{item.title}</span>
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  ))}
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
        </Sidebar>
        <SidebarInset render={<div />}>
          <header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <SidebarTrigger />
            <span className="text-sm font-medium text-foreground capitalize">
              {variant}
            </span>
          </header>
          <div className="flex flex-1 flex-col gap-3 p-4">
            <div className="h-20 rounded-lg bg-muted" />
            <div className="flex-1 rounded-lg bg-muted" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  )
}

Zwijane sekcje

Połącz Collapsible z menu, aby zbudować rozwijane sekcje. Collapsible renderuje SidebarMenuItem, a jego wyzwalacz — SidebarMenuButton, więc nie ma żadnych dodatkowych elementów opakowujących.

Dokumentacja
import {
  BookOpenIcon,
  ChevronRightIcon,
  LayersIcon,
  PaletteIcon,
} from "lucide-react"

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
} from "@/components/ui/sidebar"

const sections = [
  {
    title: "Pierwsze kroki",
    icon: BookOpenIcon,
    open: true,
    items: ["Wprowadzenie", "Instalacja", "Zasady"],
    active: "Instalacja",
  },
  {
    title: "Fundamenty",
    icon: PaletteIcon,
    items: ["Kolor", "Typografia", "Ruch"],
  },
  {
    title: "Komponenty",
    icon: LayersIcon,
    items: ["Button", "Dialog", "Sidebar"],
  },
]

export default function SidebarSubmenu() {
  return (
    <SidebarProvider className="min-h-0 w-fit">
      <Sidebar
        collapsible="none"
        className="h-96 rounded-xl border shadow-raised"
      >
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Dokumentacja</SidebarGroupLabel>
            <SidebarMenu>
              {sections.map((section) => (
                <Collapsible
                  key={section.title}
                  defaultOpen={section.open}
                  render={<SidebarMenuItem />}
                  className="group/collapsible"
                >
                  <CollapsibleTrigger render={<SidebarMenuButton />}>
                    <section.icon />
                    <span>{section.title}</span>
                    <ChevronRightIcon className="ml-auto transition-transform duration-base ease-standard group-data-open/collapsible:rotate-90" />
                  </CollapsibleTrigger>
                  <CollapsibleContent>
                    <SidebarMenuSub>
                      {section.items.map((item) => (
                        <SidebarMenuSubItem key={item}>
                          <SidebarMenuSubButton
                            render={<button type="button" />}
                            isActive={item === section.active}
                            className="w-full"
                          >
                            <span>{item}</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      ))}
                    </SidebarMenuSub>
                  </CollapsibleContent>
                </Collapsible>
              ))}
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
    </SidebarProvider>
  )
}
<Collapsible defaultOpen render={<SidebarMenuItem />} className="group/collapsible">
  <CollapsibleTrigger render={<SidebarMenuButton />}>
    <BookOpenIcon />
    <span>Get started</span>
    <ChevronRightIcon className="ml-auto transition-transform group-data-open/collapsible:rotate-90" />
  </CollapsibleTrigger>
  <CollapsibleContent>
    <SidebarMenuSub>…</SidebarMenuSub>
  </CollapsibleContent>
</Collapsible>

Akcje i odznaki

SidebarMenuBadge pokazuje licznik na końcu elementu. SidebarMenuAction dodaje akcję pomocniczą — często menu — która z showOnHover pojawia się dopiero po najechaniu. SidebarGroupAction dodaje akcję do etykiety grupy, na przykład Dodaj projekt.

Poczta
  • 24
  • 3
Projekty
import {
  ArchiveIcon,
  InboxIcon,
  MoreHorizontalIcon,
  PencilIcon,
  PlusIcon,
  SendIcon,
  Trash2Icon,
} from "lucide-react"

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
} from "@/components/ui/sidebar"

const mail = [
  { title: "Odebrane", icon: InboxIcon, count: 24 },
  { title: "Wysłane", icon: SendIcon },
  { title: "Archiwum", icon: ArchiveIcon, count: 3 },
]

const projects = [
  { title: "Design system", color: "bg-brand-9" },
  { title: "Strona marketingowa", color: "bg-pink-9" },
  { title: "Aplikacja mobilna", color: "bg-green-9" },
]

export default function SidebarActions() {
  return (
    <SidebarProvider className="min-h-0 w-fit">
      <Sidebar
        collapsible="none"
        className="h-88 rounded-xl border shadow-raised"
      >
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Poczta</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                {mail.map((item) => (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton isActive={item.title === "Odebrane"}>
                      <item.icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                    {item.count !== undefined && (
                      <SidebarMenuBadge>{item.count}</SidebarMenuBadge>
                    )}
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup>
            <SidebarGroupLabel>Projekty</SidebarGroupLabel>
            <SidebarGroupAction title="Dodaj projekt">
              <PlusIcon />
              <span className="sr-only">Dodaj projekt</span>
            </SidebarGroupAction>
            <SidebarGroupContent>
              <SidebarMenu>
                {projects.map((project) => (
                  <SidebarMenuItem key={project.title}>
                    <SidebarMenuButton>
                      <span
                        className={`ml-1 size-2 shrink-0 rounded-full ${project.color}`}
                      />
                      <span>{project.title}</span>
                    </SidebarMenuButton>
                    <DropdownMenu>
                      <DropdownMenuTrigger
                        render={<SidebarMenuAction showOnHover />}
                      >
                        <MoreHorizontalIcon />
                        <span className="sr-only">
                          Więcej akcji dla projektu {project.title}
                        </span>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent
                        side="right"
                        align="start"
                        className="w-44"
                      >
                        <DropdownMenuGroup>
                          <DropdownMenuItem>
                            <PencilIcon />
                            Zmień nazwę
                          </DropdownMenuItem>
                          <DropdownMenuItem>
                            <ArchiveIcon />
                            Archiwum
                          </DropdownMenuItem>
                        </DropdownMenuGroup>
                        <DropdownMenuSeparator />
                        <DropdownMenuGroup>
                          <DropdownMenuItem variant="destructive">
                            <Trash2Icon />
                            Usuń
                          </DropdownMenuItem>
                        </DropdownMenuGroup>
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
    </SidebarProvider>
  )
}

Ładowanie

Renderuj SidebarMenuSkeleton, dopóki elementy się ładują. Szerokości różnią się w każdym wierszu, ale wynikają z useId, więc serwer i klient renderują ten sam HTML.

Projekty
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarProvider,
} from "@/components/ui/sidebar"

export default function SidebarSkeleton() {
  return (
    <SidebarProvider className="min-h-0 w-fit">
      <Sidebar
        collapsible="none"
        className="h-72 rounded-xl border shadow-raised"
      >
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Projekty</SidebarGroupLabel>
            <SidebarMenu aria-busy="true" aria-label="Ładowanie projektów">
              {Array.from({ length: 6 }, (_, index) => (
                <SidebarMenuItem key={index}>
                  <SidebarMenuSkeleton showIcon />
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
    </SidebarProvider>
  )
}

Tryby zwijania

collapsiblePo zwinięciu na desktopieWybierz, gdy
offcanvas (domyślnie)Wysuwa się całkowicie poza widok.Treść potrzebuje całej szerokości, a z nawigacji korzysta się sporadycznie.
iconKurczy się do paska ikon o szerokości 3 rem z podpowiedziami.Ludzie bez przerwy przełączają się między sekcjami.
noneNigdy się nie zwija; renderuje się jako zwykły panel.Strony ustawień, osadzona nawigacja, podglądy.

Na ekranach węższych niż 768 px pasek boczny w trybie offcanvas lub icon staje się panelem Sheet, który wysuwa się od strony wskazanej w side; otwiera go SidebarTrigger. Pasek w trybie none pozostaje w układzie przy każdej szerokości, więc na małych ekranach samodzielnie go ukryj albo zmień jego wygląd.

Stan kontrolowany

Steruj propem open, aby zsynchronizować pasek boczny z własnym stanem, a wszystko, czego potrzebujesz, odczytasz z useSidebar() w dowolnym miejscu wewnątrz providera.

const [open, setOpen] = React.useState(true)

<SidebarProvider open={open} onOpenChange={setOpen}>…</SidebarProvider>
const { state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar } = useSidebar()

Szerokość i strona

Szerokości to zmienne CSS ustawione na providerze — --sidebar-width (16 rem) i --sidebar-width-icon (3 rem). Przekaż side="right", aby umieścić pasek przy przeciwnej krawędzi, na przykład jako panel inspektora.

<SidebarProvider
  style={{ "--sidebar-width": "18rem", "--sidebar-width-icon": "3.5rem" } as React.CSSProperties}
>
  <Sidebar side="right" variant="floating">…</Sidebar>
</SidebarProvider>

Dostosowywanie motywu

Pasek boczny ma własne tokeny semantyczne, więc może różnić się od strony bez zmian w komponentach.

TokenDomyślnie (jasny / ciemny)Rola
--sidebargray-1 / gray-1Powierzchnia
--sidebar-foregroundgray-12Tekst
--sidebar-accentgray-4Elementy po najechaniu i aktywne
--sidebar-accent-foregroundgray-12Tekst na akcencie
--sidebar-bordergray-6 / gray-5Obramowania, linia podmenu
--sidebar-ringbrand-9 / brand-10Pierścień fokusu

Wytyczne

Kiedy używać

  • W aplikacjach, które mają więcej niż pięć głównych miejsc docelowych albo miejsca, między którymi ludzie przechodzą przez cały dzień.
  • Gdy nawigacja obejmuje zagnieżdżone sekcje, projekty lub zapisane widoki, które muszą być w zasięgu jednego kliknięcia.

Kiedy nie używać

  • Przy kilku miejscach docelowych — nagłówek z Tabs albo Navigation Menu zajmie mniej miejsca.
  • Na stronach marketingowych i w dokumentacji, gdzie treść zasługuje na całą szerokość.
  • Jako miejsce na ustawienia lub formularze — użyj komponentu Sheet albo osobnej strony.

Porządkuj według tego, co ludzie robią

Grupuj elementy według zadań, nadaj każdej grupie etykietę i ogranicz etykiety do jednego lub dwóch słów. Przełącznik przestrzeni roboczej lub konta umieść w nagłówku, a pomoc, ustawienia i profil — w stopce, żeby środek zawierał wyłącznie miejsca docelowe.

Przestrzeń roboczaStrona głównaSkrzynka odbiorcza
ProjektyDesign systemPremiera w Q4
DobrzeGrupy z etykietami, a w nich krótkie, jednolite nazwy miejsc docelowych.
Strona głównaUtwórz nowy projektDesign systemUstawienia powiadomieńSkrzynka odbiorcza
ŹleJedna długa lista bez etykiet, w której miejsca docelowe mieszają się z akcjami i ustawieniami.

Pokazuj bieżące położenie

Oznacz jako bieżący dokładnie jeden element za pomocą isActive. Właściwość stylizuje element i ustawia na nim aria-current="page", żeby technologie wspomagające również to przekazały. W zagnieżdżonych sekcjach rozwiń tę, która zawiera bieżącą stronę.

Przewidywalne zwijanie

Wybierz jeden tryb zwijania dla całej aplikacji i zapamiętuj wybór ludzi (provider zapisuje ciasteczko sidebar_state; odczytaj je w układzie aplikacji). Nie zwijaj paska bocznego automatycznie przy przechodzeniu między stronami.

Dostępność

KlawiszDziałanie
Tab
Przechodzi kolejno przez przyciski i linki paska bocznego.
EnterSpacja
Aktywuje element z fokusem albo rozwija lub zwija sekcję.
⌘BCtrlB
Zwija lub rozwija pasek boczny z dowolnego miejsca w aplikacji — chyba że piszesz w polu tekstowym lub edytorze tekstu sformatowanego.
Esc
Na urządzeniach mobilnych zamyka panel z paskiem bocznym i przywraca fokus na wyzwalacz.
  • Punkty orientacyjne. Owiń menu w <nav aria-label="…">; SidebarInset renderuje <main> strony.
  • Bieżąca strona. Oprócz stylizacji isActive ustawia aria-current="page" na przycisku lub linku elementu.
  • Pasek ikon. Zwinięte elementy zachowują tekst w DOM, więc czytniki ekranu nadal odczytują etykietę; podpowiedzi pokazują tę samą nazwę osobom widzącym.
  • Kontrolki zwijania. SidebarTrigger ma etykietę „Pokaż lub ukryj pasek boczny” — w języku ustawionym przez LocaleProvider. SidebarRail to ułatwienie dla myszy i jest wyłączony z kolejności tabulacji — użytkownicy klawiatury mają do dyspozycji wyzwalacz i skrót.
  • Urządzenia mobilne. Panel jest modalnym oknem dialogowym: fokus przechodzi do jego wnętrza, nie może go opuścić, dopóki panel jest otwarty, i wraca na wyzwalacz po zamknięciu.

Dokumentacja API

SidebarProvider

Przechowuje stan otwarcia, zapisuje go w ciasteczku i rejestruje skrót klawiszowy. Renderuje <div>, który układa pasek boczny i obszar strony obok siebie.

PropTypDomyślnie
defaultOpen

Czy pasek boczny na desktopie jest na początku rozwinięty (tryb niekontrolowany).

booleantrue
open

Czy pasek boczny na desktopie jest rozwinięty (tryb kontrolowany). Używaj razem z onOpenChange.

booleanBrak wartości domyślnej
onOpenChange

Wywoływana, gdy pasek boczny na desktopie się rozwija lub zwija.

(open: boolean) => voidBrak wartości domyślnej
style

Nadpisuje --sidebar-width i --sidebar-width-icon.

CSSPropertiesBrak wartości domyślnej

Panel nawigacji. Na urządzeniach mobilnych renderuje się wewnątrz komponentu Sheet.

PropTypDomyślnie
side

Krawędź, przy której znajduje się pasek boczny.

"left" | "right""left"
variant

Wygląd na desktopie.

"sidebar" | "floating" | "inset""sidebar"
collapsible

Sposób zwijania paska na desktopie.

"offcanvas" | "icon" | "none""offcanvas"

SidebarMenuButton

Główna kontrolka elementu menu. Renderuje <button>; do nawigacji przekaż render={<Link />}.

PropTypDomyślnie
isActive

Oznacza element jako bieżący: stylizuje go (data-active) i ustawia aria-current="page".

booleanfalse
variant

outline dodaje cienkie obramowanie.

"default" | "outline""default"
size

Wysokość wiersza: 32 px, 28 px lub 48 px.

"default" | "sm" | "lg""default"
tooltip

Etykieta pokazywana po najechaniu, gdy pasek jest zwinięty do ikon.

string | TooltipContent propsBrak wartości domyślnej
render

Renderuje inny element, na przykład Link z Next.js.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej

SidebarMenuAction

Akcja pomocnicza na końcu elementu menu.

PropTypDomyślnie
showOnHover

Ukrywa akcję do czasu najechania na element lub ustawienia na nim fokusu (tylko na desktopie).

booleanfalse
render

Renderuje inny element, np. DropdownMenuTrigger.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej

SidebarMenuSubButton

Link w podmenu. Domyślnie renderuje <a>.

PropTypDomyślnie
isActive

Oznacza element jako bieżący: stylizuje go i ustawia aria-current="page".

booleanfalse
size

Rozmiar tekstu elementu.

"sm" | "md""md"
render

Renderuje inny element, na przykład Link lub przycisk.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej

SidebarInset

Obszar strony obok paska bocznego. Renderuje <main>; jeśli strona ma już taki element, przekaż render, aby użyć innego.

SidebarMenuSkeleton

Zastępczy wiersz na czas ładowania. showIcon dodaje przed tekstem blok wielkości ikony.

Pozostałe części

SidebarHeader, SidebarFooter, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuBadge i SidebarSeparator to ostylowane elementy układu, które przyjmują propsy swojego elementu HTML. SidebarGroupLabel i SidebarGroupAction przyjmują render. SidebarInput to kompaktowy Input. SidebarTrigger to ikonowy Button w wariancie ghost; SidebarRail to krawędź reagująca na najechanie, która zwija i rozwija pasek boczny.

useSidebar

Zwraca stan paska bocznego. Poza SidebarProvider rzuca błąd.

PropTypDomyślnie
state

Stan na desktopie, odzwierciedlony w atrybucie data-state paska.

"expanded" | "collapsed"Brak wartości domyślnej
open / setOpen

Stan otwarcia na desktopie.

boolean / (open: boolean) => voidBrak wartości domyślnej
openMobile / setOpenMobile

Stan panelu na urządzeniach mobilnych.

boolean / (open: boolean) => voidBrak wartości domyślnej
isMobile

Ma wartość true poniżej breakpointu md (768 px).

booleanBrak wartości domyślnej
toggleSidebar

Przełącza stan właściwy dla bieżącej szerokości ekranu.

() => voidBrak wartości domyślnej